Skip to content

Commit

Permalink
Topographical sorting of compositions .
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinJump committed Sep 14, 2023
1 parent 35950ee commit 6a99101
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 5 deletions.
85 changes: 84 additions & 1 deletion uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
using Microsoft.Extensions.Logging;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.Extensions.Logging;

using NPoco.RowMappers;

using NUglify.JavaScript.Syntax;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -16,6 +22,7 @@
using uSync.BackOffice.Configuration;
using uSync.BackOffice.Services;
using uSync.Core;
using uSync.Core.Dependency;
using uSync.Core.Serialization;

namespace uSync.BackOffice.SyncHandlers
Expand Down Expand Up @@ -191,5 +198,81 @@ protected override bool DoItemsMatch(XElement node, TObject item)
if (base.DoItemsMatch(node, item)) return true;
return node.GetAlias().InvariantEquals(GetItemAlias(item));
}

/// <summary>
/// for containers, we are building a dependency graph.
/// </summary>
protected override IList<LeveledFile> GetLevelOrderedFiles(string folder, IList<uSyncAction> actions)
{
var files = syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}");

var nodes = new Dictionary<Guid, LeveledFile>();
var graph = new List<GraphEdge<Guid>>();

foreach (var file in files)
{
var node = LoadNode(file);
if (node == null) continue;

var key = node.GetKey();
nodes.Add(key, new LeveledFile
{
Alias = node.GetAlias(),
File = file,
Level = node.GetLevel(),
});

// you can have circular dependencies in structure :(
// graph.AddRange(GetStructure(node).Select(x => GraphEdge.Create(key, x)));

graph.AddRange(GetCompositions(node).Select(x => GraphEdge.Create(key, x)));
}

var cleanGraph = graph.Where(x => x.Node != x.Edge).ToList();
var sortedList = nodes.Keys.TopologicalSort(cleanGraph);

if (sortedList == null)
return nodes.Values.OrderBy(x => x.Level).ToList();

var result = new List<LeveledFile>();
foreach(var key in sortedList)
{
if (nodes.ContainsKey(key))
result.Add(nodes[key]);
}
return result;

}

private IEnumerable<Guid> GetStructure(XElement node)
{

var structure = node.Element("Structure");
if (structure == null) return Enumerable.Empty<Guid>();

return GetKeys(structure);
}

private IEnumerable<Guid> GetCompositions(XElement node)
{
var compositionNode = node.Element("Info")?.Element("Compositions");
if (compositionNode == null) return Enumerable.Empty<Guid>();

return GetKeys(compositionNode);
}

