From 6a99101fde32f4ee9205e0d57f1d418944c7e986 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Thu, 14 Sep 2023 14:25:38 +0100 Subject: [PATCH] Topographical sorting of compositions . --- .../SyncHandlers/SyncHandlerContainerBase.cs | 85 +++++++++++++++++- .../SyncHandlers/SyncHandlerLevelBase.cs | 6 +- uSync.Core/Dependency/DependencyGraph.cs | 64 +++++++++++++ .../Serializers/ContentTypeSerializer.cs | 2 +- uSync.Tests/Extensions/SortingTests.cs | 89 +++++++++++++++++++ 5 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 uSync.Core/Dependency/DependencyGraph.cs create mode 100644 uSync.Tests/Extensions/SortingTests.cs diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs index d69d7021..00894fff 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs @@ -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; @@ -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 @@ -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)); } + + /// + /// for containers, we are building a dependency graph. + /// + protected override IList GetLevelOrderedFiles(string folder, IList actions) + { + var files = syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}"); + + var nodes = new Dictionary(); + var graph = new List>(); + + 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(); + foreach(var key in sortedList) + { + if (nodes.ContainsKey(key)) + result.Add(nodes[key]); + } + return result; + + } + + private IEnumerable GetStructure(XElement node) + { + + var structure = node.Element("Structure"); + if (structure == null) return Enumerable.Empty(); + + return GetKeys(structure); + } + + private IEnumerable GetCompositions(XElement node) + { + var compositionNode = node.Element("Info")?.Element("Compositions"); + if (compositionNode == null) return Enumerable.Empty(); + + return GetKeys(compositionNode); + } + + private IEnumerable 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; + } + } + } } } diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs index 3d274fc6..3bb89254 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs @@ -130,7 +130,7 @@ protected override IEnumerable ImportFolder(string folder, HandlerS /// /// Get all the files in a folder and return them sorted by their level /// - private IList GetLevelOrderedFiles(string folder, IList actions) + protected virtual IList GetLevelOrderedFiles(string folder, IList actions) { List nodes = new List(); @@ -162,14 +162,14 @@ private IList GetLevelOrderedFiles(string folder, IList x.Level).ToList(); } - private class LeveledFile + protected class LeveledFile { public string Alias { get; set; } public int Level { get; set; } public string File { get; set; } } - private XElement LoadNode(string path) + protected XElement LoadNode(string path) { syncFileService.EnsureFileExists(path); diff --git a/uSync.Core/Dependency/DependencyGraph.cs b/uSync.Core/Dependency/DependencyGraph.cs new file mode 100644 index 00000000..f1e25443 --- /dev/null +++ b/uSync.Core/Dependency/DependencyGraph.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace uSync.Core.Dependency; + +/// +/// builds a graph of dependencies, so things can be installed in order. +/// +public static class DependencyGraph +{ + public static List TopologicalSort(this ICollection nodes, ICollection> edges) + where T : IEquatable + { + var sortedList = new List(); + + // all items where they don't have a dependency on themselves. + var queue = new Queue( + 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 + where T : IEquatable +{ + public T Node { get; set; } + public T Edge { get; set; } + +} + +public class GraphEdge +{ + public static GraphEdge Create(T node, T edge) where T : IEquatable + => new GraphEdge { Node = node, Edge = edge }; +} + diff --git a/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs b/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs index 0baf4e35..07bc4ab1 100644 --- a/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs +++ b/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs @@ -151,7 +151,7 @@ public override SyncAttempt 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. diff --git a/uSync.Tests/Extensions/SortingTests.cs b/uSync.Tests/Extensions/SortingTests.cs new file mode 100644 index 00000000..a8a05e3b --- /dev/null +++ b/uSync.Tests/Extensions/SortingTests.cs @@ -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 _nodes = new HashSet + { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + }; + + private List _guidNodes = new List + { + 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>(); + + 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.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>( + 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 { + 1, 3, 7, 8, 9, 10, 4, 5, 6, 2 + }; + + Assert.AreEqual(expected, result); + } + + [Test] + public void CircularDependencyReturnsNull() + { + var graph = new HashSet>( + 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); + } +}