diff --git a/ArchUnitNET/Domain/Namespace.cs b/ArchUnitNET/Domain/Namespace.cs index db8afdd44..b958c1a07 100644 --- a/ArchUnitNET/Domain/Namespace.cs +++ b/ArchUnitNET/Domain/Namespace.cs @@ -1,13 +1,83 @@ using System.Collections.Generic; +using System.Linq; +using ArchUnitNET.Domain.Dependencies; namespace ArchUnitNET.Domain { - public class Namespace : Slice, IHasName + /// + /// A namespace of the loaded architecture together with the types declared directly in it. + /// Namespaces are equal exactly when their full names are. + /// + public class Namespace : IHasName, IHasDescription, IHasDependencies { + private readonly string _name; + private readonly List _types; + public Namespace(string name, IEnumerable types) - : base(SliceIdentifier.Of(name), types) { } + { + _name = name; + _types = types.ToList(); + } + + /// The full name of this namespace. + public string Name => _name; + + /// The full name of this namespace. + public string FullName => _name; + + /// The full name of this namespace. + public string Description => _name; + + /// The types declared directly in this namespace. + public IEnumerable Types => _types; + + /// The classes declared directly in this namespace. + public IEnumerable Classes => Types.OfType(); + + /// The interfaces declared directly in this namespace. + public IEnumerable Interfaces => Types.OfType(); + + /// Every dependency going out of this namespace's types, including internal ones. + public List Dependencies => + Types.SelectMany(type => type.Dependencies).ToList(); + + /// Every dependency pointing at this namespace's types. + public List BackwardsDependencies => + Types.SelectMany(type => type.BackwardsDependencies).ToList(); + + internal void AddType(IType type) + { + _types.Add(type); + } + + private bool Equals(Namespace other) + { + return FullName == other.FullName; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return obj.GetType() == GetType() && Equals((Namespace)obj); + } + + public override int GetHashCode() + { + return FullName != null ? FullName.GetHashCode() : 0; + } - public string Name => Description; - public string FullName => Description; + public override string ToString() + { + return FullName; + } } } diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs index b02092c59..7432a2354 100644 --- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs +++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs @@ -139,13 +139,15 @@ public PlantUmlFileBuilder WithDependenciesFrom( foreach (var slice in _sliceList) { var sliceIsPackage = IsPackage(slice); - var dependencyTargets = SelectDependencies(slice, generationOptions); + var dependencyTargets = SelectDependencies( + slice, + _sliceList, + target => target.Description, + target => target.Types, + generationOptions + ); - if (slice is Namespace) //This throws errors if namespaces and slices are used in the same diagram, the syntax can't be mixed in PlantUML - { - nodes.Add(slice, new PlantUmlNamespace(slice.Description)); - } - else if (!sliceIsPackage) + if (!sliceIsPackage) { var slc = new PlantUmlSlice(slice.Description, slice.NameSpace); if (generationOptions.C4Style) @@ -222,11 +224,9 @@ public PlantUmlFileBuilder WithDependenciesFrom( if (!generationOptions.IncludeNodesWithoutDependencies) { - nodes = RemoveNodesNotContainedInDependencies(nodes); + RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description); } - var nodeElements = HandleNodes(nodes); - if (!generationOptions.LimitDependencies) { RemoveDuplicateDependenciesWhenShowingPackages(); @@ -234,10 +234,94 @@ public PlantUmlFileBuilder WithDependenciesFrom( } RemoveDuplicatedArrowsIfExist(); - _diagram.AddElements( - nodeElements.OrderBy(element => - element is PlantUmlNamespace @namespace ? @namespace.Name.Length : -1 + _diagram.AddElements(nodes.Values); + _diagram.AddElements(_dependencies); + + return this; + } + + /// + /// Draws the given namespaces as nested PlantUML namespaces, with an arrow from one + /// namespace to another wherever one of its types depends on one of the other's. Parent + /// namespaces that are not in the list are added so that the nesting is complete. + /// + /// The namespaces to draw, e.g. . + /// Filters and layout options; the defaults if null. + /// does not apply to namespaces. + public PlantUmlFileBuilder WithDependenciesFrom( + IEnumerable namespaces, + GenerationOptions generationOptions = null + ) + { + var namespaceList = namespaces.Distinct().ToList(); + if (generationOptions == null) + { + generationOptions = new GenerationOptions(); + } + + var dependencyType = generationOptions.LimitDependencies + ? DependencyType.OneToOneCompact + : DependencyType.OneToOne; + var nodes = new Dictionary(); + + foreach (var ns in namespaceList) + { + nodes.Add(ns, new PlantUmlNamespace(ns.FullName)); + _dependencies.AddRange( + SelectDependencies( + ns, + namespaceList, + target => target.FullName, + target => target.Types, + generationOptions + ) + .Select(target => new PlantUmlDependency( + ns.FullName, + target.FullName, + dependencyType + )) + ); + } + + if (!generationOptions.IncludeNodesWithoutDependencies) + { + RemoveNodesNotContainedInDependencies(nodes, ns => ns.FullName); + } + + var nodeElements = nodes.Values.ToList(); + foreach (var ns in nodes.Keys) + { + var namespaceName = ns.FullName; + var dotIndex = namespaceName.Length; + var parentNamespaces = new List(); + while (dotIndex != -1) + { + namespaceName = namespaceName.Substring(0, dotIndex); + parentNamespaces.Add(namespaceName); + dotIndex = namespaceName.LastIndexOf('.'); + } + + parentNamespaces.Reverse(); + foreach ( + var parent in parentNamespaces.Where(parentName => + nodeElements + .OfType() + .All(element => element.Name != parentName) + ) ) + { + nodeElements.Add(new PlantUmlNamespace(parent)); + } + } + + if (!generationOptions.LimitDependencies) + { + ReplaceCirclesWithAppropriateDependencyType(); + } + RemoveDuplicatedArrowsIfExist(); + + _diagram.AddElements( + nodeElements.OrderBy(element => ((PlantUmlNamespace)element).Name.Length) ); _diagram.AddElements(_dependencies); @@ -282,7 +366,13 @@ string package foreach (var slice in _sliceList) { var sliceIsPackage = IsPackage(slice); - var dependencyTargets = SelectDependencies(slice, generationOptions); + var dependencyTargets = SelectDependencies( + slice, + _sliceList, + target => target.Description, + target => target.Types, + generationOptions + ); if (sliceIsPackage) { @@ -305,11 +395,7 @@ string package ); } - if (slice is Namespace) //This throws errors if namespaces and slices are used in the same diagram, the syntax can't be mixed in PlantUML - { - nodes.Add(slice, new PlantUmlNamespace(slice.Description)); - } - else if (!sliceIsPackage) + if (!sliceIsPackage) { nodes.Add( slice, @@ -326,7 +412,7 @@ string package if (!generationOptions.IncludeNodesWithoutDependencies) { - nodes = RemoveNodesNotContainedInDependencies(nodes); + RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description); } RemoveDuplicateDependenciesWhenShowingPackages(); @@ -356,13 +442,7 @@ string package } } - var nodeElements = HandleNodes(nodes); - - _diagram.AddElements( - nodeElements.OrderBy(element => - element is PlantUmlNamespace @namespace ? @namespace.Name.Length : -1 - ) - ); + _diagram.AddElements(nodes.Values); var tempList = _sliceList .Where(slice => IsPackage(slice) && slice.Description.Contains(package)) @@ -381,72 +461,52 @@ string package return this; } - private Dictionary RemoveNodesNotContainedInDependencies( - Dictionary nodes + /// + /// Removes the nodes whose name no dependency starts or ends at. + /// + private void RemoveNodesNotContainedInDependencies( + Dictionary nodes, + Func nameOf ) { + var namesInDependencies = new HashSet( + _dependencies.SelectMany(dep => new[] { dep.Origin, dep.Target }) + ); + // Collected first: removing entries while enumerating the dictionary throws on + // .NET Framework. foreach ( - var entry in nodes.Where(node => - !_dependencies - .SelectMany(dep => new[] { dep.Origin, dep.Target }) - .Contains(node.Key.Description) - ) + var key in nodes + .Keys.Where(key => !namesInDependencies.Contains(nameOf(key))) + .ToList() ) { - nodes.Remove(entry.Key); + nodes.Remove(key); } - return nodes; } - private static IEnumerable HandleNodes( - Dictionary nodes + /// + /// The candidates, other than itself, that one of the origin's + /// dependencies passing points into. + /// + private static IEnumerable SelectDependencies( + T origin, + IEnumerable candidates, + Func nameOf, + Func> typesOf, + GenerationOptions generationOptions ) + where T : IHasDependencies { - var nodeElements = nodes.Values.ToList(); - - foreach (var node in nodes) - { - if (node.Key is Namespace) - { - var namespaceName = node.Key.Description; - var dotIndex = namespaceName.Length; - var parentNamespaces = new List(); - while (dotIndex != -1) - { - namespaceName = namespaceName.Substring(0, dotIndex); - parentNamespaces.Add(namespaceName); - dotIndex = namespaceName.LastIndexOf('.'); - } - - parentNamespaces.Reverse(); - foreach ( - var namespc in parentNamespaces.Where(namespcName => - nodeElements - .OfType() - .All(element => element.Name != namespcName) - ) - ) - { - nodeElements.Add(new PlantUmlNamespace(namespc)); - } - } - } - - return nodeElements; - } - - private IEnumerable SelectDependencies( - Slice slice, - GenerationOptions generationOptions - ) => - _sliceList.Where(targetSlice => - targetSlice.Description != slice.Description - && slice + var originName = nameOf(origin); + return candidates.Where(target => + nameOf(target) != originName + && origin .Dependencies.Where(dep => generationOptions.DependencyFilter?.Invoke(dep) ?? true ) - .Any(dep => targetSlice.Types.Contains(dep.Target)) + .Any(dep => typesOf(target).Contains(dep.Target)) ); + } private void RemovePatternInappropriateSlices(string thatContainsThisString = null) { diff --git a/ArchUnitNET/Loader/TypeProcessor.cs b/ArchUnitNET/Loader/TypeProcessor.cs index dceed9363..27c49304e 100644 --- a/ArchUnitNET/Loader/TypeProcessor.cs +++ b/ArchUnitNET/Loader/TypeProcessor.cs @@ -1119,7 +1119,7 @@ internal static void AddTypesToNamespaces(IEnumerable types) { foreach (var type in types) { - ((List)type.Namespace.Types).Add(type); + type.Namespace.AddType(type); } } } diff --git a/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs index 601349c51..64302e59f 100644 --- a/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs +++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs @@ -55,6 +55,16 @@ public void BuildUmlByNamespacesTest() Assert.NotEmpty(uml); } + [Fact] + public Task BuildUmlByNamespacesSnapshotTest() + { + var namespaces = StaticTestArchitectures.SlicesTestArchitecture.Namespaces.Where(ns => + ns.FullName.StartsWith("SlicesTestAssembly.MultipleSubnamespaces") + ); + var uml = new PlantUmlFileBuilder().WithDependenciesFrom(namespaces).AsString(); + return Verifier.Verify(uml).DisableDiff().UseDirectory("Snapshots"); + } + [Fact] public void BuildUmlBySlicesTest() { diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.BuildUmlByNamespacesSnapshotTest.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.BuildUmlByNamespacesSnapshotTest.verified.txt new file mode 100644 index 000000000..5b87b1575 --- /dev/null +++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.BuildUmlByNamespacesSnapshotTest.verified.txt @@ -0,0 +1,35 @@ +@startuml + +!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml + +HIDE_STEREOTYPE() + +namespace SlicesTestAssembly { +} +namespace SlicesTestAssembly.MultipleSubnamespaces { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3 { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice2 { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice1 { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner { +} +namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner { +} +[SlicesTestAssembly.MultipleSubnamespaces.Slice3] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice1] +[SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice3] +[SlicesTestAssembly.MultipleSubnamespaces.Slice2] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice3] +[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice3] +[SlicesTestAssembly.MultipleSubnamespaces.Slice1] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2] +[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] +@enduml diff --git a/ArchUnitNETTests/Loader/NamespaceTests.cs b/ArchUnitNETTests/Loader/NamespaceTests.cs index a5c56c426..4d29d6bfd 100644 --- a/ArchUnitNETTests/Loader/NamespaceTests.cs +++ b/ArchUnitNETTests/Loader/NamespaceTests.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; +using ArchUnitNET.Domain; using Xunit; namespace ArchUnitNETTests.Loader @@ -25,5 +27,21 @@ public void NoEmptyNamespacesTest() ) ); } + + [Fact] + public void NamespacesAreEqualByName() + { + var ns = new Namespace("A.B", new List()); + var sameName = new Namespace("A.B", new List()); + var otherName = new Namespace("A.C", new List()); + + Assert.Equal(ns, sameName); + Assert.Equal(ns.GetHashCode(), sameName.GetHashCode()); + Assert.NotEqual(ns, otherName); + Assert.Equal("A.B", ns.Name); + Assert.Equal("A.B", ns.FullName); + Assert.Equal("A.B", ns.Description); + Assert.Equal("A.B", ns.ToString()); + } } }