private IEnumerable<Guid> GetKeys(XElement node)
{
if (node != null)
{
foreach (var item in node.Elements())
{
var key = item.Attribute("Key").ValueOrDefault(Guid.Empty);
if (key == Guid.Empty) continue;

yield return key;
}
}
}
}
}
6 changes: 3 additions & 3 deletions uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ protected override IEnumerable<uSyncAction> ImportFolder(string folder, HandlerS
/// <summary>
/// Get all the files in a folder and return them sorted by their level
/// </summary>
private IList<LeveledFile> GetLevelOrderedFiles(string folder, IList<uSyncAction> actions)
protected virtual IList<LeveledFile> GetLevelOrderedFiles(string folder, IList<uSyncAction> actions)
{
List<LeveledFile> nodes = new List<LeveledFile>();

Expand Down Expand Up @@ -162,14 +162,14 @@ private IList<LeveledFile> GetLevelOrderedFiles(string folder, IList<uSyncAction
return nodes.OrderBy(x => x.Level).ToList();
}

private class LeveledFile
protected class LeveledFile

Check warning on line 165 in uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'SyncHandlerLevelBase<TObject, TService>.LeveledFile'
{
public string Alias { get; set; }

Check warning on line 167 in uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'SyncHandlerLevelBase<TObject, TService>.LeveledFile.Alias'
public int Level { get; set; }

Check warning on line 168 in uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'SyncHandlerLevelBase<TObject, TService>.LeveledFile.Level'
public string File { get; set; }

Check warning on line 169 in uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'SyncHandlerLevelBase<TObject, TService>.LeveledFile.File'
}

private XElement LoadNode(string path)
protected XElement LoadNode(string path)

Check warning on line 172 in uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'SyncHandlerLevelBase<TObject, TService>.LoadNode(string)'
{
syncFileService.EnsureFileExists(path);

Expand Down
64 changes: 64 additions & 0 deletions uSync.Core/Dependency/DependencyGraph.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace uSync.Core.Dependency;

/// <summary>
/// builds a graph of dependencies, so things can be installed in order.
/// </summary>
public static class DependencyGraph
{
public static List<T> TopologicalSort<T>(this ICollection<T> nodes, ICollection<GraphEdge<T>> edges)
where T : IEquatable<T>
{
var sortedList = new List<T>();

// all items where they don't have a dependency on themselves.
var queue = new Queue<T>(
nodes.Where(x => edges.All(e => e.Node.Equals(x) == false)));


while (queue.Any())
{
// remove this item add it to the queue.
var next = queue.Dequeue();
sortedList.Add(next);

// look for any edges for this queue.
foreach(var edge in edges.Where(e => e.Edge.Equals(next)).ToList())
{
var dependency = edge.Node;
edges.Remove(edge);

if (edges.All(x => x.Node.Equals(dependency) == false))
{
queue.Enqueue(dependency);
}
}
}

if (edges.Any())
{
return null;
}

return sortedList;

}
}

public class GraphEdge<T>
where T : IEquatable<T>
{
public T Node { get; set; }
public T Edge { get; set; }

}

public class GraphEdge
{
public static GraphEdge<T> Create<T>(T node, T edge) where T : IEquatable<T>
=> new GraphEdge<T> { Node = node, Edge = edge };
}

Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public override SyncAttempt<IContentType> DeserializeSecondPass(IContentType ite

SetSafeAliasValue(item, node, false);

details.AddRange(DeserializeCompositions(item, node));
// details.AddRange(DeserializeCompositions(item, node));
details.AddRange(DeserializeStructure(item, node));

// When doing this reflectiony - it doesn't set is dirty.
Expand Down
89 changes: 89 additions & 0 deletions uSync.Tests/Extensions/SortingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;

using NUnit.Framework;

using Umbraco.Cms.Core.WebAssets;

using uSync.Core.Dependency;

namespace uSync.Tests.Extensions;

[TestFixture]
public class SortingTests
{
private HashSet<int> _nodes = new HashSet<int>
{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};

private List<Guid> _guidNodes = new List<Guid>
{
Guid.Parse("{5E37F691-FF91-45DA-9F53-8641FD9FE233}"), // 0
Guid.Parse("{5A35701C-349C-4AAE-BBCE-964B5C196989}"), // 1
Guid.Parse("{C70BE6CF-4923-4E2B-8742-B90FB7BBAFCB}"), // 2
Guid.Parse("{DE77C37C-DBC7-4806-8B0F-332BC38A87A4}"), // 3
Guid.Parse("{3C90BAD8-09F2-486E-BAA7-474F0D423C4D}") // 4
};

[Test]
public void GraphSortGuidNodes()
{
var graph = new List<GraphEdge<Guid>>();

graph.Add(GraphEdge.Create(_guidNodes[2], _guidNodes[1]));
graph.Add(GraphEdge.Create(_guidNodes[2], _guidNodes[4]));

var result = _guidNodes.TopologicalSort(graph);

var expected = new List<Guid>
{
Guid.Parse("{5E37F691-FF91-45DA-9F53-8641FD9FE233}"), // 0
Guid.Parse("{5A35701C-349C-4AAE-BBCE-964B5C196989}"), // 1
Guid.Parse("{DE77C37C-DBC7-4806-8B0F-332BC38A87A4}"), // 3
Guid.Parse("{3C90BAD8-09F2-486E-BAA7-474F0D423C4D}"), // 4
Guid.Parse("{C70BE6CF-4923-4E2B-8742-B90FB7BBAFCB}"), // 2
};

Assert.AreEqual(expected, result);
}

[Test]
public void GraphSortNodes()
{
var graph = new HashSet<GraphEdge<int>>(
new[]
{
GraphEdge.Create(2,4),
GraphEdge.Create(4,7),
GraphEdge.Create(5,8),
GraphEdge.Create(6,9),
});


var result = _nodes.TopologicalSort(graph);
var expected = new List<int> {
1, 3, 7, 8, 9, 10, 4, 5, 6, 2
};

Assert.AreEqual(expected, result);
}

[Test]
public void CircularDependencyReturnsNull()
{
var graph = new HashSet<GraphEdge<int>>(
new[]
{
GraphEdge.Create(2,4),
GraphEdge.Create(4,7),
GraphEdge.Create(5,8),
GraphEdge.Create(6,9),
GraphEdge.Create(7,2), // 7 can't depend on 2 and it depends on 4 which depends on 7
});

var result = _nodes.TopologicalSort(graph);

Assert.IsNull(result);
}
}

0 comments on commit 6a99101

Please sign in to comment.