diff --git a/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs b/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs
index 44348f502..1b995aa7b 100644
--- a/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs
+++ b/ArchUnitNET/Domain/PlantUml/Export/GenerationOptions.cs
@@ -10,5 +10,14 @@ public class GenerationOptions
public bool IncludeNodesWithoutDependencies { get; set; } = true;
public bool LimitDependencies { get; set; } = false;
public bool C4Style { get; set; } = false;
+
+ ///
+ /// Whether slice diagrams nest each slice in package blocks along the namespace its
+ /// types matched, instead of drawing every slice on its own under its name. A slice whose
+ /// types matched different namespaces before the first capture group, or that does not
+ /// come from a slice pattern, is drawn on its own either way. Slices of the obsolete
+ /// MatchingWithPackages are always nested.
+ ///
+ public bool NestSlicesInPackages { get; set; } = false;
}
}
diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs
index 7432a2354..fc758aa6f 100644
--- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs
+++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs
@@ -15,6 +15,12 @@ public class PlantUmlFileBuilder
private List _sliceList = new List();
+ ///
+ /// The name each slice is drawn under, and the root package it is nested in, if any.
+ ///
+ private Dictionary _diagramNames =
+ new Dictionary();
+
public string AsString(RenderOptions renderOptions = null)
{
if (renderOptions == null)
@@ -122,13 +128,21 @@ public PlantUmlFileBuilder WithDependenciesFrom(
return this;
}
+ ///
+ /// Draws the given slices as components, with an arrow from one slice to another wherever
+ /// one of its types depends on one of the other's.
+ ///
+ /// The slices to draw, e.g. from GivenSlices.GetObjects.
+ /// Filters and layout options; the defaults if null.
+ /// With , slices from a slice pattern
+ /// are nested in package blocks along the namespace they matched.
+ /// Two slices would be drawn under the same name.
public PlantUmlFileBuilder WithDependenciesFrom(
IEnumerable slices,
GenerationOptions generationOptions = null
)
{
_sliceList = slices.Distinct().ToList();
- RemovePatternInappropriateSlices();
var nodes = new Dictionary();
if (generationOptions == null)
@@ -136,20 +150,23 @@ public PlantUmlFileBuilder WithDependenciesFrom(
generationOptions = new GenerationOptions();
}
+ AssignDiagramNames(generationOptions.NestSlicesInPackages);
+ RemovePatternInappropriateSlices();
+
foreach (var slice in _sliceList)
{
var sliceIsPackage = IsPackage(slice);
var dependencyTargets = SelectDependencies(
slice,
_sliceList,
- target => target.Description,
+ NameOf,
target => target.Types,
generationOptions
);
if (!sliceIsPackage)
{
- var slc = new PlantUmlSlice(slice.Description, slice.NameSpace);
+ var slc = new PlantUmlSlice(NameOf(slice), NameSpaceOf(slice));
if (generationOptions.C4Style)
{
slc.UseS4Style();
@@ -163,8 +180,8 @@ public PlantUmlFileBuilder WithDependenciesFrom(
{
_dependencies.AddRange(
dependencyTargets.Select(target => new PlantUmlDependency(
- slice.Description,
- target.Description,
+ NameOf(slice),
+ NameOf(target),
DependencyType.PackageToOne
))
);
@@ -173,21 +190,21 @@ public PlantUmlFileBuilder WithDependenciesFrom(
{
_dependencies.AddRange(
dependencyTargets.Select(target => new PlantUmlDependency(
- slice.Description,
- target.Description,
+ NameOf(slice),
+ NameOf(target),
DependencyType.OneToOne
))
);
}
}
- else if (slice.ContainsNamespace())
+ else if (NameSpaceOf(slice) != null)
{
if (sliceIsPackage)
{
_dependencies.AddRange(
dependencyTargets.Select(target => new PlantUmlDependency(
- slice.Description,
- target.Description,
+ NameOf(slice),
+ NameOf(target),
DependencyType.PackageToPackageIfSameParentNamespace
))
);
@@ -196,15 +213,15 @@ public PlantUmlFileBuilder WithDependenciesFrom(
{
var dt = dependencyTargets
.Select(target => new PlantUmlDependency(
- slice.Description,
- target.Description,
+ NameOf(slice),
+ NameOf(target),
DependencyType.OneToOneIfSameParentNamespace
))
.Where(dependency =>
dependency.OriginCountOfDots() < dependency.TargetCountOfDots()
|| _sliceList.All(slc =>
- slc.Description == dependency.Target
- || !slc.Description.Contains(dependency.Target)
+ NameOf(slc) == dependency.Target
+ || !NameOf(slc).Contains(dependency.Target)
)
);
_dependencies.AddRange(dt);
@@ -214,8 +231,8 @@ public PlantUmlFileBuilder WithDependenciesFrom(
{
_dependencies.AddRange(
dependencyTargets.Select(target => new PlantUmlDependency(
- slice.Description,
- target.Description,
+ NameOf(slice),
+ NameOf(target),
DependencyType.OneToOneCompact
))
);
@@ -224,7 +241,7 @@ public PlantUmlFileBuilder WithDependenciesFrom(
if (!generationOptions.IncludeNodesWithoutDependencies)
{
- RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description);
+ RemoveNodesNotContainedInDependencies(nodes, NameOf);
}
if (!generationOptions.LimitDependencies)
@@ -328,12 +345,25 @@ var parent in parentNamespaces.Where(parentName =>
return this;
}
+ ///
+ /// Draws only the dependencies from and to the slices whose diagram name contains
+ /// , and colors those slices.
+ ///
+ /// The slices to draw, e.g. from GivenSlices.GetObjects.
+ /// The text to focus on. When slices are nested in packages, their
+ /// diagram names are fully qualified, so this has to be as well.
+ /// Only
+ /// is read; the defaults if null.
+ /// The package is empty, a single dot or not contained
+ /// in any slice's diagram name, or two slices would be drawn under the same name.
public PlantUmlFileBuilder WithDependenciesFromFocusOn(
IEnumerable slices,
- string package
+ string package,
+ GenerationOptions generationOptions = null
)
{
_sliceList = slices.Distinct().ToList();
+ AssignDiagramNames(generationOptions?.NestSlicesInPackages ?? false);
switch (package)
{
@@ -351,7 +381,7 @@ string package
RemovePatternInappropriateSlices(package);
var existPackage = _sliceList.Any(slice =>
- (slice.NameSpace + slice.Description).Contains(package)
+ (NameSpaceOf(slice) + NameOf(slice)).Contains(package)
);
if (!existPackage)
{
@@ -361,7 +391,7 @@ string package
}
var nodes = new Dictionary();
- var generationOptions = new GenerationOptions();
+ var defaultOptions = new GenerationOptions();
foreach (var slice in _sliceList)
{
@@ -369,17 +399,17 @@ string package
var dependencyTargets = SelectDependencies(
slice,
_sliceList,
- target => target.Description,
+ NameOf,
target => target.Types,
- generationOptions
+ defaultOptions
);
if (sliceIsPackage)
{
_dependencies.AddRange(
dependencyTargets.Select(target => new PlantUmlDependency(
- slice.Description,
- target.Description,
+ NameOf(slice),
+ NameOf(target),
DependencyType.PackageToOne
))
);
@@ -388,8 +418,8 @@ string package
{
_dependencies.AddRange(
dependencyTargets.Select(target => new PlantUmlDependency(
- slice.Description,
- target.Description,
+ NameOf(slice),
+ NameOf(target),
DependencyType.OneToOne
))
);
@@ -399,20 +429,20 @@ string package
{
nodes.Add(
slice,
- slice.Description.Contains(package)
- ? new PlantUmlSlice(slice.Description, slice.NameSpace, "99ffd1")
- : new PlantUmlSlice(slice.Description, slice.NameSpace)
+ NameOf(slice).Contains(package)
+ ? new PlantUmlSlice(NameOf(slice), NameSpaceOf(slice), "99ffd1")
+ : new PlantUmlSlice(NameOf(slice), NameSpaceOf(slice))
);
}
- else if (!slice.Description.Contains(package))
+ else if (!NameOf(slice).Contains(package))
{
- nodes.Add(slice, new PlantUmlSlice(slice.Description + ".", slice.NameSpace));
+ nodes.Add(slice, new PlantUmlSlice(NameOf(slice) + ".", NameSpaceOf(slice)));
}
}
- if (!generationOptions.IncludeNodesWithoutDependencies)
+ if (!defaultOptions.IncludeNodesWithoutDependencies)
{
- RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description);
+ RemoveNodesNotContainedInDependencies(nodes, NameOf);
}
RemoveDuplicateDependenciesWhenShowingPackages();
@@ -433,8 +463,8 @@ string package
for (var i = nodes.Count - 1; i >= 0; i--)
{
var nodeExist = _dependencies.Any(dep =>
- nodes.ElementAt(i).Key.Description == dep.Origin
- || nodes.ElementAt(i).Key.Description == dep.Target
+ NameOf(nodes.ElementAt(i).Key) == dep.Origin
+ || NameOf(nodes.ElementAt(i).Key) == dep.Target
);
if (!nodeExist)
{
@@ -445,10 +475,10 @@ string package
_diagram.AddElements(nodes.Values);
var tempList = _sliceList
- .Where(slice => IsPackage(slice) && slice.Description.Contains(package))
+ .Where(slice => IsPackage(slice) && NameOf(slice).Contains(package))
.Select(slice => new PlantUmlSlice(
- slice.Description + ".",
- slice.NameSpace,
+ NameOf(slice) + ".",
+ NameSpaceOf(slice),
"99ffd1"
))
.ToList();
@@ -517,7 +547,7 @@ private void RemovePatternInappropriateSlices(string thatContainsThisString = nu
if (
thatContainsThisString != null
&& !_sliceList.Any(slice =>
- (slice.NameSpace + slice.Description).Contains(thatContainsThisString)
+ (NameSpaceOf(slice) + NameOf(slice)).Contains(thatContainsThisString)
)
)
{
@@ -530,14 +560,15 @@ private void RemovePatternInappropriateSlices(string thatContainsThisString = nu
if (
thatContainsThisString != null
- && !(_sliceList[i].Description).Contains(thatContainsThisString)
+ && !NameOf(_sliceList[i]).Contains(thatContainsThisString)
)
{
continue;
}
+ // The description is never qualified, so its dots are those below the namespace
+ // the pattern matched before its capture group.
dots += _sliceList[i].Description.Count(c => c == '.');
- dots -= (_sliceList[i].NameSpace ?? string.Empty).Count(c => c == '.');
if (dots >= _sliceList[i].CountOfAsteriskInPattern)
{
@@ -565,7 +596,7 @@ private void RemoveDuplicatedArrowsIfExist()
private void RemoveDuplicateDependenciesWhenShowingPackages()
{
- if (_sliceList.Any(slice => !slice.ContainsNamespace()))
+ if (_sliceList.Any(slice => NameSpaceOf(slice) == null))
{
return;
}
@@ -574,8 +605,8 @@ private void RemoveDuplicateDependenciesWhenShowingPackages()
{
if (
_sliceList.Any(slice =>
- slice.Description.Contains(_dependencies[j].Target)
- && slice.Description != _dependencies[j].Target
+ NameOf(slice).Contains(_dependencies[j].Target)
+ && NameOf(slice) != _dependencies[j].Target
)
)
{
@@ -686,9 +717,79 @@ private void ReplaceCirclesWithAppropriateDependencyType()
}
}
+ ///
+ /// Decides for every slice the name it is drawn under and the root package it is nested
+ /// in, if any.
+ ///
+ ///
+ ///
+ /// The slice exporter works out the layout from these names alone: which slice is the
+ /// package of another, which arrows to merge, how deep LimitDependencies goes. A
+ /// nested slice is therefore drawn under its qualified name, the namespace its types
+ /// matched before the first capture group followed by its
+ /// , e.g. "App.Orders.Http", with that namespace
+ /// as its root package. Every other slice is drawn under its description, without a
+ /// package.
+ ///
+ ///
+ /// Qualified names of one pattern are distinct, but a description can coincide with a
+ /// qualified name. Drawing two slices as one node would be wrong, so that is rejected.
+ ///
+ ///
+ ///
+ /// Whether the options ask for nesting; slices of MatchingWithPackages are nested
+ /// regardless.
+ ///
+ private void AssignDiagramNames(bool nestSlicesInPackages)
+ {
+ var nest = nestSlicesInPackages || _sliceList.Any(slice => slice.NestByDefault);
+ _diagramNames = new Dictionary();
+ var slicesByName = new Dictionary();
+ foreach (var slice in _sliceList)
+ {
+ var nameSpace = slice.DiagramNamespace;
+ var diagramPath = slice.Identifier.DiagramPath;
+ (string Name, string NameSpace) diagramName =
+ nest && nameSpace != null && diagramPath != null
+ ? (nameSpace + diagramPath, nameSpace == "" ? null : nameSpace)
+ : (slice.Description, null);
+
+ if (slicesByName.TryGetValue(diagramName.Name, out var other))
+ {
+ throw new ArgumentException(
+ "The slices \""
+ + other.Identifier.Identifier
+ + "\" and \""
+ + slice.Identifier.Identifier
+ + "\" would both be drawn as \""
+ + diagramName.Name
+ + "\"."
+ );
+ }
+
+ slicesByName.Add(diagramName.Name, slice);
+ _diagramNames.Add(slice, diagramName);
+ }
+ }
+
+ /// The name the diagram draws a slice under; see .
+ private string NameOf(Slice slice)
+ {
+ return _diagramNames[slice].Name;
+ }
+
+ ///
+ /// The root package a slice is nested in, with a trailing ".", or null if it is
+ /// drawn on its own.
+ ///
+ private string NameSpaceOf(Slice slice)
+ {
+ return _diagramNames[slice].NameSpace;
+ }
+
private bool IsPackage(Slice slice)
{
- if (!slice.ContainsNamespace())
+ if (NameSpaceOf(slice) == null)
{
return false;
}
@@ -696,8 +797,8 @@ private bool IsPackage(Slice slice)
for (var i = _sliceList.Count - 1; i >= 0; i--)
{
if (
- _sliceList[i].Description != slice.Description
- & _sliceList[i].Description.StartsWith(slice.Description)
+ NameOf(_sliceList[i]) != NameOf(slice)
+ & NameOf(_sliceList[i]).StartsWith(NameOf(slice))
)
{
return true;
diff --git a/ArchUnitNET/Domain/Slice.cs b/ArchUnitNET/Domain/Slice.cs
index 658a06596..d2261b35f 100644
--- a/ArchUnitNET/Domain/Slice.cs
+++ b/ArchUnitNET/Domain/Slice.cs
@@ -27,10 +27,25 @@ public Slice(SliceIdentifier identifier, IEnumerable types)
public string Description => Identifier.Description;
- [CanBeNull]
- public string NameSpace => Identifier.NameSpace;
public int? CountOfAsteriskInPattern => Identifier.CountOfAsteriskInPattern;
+ ///
+ /// The namespace a diagram nests this slice under when it nests slices in packages: the
+ /// part of the namespace that the pattern matched before the first capture group, if
+ /// it is the same for every type in the slice, which may be empty. null if it
+ /// differs between the types, or if the slice does not come from a slice pattern; a
+ /// diagram then draws the slice on its own. It takes no part in the slice's identity.
+ ///
+ [CanBeNull]
+ internal string DiagramNamespace { get; set; }
+
+ ///
+ /// Whether diagrams nest this slice in packages even if
+ /// is not set, which
+ /// is the case for slices of the obsolete MatchingWithPackages.
+ ///
+ internal bool NestByDefault { get; set; }
+
protected bool Equals(Slice other)
{
return Equals(Identifier, other.Identifier);
@@ -65,10 +80,5 @@ public override string ToString()
{
return Description;
}
-
- public bool ContainsNamespace()
- {
- return NameSpace != null;
- }
}
}
diff --git a/ArchUnitNET/Domain/SliceIdentifier.cs b/ArchUnitNET/Domain/SliceIdentifier.cs
index e84a4a539..8dc528989 100644
--- a/ArchUnitNET/Domain/SliceIdentifier.cs
+++ b/ArchUnitNET/Domain/SliceIdentifier.cs
@@ -11,28 +11,62 @@ private SliceIdentifier(
string identifier,
bool ignored,
int? countOfAsteriskInPattern = null,
- string nameSpace = null
+ string matchedPrefix = null,
+ string diagramPath = null
)
: base(identifier)
{
Ignored = ignored;
CountOfAsteriskInPattern = countOfAsteriskInPattern;
- NameSpace = nameSpace;
+ MatchedPrefix = matchedPrefix;
+ DiagramPath = diagramPath;
}
public string Description => Identifier;
+ ///
+ /// The part of the type's namespace the pattern matched before the first capture group,
+ /// e.g. "App." for "App.(*)" over "App.Orders", or an empty string if the pattern starts
+ /// with a group; null if the identifier does not come from a slice pattern. Only
+ /// used to nest the slice in diagrams; it takes no part in the slice's identity.
+ ///
[CanBeNull]
- public readonly string NameSpace;
+ internal readonly string MatchedPrefix;
+
+ ///
+ /// The path a diagram nests the slice along below : the part of
+ /// the namespace the pattern captured, e.g. "Orders.Web" for "App.(**)" over
+ /// "App.Orders.Web". null if the identifier does not come from a slice pattern. It
+ /// takes no part in the slice's identity.
+ ///
+ [CanBeNull]
+ internal readonly string DiagramPath;
+
public readonly int? CountOfAsteriskInPattern;
- public static SliceIdentifier Of(
+ public static SliceIdentifier Of(string identifier, int? countOfAsteriskInPattern = null)
+ {
+ return new SliceIdentifier(identifier, false, countOfAsteriskInPattern);
+ }
+
+ ///
+ /// Creates the identifier a slice pattern assigns to a type, together with where the
+ /// pattern matched, which diagrams use to nest the slice.
+ ///
+ internal static SliceIdentifier OfMatch(
string identifier,
- int? countOfAsteriskInPattern = null,
- string nameSpace = null
+ int? countOfAsteriskInPattern,
+ string matchedPrefix,
+ string diagramPath
)
{
- return new SliceIdentifier(identifier, false, countOfAsteriskInPattern, nameSpace);
+ return new SliceIdentifier(
+ identifier,
+ false,
+ countOfAsteriskInPattern,
+ matchedPrefix,
+ diagramPath
+ );
}
public static SliceIdentifier Ignore()
diff --git a/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs b/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs
index 2bc3f92bc..93d65edc3 100644
--- a/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs
+++ b/ArchUnitNET/Fluent/PlantUml/PlantUmlFluentComponentDiagramInitializer.cs
@@ -65,14 +65,26 @@ public GivenPlantUmlFluentComponentDiagram WithDependenciesFromSlices(
return new GivenPlantUmlFluentComponentDiagram(_fluentComponentDiagramCreator);
}
+ ///
+ /// Draws only the dependencies from and to the slices whose diagram name contains
+ /// ; see
+ /// .
+ ///
+ /// The slices to draw.
+ /// The text to focus on, fully qualified when slices are
+ /// nested in packages.
+ /// Only
+ /// is read.
public GivenPlantUmlFluentComponentDiagram WithDependenciesFromSlices(
IEnumerable slices,
- string focusOnPackage
+ string focusOnPackage,
+ GenerationOptions generationOptions = null
)
{
_fluentComponentDiagramCreator.Builder.WithDependenciesFromFocusOn(
slices,
- focusOnPackage
+ focusOnPackage,
+ generationOptions
);
_fluentComponentDiagramCreator.AddToDescription("with dependencies from slices");
return new GivenPlantUmlFluentComponentDiagram(_fluentComponentDiagramCreator);
diff --git a/ArchUnitNET/Fluent/Slices/SliceAssignment.cs b/ArchUnitNET/Fluent/Slices/SliceAssignment.cs
index db5bdf1c4..9fdfe8aa7 100644
--- a/ArchUnitNET/Fluent/Slices/SliceAssignment.cs
+++ b/ArchUnitNET/Fluent/Slices/SliceAssignment.cs
@@ -12,21 +12,57 @@ public class SliceAssignment : IHasDescription
public SliceAssignment(
Func assignIdentifierFunc,
string description
+ )
+ : this(assignIdentifierFunc, description, false) { }
+
+ internal SliceAssignment(
+ Func assignIdentifierFunc,
+ string description,
+ bool nestByDefault
)
{
_assignIdentifierFunc = assignIdentifierFunc;
Description = description;
+ _nestByDefault = nestByDefault;
}
+ ///
+ /// Whether diagrams nest the slices in packages even if
+ /// is not set;
+ /// only for the obsolete MatchingWithPackages.
+ ///
+ private readonly bool _nestByDefault;
+
public string Description { get; }
public IEnumerable Apply(IEnumerable types)
{
- return types.GroupBy(
- _assignIdentifierFunc,
- (identifier, enumerable) => new Slice(identifier, enumerable),
- SliceIdentifier.Comparer
- );
+ return types
+ .Select(type => (Type: type, Identifier: _assignIdentifierFunc(type)))
+ .GroupBy(
+ assigned => assigned.Identifier,
+ (identifier, assigned) => CreateSlice(identifier, assigned.ToList()),
+ SliceIdentifier.Comparer
+ );
+ }
+
+ private Slice CreateSlice(
+ SliceIdentifier identifier,
+ List<(IType Type, SliceIdentifier Identifier)> assigned
+ )
+ {
+ // The types of one slice can have matched different prefixes, e.g. with a leading
+ // ".."; such a slice has no single namespace to be nested under.
+ var prefixes = assigned
+ .Select(typeAndIdentifier => typeAndIdentifier.Identifier.MatchedPrefix)
+ .Distinct()
+ .ToList();
+ var types = assigned.Select(typeAndIdentifier => typeAndIdentifier.Type).ToList();
+ return new Slice(identifier, types)
+ {
+ DiagramNamespace = prefixes.Count == 1 ? prefixes[0] : null,
+ NestByDefault = _nestByDefault,
+ };
}
}
}
diff --git a/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs b/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs
index 228de6820..1482a6602 100644
--- a/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs
+++ b/ArchUnitNET/Fluent/Slices/SliceRuleInitializer.cs
@@ -21,29 +21,36 @@ public SliceRuleInitializer(SliceRuleCreator ruleCreator)
///
public GivenSlices Matching(string pattern)
{
- _ruleCreator.SetSliceAssignment(
- new SliceAssignment(
- t =>
- {
- var parseResult = Parse(pattern);
- return AssignFunc(t, parseResult.pattern, parseResult.asterisk);
- },
- "matching \"" + pattern + "\""
- )
- );
- return new GivenSlices(_ruleCreator);
+ return Matching(pattern, false);
}
+ ///
+ /// Creates the same slices as , which PlantUML diagrams nest in
+ /// package blocks even without
+ /// .
+ ///
+ /// The namespace pattern, as for .
+ [Obsolete(
+ "Use Matching(pattern) and draw the slices with "
+ + "new GenerationOptions { NestSlicesInPackages = true }. MatchingWithPackages "
+ + "creates the same slices as Matching and will be removed in a future release."
+ )]
public GivenSlices MatchingWithPackages(string pattern)
+ {
+ return Matching(pattern, true);
+ }
+
+ private GivenSlices Matching(string pattern, bool nestByDefault)
{
_ruleCreator.SetSliceAssignment(
new SliceAssignment(
t =>
{
var parseResult = Parse(pattern);
- return AssignFunc(t, parseResult.pattern, parseResult.asterisk, true);
+ return AssignFunc(t, parseResult.pattern, parseResult.asterisk);
},
- "matching \"" + pattern + "\""
+ "matching \"" + pattern + "\"",
+ nestByDefault
)
);
return new GivenSlices(_ruleCreator);
@@ -87,8 +94,7 @@ private static (string pattern, int? asterisk) Parse(string pattern)
private static SliceIdentifier AssignFunc(
IType type,
string pattern,
- int? countOfSingleAsterisk,
- bool fullName = false
+ int? countOfSingleAsterisk
)
{
var indexOfAsteriskInPattern = pattern.IndexOf("(*", StringComparison.Ordinal);
@@ -133,14 +139,14 @@ private static SliceIdentifier AssignFunc(
}
var sliceString = namespc;
+ var matchedPrefix = "";
if (slicePrefix != "")
{
- sliceString = namespc
- .Substring(
- namespc.IndexOf(slicePrefix, StringComparison.Ordinal) + slicePrefix.Length
- )
- .TrimStart('.');
+ var prefixEnd =
+ namespc.IndexOf(slicePrefix, StringComparison.Ordinal) + slicePrefix.Length;
+ matchedPrefix = namespc.Substring(0, prefixEnd);
+ sliceString = namespc.Substring(prefixEnd).TrimStart('.');
}
if (!sliceString.Contains(slicePostfix))
@@ -161,15 +167,14 @@ private static SliceIdentifier AssignFunc(
);
}
- // An empty prefix means the pattern starts with its capture group; there is no
- // namespace to nest the slice under, so report none rather than an empty one.
- return fullName
- ? SliceIdentifier.Of(
- slicePrefix + sliceString,
- countOfSingleAsterisk,
- slicePrefix == "" ? null : slicePrefix
- )
- : SliceIdentifier.Of(sliceString, countOfSingleAsterisk);
+ // What the pattern captured is both what identifies the slice and the path a
+ // diagram nests it along.
+ return SliceIdentifier.OfMatch(
+ sliceString,
+ countOfSingleAsterisk,
+ matchedPrefix,
+ sliceString
+ );
}
}
}
diff --git a/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs
index 824bece2b..d3f13c11b 100644
--- a/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs
+++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlSliceDiagramTests.cs
@@ -22,12 +22,15 @@ public class PlantUmlSliceDiagramTests
private const string Root = "SlicesTestAssembly.MultipleSubnamespaces.";
+ private static GenerationOptions Nested =>
+ new GenerationOptions { NestSlicesInPackages = true };
+
// --- The asterisk count decides which slices the exporter renders ---------------
//
// Every pattern below matches the same nine slices -- SlicesTests.MatchingTest and
// Matching_SingleAsterisk_CapturesEveryDepth assert that. What differs is how many
// of them survive PlantUmlFileBuilder.RemovePatternInappropriateSlices, which drops
- // a slice once its depth (dots in Description, less dots in NameSpace) reaches
+ // a slice once its depth (dots in its Description) reaches
// Slice.CountOfAsteriskInPattern:
//
// (*) count 1 -> 3 of 9 slices rendered
@@ -54,7 +57,7 @@ public Task BuildUmlBySlices_DoubleAsterisk()
// The matcher discards everything after the first "(*", so "(*).(*)" and "(*)..(*)"
// are indistinguishable to it and the next two snapshots are byte-identical -- as are
- // their MatchingWithPackages counterparts below. That identity is the point: it is
+ // their nested counterparts below. That identity is the point: it is
// what a reimplementation giving the second capture group any meaning would have to
// change. SlicesTests.DotDot_* cover the shapes that would tell the two patterns
// apart.
@@ -76,64 +79,60 @@ public Task BuildUmlBySlices_NonContiguousCaptureGroups()
}
[Fact]
- public Task BuildUmlBySlicesMatchingWithPackages_SingleAsterisk()
+ public Task BuildUmlBySlicesNested_SingleAsterisk()
{
- return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)"));
+ return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*)"), Nested);
}
[Fact]
- public Task BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk()
+ public Task BuildUmlBySlicesNested_DoubleAsterisk()
{
- return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)"));
+ return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(**)"), Nested);
}
[Fact]
- public Task BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups()
+ public Task BuildUmlBySlicesNested_MultipleCaptureGroups()
{
- return VerifySlices(
- SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*).(*)")
- );
+ return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*).(*)"), Nested);
}
[Fact]
- public Task BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups()
+ public Task BuildUmlBySlicesNested_NonContiguousCaptureGroups()
{
- return VerifySlices(
- SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)..(*)")
- );
+ return VerifySlices(SliceRuleDefinition.Slices().Matching(Root + "(*)..(*)"), Nested);
}
[Fact]
- public Task BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart()
+ public Task BuildUmlBySlicesNested_CaptureGroupAtStart()
{
// Nothing precedes the capture group, so there is no namespace to nest the slices
// under and they are drawn flat, like those of Matching.
- return VerifySlices(SliceRuleDefinition.Slices().MatchingWithPackages("(**).Service"));
+ return VerifySlices(SliceRuleDefinition.Slices().Matching("(**).Service"), Nested);
}
[Fact]
- public Task BuildUmlBySlicesMatchingWithPackages_LimitDependencies()
+ public Task BuildUmlBySlicesNested_LimitDependencies()
{
return VerifySlices(
- SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)"),
- new GenerationOptions { LimitDependencies = true }
+ SliceRuleDefinition.Slices().Matching(Root + "(**)"),
+ new GenerationOptions { NestSlicesInPackages = true, LimitDependencies = true }
);
}
[Fact]
- public Task BuildUmlBySlicesMatchingWithPackages_C4Style()
+ public Task BuildUmlBySlicesNested_C4Style()
{
return VerifySlices(
- SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)"),
- new GenerationOptions { C4Style = true }
+ SliceRuleDefinition.Slices().Matching(Root + "(**)"),
+ new GenerationOptions { NestSlicesInPackages = true, C4Style = true }
);
}
[Fact]
public Task BuildUmlBySlices_C4Style()
{
- // Slices produced by Matching (as opposed to MatchingWithPackages) have no namespace
- // prefix, so this exercises PlantUmlSlice.BuildStringC4Style's no-namespace branch.
+ // Slices that are not nested have no namespace prefix, so this exercises
+ // PlantUmlSlice.BuildStringC4Style's no-namespace branch.
return VerifySlices(
SliceRuleDefinition.Slices().Matching(Root + "(**)"),
new GenerationOptions { C4Style = true }
@@ -143,11 +142,9 @@ public Task BuildUmlBySlices_C4Style()
[Fact]
public Task BuildUmlBySlicesFocusOn()
{
- var slices = SortedSlices(
- SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)")
- );
+ var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)"));
var uml = new PlantUmlFileBuilder()
- .WithDependenciesFromFocusOn(slices, Root + "Slice1")
+ .WithDependenciesFromFocusOn(slices, Root + "Slice1", Nested)
.AsString();
return VerifyUml(uml);
}
@@ -155,10 +152,11 @@ public Task BuildUmlBySlicesFocusOn()
[Fact]
public Task BuildUmlBySlicesFocusOn_Matching_ColorsFocusedSlices()
{
- // Slices produced by Matching have no namespace, so the focused ones take
+ // Slices that are not nested have no namespace, so the focused ones take
// PlantUmlSlice.BuildString's namespace-less branch with a colour set -- the only
// path on which the exporter renders "[Slice1] #99ffd1" rather than a package.
- // Their Description carries no Root prefix either, hence the bare focus string.
+ // They are drawn under their description, which carries no Root prefix, hence the
+ // bare focus string.
var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)"));
var uml = new PlantUmlFileBuilder()
.WithDependenciesFromFocusOn(slices, "Slice1")
@@ -179,14 +177,12 @@ public Task BuildUmlBySlices_DirectCircle_RendersCircleDependency()
}
[Fact]
- public Task BuildUmlBySlicesMatchingWithPackages_DirectCircle()
+ public Task BuildUmlBySlicesNested_DirectCircle()
{
var slices = SortedSlices(
- SliceRuleDefinition
- .Slices()
- .MatchingWithPackages("SlicesTestAssembly.DirectCircle.(*)")
+ SliceRuleDefinition.Slices().Matching("SlicesTestAssembly.DirectCircle.(*)")
);
- var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices).AsString();
+ var uml = new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested).AsString();
return VerifyUml(uml);
}
@@ -199,19 +195,20 @@ public Task BuildUmlBySlicesMatchingWithPackages_DirectCircle()
private const string SameNamedRoot = "SlicesTestAssembly.SameNamedSubnamespaces.";
[Fact]
- public Task BuildUmlBySlicesMatchingWithPackages_SameNamedPackages()
+ public Task BuildUmlBySlicesNested_SameNamedPackages()
{
return VerifySlices(
- SliceRuleDefinition.Slices().MatchingWithPackages(SameNamedRoot + "(**)")
+ SliceRuleDefinition.Slices().Matching(SameNamedRoot + "(**)"),
+ Nested
);
}
[Fact]
- public Task BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style()
+ public Task BuildUmlBySlicesNested_SameNamedPackages_C4Style()
{
return VerifySlices(
- SliceRuleDefinition.Slices().MatchingWithPackages(SameNamedRoot + "(**)"),
- new GenerationOptions { C4Style = true }
+ SliceRuleDefinition.Slices().Matching(SameNamedRoot + "(**)"),
+ new GenerationOptions { NestSlicesInPackages = true, C4Style = true }
);
}
@@ -272,10 +269,10 @@ public void BuildUmlBySlicesFocusOn_EmptyPackage_Throws()
{
var slices = SliceRuleDefinition
.Slices()
- .MatchingWithPackages(Root + "(**)")
+ .Matching(Root + "(**)")
.GetObjects(Architecture);
var ex = Assert.Throws(() =>
- new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "")
+ new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "", Nested)
);
Assert.Equal("Package can't be empty", ex.Message);
}
@@ -285,10 +282,10 @@ public void BuildUmlBySlicesFocusOn_DotOnlyPackage_Throws()
{
var slices = SliceRuleDefinition
.Slices()
- .MatchingWithPackages(Root + "(**)")
+ .Matching(Root + "(**)")
.GetObjects(Architecture);
var ex = Assert.Throws(() =>
- new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, ".")
+ new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, ".", Nested)
);
Assert.Equal("Package can't contain a single dot only", ex.Message);
}
@@ -301,10 +298,10 @@ public void BuildUmlBySlicesFocusOn_UnknownPackage_Throws()
// focus string that no slice contains, on the way to this exception.
var slices = SliceRuleDefinition
.Slices()
- .MatchingWithPackages(Root + "(*)")
+ .Matching(Root + "(*)")
.GetObjects(Architecture);
var ex = Assert.Throws(() =>
- new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "NotAPackage")
+ new PlantUmlFileBuilder().WithDependenciesFromFocusOn(slices, "NotAPackage", Nested)
);
Assert.Contains("is not contained in this slice", ex.Message);
}
@@ -314,18 +311,18 @@ public void BuildUmlBySlicesFocusOn_TrailingDotPackage_IsTrimmed()
{
var slicesWithTrailingDot = SliceRuleDefinition
.Slices()
- .MatchingWithPackages(Root + "(**)")
+ .Matching(Root + "(**)")
.GetObjects(Architecture);
var umlWithTrailingDot = new PlantUmlFileBuilder()
- .WithDependenciesFromFocusOn(slicesWithTrailingDot, Root + "Slice1.")
+ .WithDependenciesFromFocusOn(slicesWithTrailingDot, Root + "Slice1.", Nested)
.AsString();
var slicesWithoutTrailingDot = SliceRuleDefinition
.Slices()
- .MatchingWithPackages(Root + "(**)")
+ .Matching(Root + "(**)")
.GetObjects(Architecture);
var umlWithoutTrailingDot = new PlantUmlFileBuilder()
- .WithDependenciesFromFocusOn(slicesWithoutTrailingDot, Root + "Slice1")
+ .WithDependenciesFromFocusOn(slicesWithoutTrailingDot, Root + "Slice1", Nested)
.AsString();
Assert.Equal(umlWithoutTrailingDot, umlWithTrailingDot);
@@ -340,15 +337,68 @@ public Task BuildUmlBySlicesFocusOn_SingleAsteriskPattern_SkipsSlicesWithoutFocu
// doesn't contain it (e.g. "Slice1") skip the pattern-appropriateness check via
// RemovePatternInappropriateSlices' per-slice "continue", while "Slice3" itself is
// shallow enough to survive that check and keep the package reachable.
- var slices = SortedSlices(
- SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(*)")
- );
+ var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(*)"));
var uml = new PlantUmlFileBuilder()
- .WithDependenciesFromFocusOn(slices, Root + "Slice3")
+ .WithDependenciesFromFocusOn(slices, Root + "Slice3", Nested)
.AsString();
return VerifyUml(uml);
}
+ // --- Where nested slices are rooted ----------------------------------------------
+ //
+ // A nested slice's root package is the namespace its types matched before the first
+ // capture group, so it has to come from the match, not from the pattern text.
+
+ [Fact]
+ public Task BuildUmlBySlicesNested_DuplicatePrefixSegment()
+ {
+ // The leading ".." matches "SlicesTestAssembly.DuplicatePrefix.Sub.", so that whole
+ // namespace is the root package, although "DuplicatePrefix.Sub" occurs in it twice.
+ return VerifySlices(
+ SliceRuleDefinition.Slices().Matching("..DuplicatePrefix.Sub.(**)"),
+ Nested
+ );
+ }
+
+ // --- One set of slices, drawn flat and nested ------------------------------------
+
+ [Fact]
+ public void BuildUmlBySlices_SameSlicesDrawnFlatAndNested()
+ {
+ var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)"));
+
+ var flat = new PlantUmlFileBuilder().WithDependenciesFrom(slices).AsString();
+ var nested = new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested).AsString();
+
+ Assert.DoesNotContain("package", flat);
+ Assert.Contains("[Slice1.Service]", flat);
+ Assert.Contains("package \"SlicesTestAssembly.MultipleSubnamespaces\"", nested);
+ Assert.DoesNotContain("[Slice1.Service]", nested);
+ }
+
+ // --- Two slices may not share a node ----------------------------------------------
+
+ [Fact]
+ public void BuildUmlBySlices_SameDiagramName_Throws()
+ {
+ // A slice that is not nested is drawn under its description, which here is the
+ // qualified name a nested slice is drawn under.
+ var slices = SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)"))
+ .Concat(
+ new[] { new Slice(SliceIdentifier.Of(Root + "Slice1"), Array.Empty()) }
+ )
+ .ToList();
+
+ var ex = Assert.Throws(() =>
+ new PlantUmlFileBuilder().WithDependenciesFrom(slices, Nested)
+ );
+ Assert.Equal(
+ "The slices \"Slice1\" and \"SlicesTestAssembly.MultipleSubnamespaces.Slice1\" "
+ + "would both be drawn as \"SlicesTestAssembly.MultipleSubnamespaces.Slice1\".",
+ ex.Message
+ );
+ }
+
///
/// GetObjects returns slices in Mono.Cecil's type-table order, which is deterministic
/// per build but not contractually stable across Cecil or runtime upgrades. Sorting by
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_C4Style.verified.txt
similarity index 100%
rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_C4Style.verified.txt
rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_C4Style.verified.txt
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_CaptureGroupAtStart.verified.txt
similarity index 100%
rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_CaptureGroupAtStart.verified.txt
rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_CaptureGroupAtStart.verified.txt
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DirectCircle.verified.txt
similarity index 100%
rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DirectCircle.verified.txt
rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DirectCircle.verified.txt
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DoubleAsterisk.verified.txt
similarity index 100%
rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_DoubleAsterisk.verified.txt
rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DoubleAsterisk.verified.txt
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt
new file mode 100644
index 000000000..3e8672334
--- /dev/null
+++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_DuplicatePrefixSegment.verified.txt
@@ -0,0 +1,13 @@
+@startuml
+
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
+
+HIDE_STEREOTYPE()
+
+package "SlicesTestAssembly.DuplicatePrefix.Sub" as SlicesTestAssembly.DuplicatePrefix.Sub {
+ package "DuplicatePrefix" as SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix {
+ [Sub] as SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix.Sub
+ }
+}
+
+@enduml
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_LimitDependencies.verified.txt
similarity index 100%
rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_LimitDependencies.verified.txt
rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_LimitDependencies.verified.txt
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_MultipleCaptureGroups.verified.txt
similarity index 100%
rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_MultipleCaptureGroups.verified.txt
rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_MultipleCaptureGroups.verified.txt
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_NonContiguousCaptureGroups.verified.txt
similarity index 100%
rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_NonContiguousCaptureGroups.verified.txt
rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_NonContiguousCaptureGroups.verified.txt
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages.verified.txt
similarity index 100%
rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages.verified.txt
rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages.verified.txt
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages_C4Style.verified.txt
similarity index 100%
rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SameNamedPackages_C4Style.verified.txt
rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SameNamedPackages_C4Style.verified.txt
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SingleAsterisk.verified.txt
similarity index 100%
rename from ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesMatchingWithPackages_SingleAsterisk.verified.txt
rename to ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlSliceDiagramTests.BuildUmlBySlicesNested_SingleAsterisk.verified.txt
diff --git a/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs b/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs
index 033fd5683..a18ebfeb7 100644
--- a/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs
+++ b/ArchUnitNETTests/Fluent/PlantUml/PlantUmlFluentComponentDiagramTests.cs
@@ -45,11 +45,14 @@ public void ComponentDiagramFromSlicesTest()
Assert.NotEmpty(uml2);
var arch1 = new ArchLoader().LoadAssembly(typeof(Architecture).Assembly).Build();
- var sliceRule1 = SliceRuleDefinition
- .Slices()
- .MatchingWithPackages("ArchUnitNET.(*).(*).(*)");
+ var sliceRule1 = SliceRuleDefinition.Slices().Matching("ArchUnitNET.(*).(*).(*)");
const string path = "../../../Fluent/PlantUml/Test.puml";
- var g = new GenerationOptions() { C4Style = true, LimitDependencies = true };
+ var g = new GenerationOptions()
+ {
+ C4Style = true,
+ LimitDependencies = true,
+ NestSlicesInPackages = true,
+ };
ComponentDiagram()
.WithDependenciesFromSlices(sliceRule1.GetObjects(arch1), g)
.WriteToFile(path);
diff --git a/ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs b/ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs
new file mode 100644
index 000000000..2b38579ed
--- /dev/null
+++ b/ArchUnitNETTests/Fluent/Slices/MatchingWithPackagesTests.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Linq;
+using ArchUnitNET.Domain;
+using ArchUnitNET.Domain.PlantUml.Export;
+using ArchUnitNET.Fluent;
+using ArchUnitNET.Fluent.Slices;
+using Xunit;
+
+// These tests cover the obsolete method itself.
+#pragma warning disable CS0618
+
+namespace ArchUnitNETTests.Fluent.Slices
+{
+ ///
+ /// is obsolete. It creates exactly the
+ /// slices creates, so its rules behave the same,
+ /// and the only thing it still does differently is nest its slices in diagrams by default.
+ ///
+ public class MatchingWithPackagesTests
+ {
+ private static Architecture Architecture => StaticTestArchitectures.SlicesTestArchitecture;
+
+ private const string Root = "SlicesTestAssembly.MultipleSubnamespaces.";
+
+ public static TheoryData Patterns =>
+ new TheoryData
+ {
+ Root + "(*)",
+ Root + "(**)",
+ Root + "(**)..",
+ Root + "(*).(*)",
+ Root + "(*)..(*)",
+ "..DuplicatePrefix.Sub.(**)",
+ "(**).Service",
+ "SlicesTestAssembly.SubnamespaceCircle.(**)",
+ };
+
+ [Theory]
+ [MemberData(nameof(Patterns))]
+ public void Slices_AreThoseOfMatching(string pattern)
+ {
+ var withPackages = SliceRuleDefinition
+ .Slices()
+ .MatchingWithPackages(pattern)
+ .GetObjects(Architecture)
+ .ToList();
+ var matching = SliceRuleDefinition.Slices().Matching(pattern).GetObjects(Architecture);
+
+ Assert.Equal(matching.Select(Describe), withPackages.Select(Describe));
+ }
+
+ [Theory]
+ [MemberData(nameof(Patterns))]
+ public void Rules_EvaluateLikeThoseOfMatching(string pattern)
+ {
+ AssertSameResults(
+ SliceRuleDefinition.Slices().Matching(pattern).Should().BeFreeOfCycles(),
+ SliceRuleDefinition.Slices().MatchingWithPackages(pattern).Should().BeFreeOfCycles()
+ );
+ AssertSameResults(
+ SliceRuleDefinition.Slices().Matching(pattern).Should().NotDependOnEachOther(),
+ SliceRuleDefinition
+ .Slices()
+ .MatchingWithPackages(pattern)
+ .Should()
+ .NotDependOnEachOther()
+ );
+ }
+
+ [Fact]
+ public void BeFreeOfCycles_DetectsCycle()
+ {
+ Assert.False(
+ SliceRuleDefinition
+ .Slices()
+ .MatchingWithPackages(Root + "(**)")
+ .Should()
+ .BeFreeOfCycles()
+ .HasNoViolations(Architecture)
+ );
+ }
+
+ // SubnamespaceCircle has real edges (Slice1 -> Slice2, Slice2.Inner -> Slice1) but no
+ // cycle, so an empty graph is not what makes this pass.
+ [Fact]
+ public void BeFreeOfCycles_WhenAcyclic_Passes()
+ {
+ Assert.True(
+ SliceRuleDefinition
+ .Slices()
+ .MatchingWithPackages("SlicesTestAssembly.SubnamespaceCircle.(**)")
+ .Should()
+ .BeFreeOfCycles()
+ .HasNoViolations(Architecture)
+ );
+ }
+
+ [Theory]
+ [MemberData(nameof(Patterns))]
+ public void Diagram_IsThatOfMatchingNestedInPackages(string pattern)
+ {
+ foreach (
+ var options in new[]
+ {
+ new GenerationOptions(),
+ new GenerationOptions { C4Style = true },
+ new GenerationOptions { LimitDependencies = true },
+ }
+ )
+ {
+ var expected = new PlantUmlFileBuilder()
+ .WithDependenciesFrom(
+ SortedSlices(SliceRuleDefinition.Slices().Matching(pattern)),
+ new GenerationOptions
+ {
+ NestSlicesInPackages = true,
+ C4Style = options.C4Style,
+ LimitDependencies = options.LimitDependencies,
+ }
+ )
+ .AsString();
+ var actual = new PlantUmlFileBuilder()
+ .WithDependenciesFrom(
+ SortedSlices(SliceRuleDefinition.Slices().MatchingWithPackages(pattern)),
+ options
+ )
+ .AsString();
+
+ Assert.Equal(expected, actual);
+ }
+ }
+
+ [Fact]
+ public void Diagram_FocusOnQualifiedPackage_IsThatOfMatchingNestedInPackages()
+ {
+ var expected = new PlantUmlFileBuilder()
+ .WithDependenciesFromFocusOn(
+ SortedSlices(SliceRuleDefinition.Slices().Matching(Root + "(**)")),
+ Root + "Slice1",
+ new GenerationOptions { NestSlicesInPackages = true }
+ )
+ .AsString();
+ var actual = new PlantUmlFileBuilder()
+ .WithDependenciesFromFocusOn(
+ SortedSlices(SliceRuleDefinition.Slices().MatchingWithPackages(Root + "(**)")),
+ Root + "Slice1"
+ )
+ .AsString();
+
+ Assert.Equal(expected, actual);
+ Assert.Contains("package \"SlicesTestAssembly.MultipleSubnamespaces\"", actual);
+ }
+
+ private static string Describe(Slice slice)
+ {
+ return slice.Description
+ + ": "
+ + string.Join(
+ ", ",
+ slice
+ .Types.Select(type => type.FullName)
+ .OrderBy(n => n, StringComparer.Ordinal)
+ );
+ }
+
+ private static void AssertSameResults(IArchRule matching, IArchRule withPackages)
+ {
+ Assert.Equal(matching.Description, withPackages.Description);
+ Assert.Equal(
+ matching.Evaluate(Architecture).Select(Summarize),
+ withPackages.Evaluate(Architecture).Select(Summarize)
+ );
+ }
+
+ private static string Summarize(EvaluationResult result)
+ {
+ return result.Passed
+ + " "
+ + result.EvaluatedObjectIdentifier.Identifier
+ + " "
+ + result.Description;
+ }
+
+ private static Slice[] SortedSlices(GivenSlices slices)
+ {
+ return slices
+ .GetObjects(Architecture)
+ .OrderBy(slice => slice.Description, StringComparer.Ordinal)
+ .ToArray();
+ }
+ }
+}
diff --git a/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs b/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs
index bf2866ab8..0c2b92e02 100644
--- a/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs
+++ b/ArchUnitNETTests/Fluent/Slices/SliceDomainTests.cs
@@ -116,6 +116,17 @@ public void SliceIdentifier_Equals_Branches()
Assert.False(identifier.Equals(SliceIdentifier.Of("B")));
}
+ [Fact]
+ public void SliceIdentifier_Equals_IgnoresDiagramData()
+ {
+ // Where a diagram draws the slice is no part of its identity.
+ var nested = SliceIdentifier.OfMatch("A.B", null, "X.", "A.B");
+
+ Assert.Equal(SliceIdentifier.Of("A.B"), nested);
+ Assert.Equal(SliceIdentifier.Of("A.B").GetHashCode(), nested.GetHashCode());
+ Assert.Equal("A.B", nested.Description);
+ }
+
[Fact]
public void SliceIdentifierComparer_Equals_NullFirstArgument_ReturnsFalse()
{
diff --git a/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs
index 143d03af5..232cb4a2b 100644
--- a/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs
+++ b/ArchUnitNETTests/Fluent/Slices/SlicesTests.cs
@@ -27,48 +27,6 @@ public void CycleDetectionTest()
.AssertNoViolations(helper);
}
- // --- Rule evaluation driven by MatchingWithPackages (not just Matching) -------
-
- [Fact]
- public void BeFreeOfCycles_MatchingWithPackages_DetectsCycle()
- {
- var helper = new SlicesAssemblyTestHelper().WithoutSnapshot();
- SliceRuleDefinition
- .Slices()
- .MatchingWithPackages("SlicesTestAssembly.MultipleSubnamespaces.(**)")
- .Should()
- .BeFreeOfCycles()
- .AssertOnlyViolations(helper);
- }
-
- // SubnamespaceCircle has real edges (Slice1 -> Slice2, Slice2.Inner -> Slice1) but no
- // cycle, unlike DotDotSemantics whose fixtures have no members and thus no edges at all --
- // an empty graph would pass here even if BeFreeOfCycles always returned "no cycles".
- [Fact]
- public void BeFreeOfCycles_MatchingWithPackages_WhenAcyclic_Passes()
- {
- var helper = new SlicesAssemblyTestHelper().WithoutSnapshot();
- SliceRuleDefinition
- .Slices()
- .MatchingWithPackages("SlicesTestAssembly.SubnamespaceCircle.(**)")
- .Should()
- .BeFreeOfCycles()
- .AssertNoViolations(helper);
- }
-
- [Fact]
- public Task NotDependOnEachOther_MatchingWithPackages_ReportsViolations()
- {
- var helper = new SlicesAssemblyTestHelper();
- var rule = SliceRuleDefinition
- .Slices()
- .MatchingWithPackages("SlicesTestAssembly.MultipleSubnamespaces.(**)..")
- .Should()
- .NotDependOnEachOther();
- rule.AssertAnyViolations(helper);
- return helper.AssertSnapshotMatches();
- }
-
[Fact]
public void MatchingTest()
{
@@ -305,21 +263,20 @@ public void Matching_LeadingDotDot_CapturesEveryDepth()
// (SlicesTestAssembly.DuplicatePrefix.Sub.DuplicatePrefix.Sub), so a leading ".." pattern
// for that prefix has two candidate starting positions to strip from. This pins that
// AssignFunc's IndexOf (leftmost match) picks the first occurrence, leaving the second
- // "DuplicatePrefix.Sub" -- and the one leading dot "..DuplicatePrefix.Sub." keeps after
- // stripping only one of its two literal dots -- in the description. With an absolute
- // (non-"..") prefix, the prefix can only ever be found at index 0, so that variant of this
- // test would pass even if IndexOf were replaced by a hardcoded 0.
+ // "DuplicatePrefix.Sub" in the description. With an absolute (non-"..") prefix, the
+ // prefix can only ever be found at index 0, so that variant of this test would pass even
+ // if IndexOf were replaced by a hardcoded 0.
[Fact]
- public void MatchingWithPackages_DuplicatePrefixSegment_UsesFirstOccurrenceAsPrefix()
+ public void DuplicatePrefixSegment_CapturesAfterTheFirstOccurrenceOfThePrefix()
{
var slices = SliceRuleDefinition
.Slices()
- .MatchingWithPackages("..DuplicatePrefix.Sub.(*)")
+ .Matching("..DuplicatePrefix.Sub.(**)")
.GetObjects(StaticTestArchitectures.SlicesTestArchitecture)
.ToList();
Assert.Single(slices);
- Assert.Equal(".DuplicatePrefix.Sub.DuplicatePrefix.Sub", slices[0].Description);
+ Assert.Equal("DuplicatePrefix.Sub", slices[0].Description);
}
// --- ".." semantics -------------------------------------------------------------
diff --git a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt b/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt
deleted file mode 100644
index ae856ef18..000000000
--- a/ArchUnitNETTests/Fluent/Slices/Snapshots/SlicesTests.NotDependOnEachOther_MatchingWithPackages_ReportsViolations.verified.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-Query: Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**).." should not depend on each other
-Result: False
-Description: SlicesTestAssembly.MultipleSubnamespaces.Slice1 does depend on other slices:
-SlicesTestAssembly.MultipleSubnamespaces.Slice1 -> SlicesTestAssembly.MultipleSubnamespaces.Slice2
- SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service.Service1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class
-
-Result: True
-Description: SlicesTestAssembly.MultipleSubnamespaces.Slice2 does not depend on another slice.
-Result: True
-Description: SlicesTestAssembly.MultipleSubnamespaces.Slice3 does not depend on another slice.
-Message:
-"Slices matching "SlicesTestAssembly.MultipleSubnamespaces.(**).." should not depend on each other" failed:
- SlicesTestAssembly.MultipleSubnamespaces.Slice1 does depend on other slices:
-SlicesTestAssembly.MultipleSubnamespaces.Slice1 -> SlicesTestAssembly.MultipleSubnamespaces.Slice2
- SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service.Service1Class -> SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service.Service2Class
-
-
-
-
diff --git a/documentation/docs/guide.md b/documentation/docs/guide.md
index 022bf60d9..2c522a8c1 100644
--- a/documentation/docs/guide.md
+++ b/documentation/docs/guide.md
@@ -239,17 +239,18 @@ PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetOb
```
### 5.2 Structured diagram dependencies
-On the previous case there are too many dependencies, this option is suitable mainly for small projects / architectures / slices. With an increase in the number of objects, it makes sense to introduce another type of display - a display with packages. It differs from the previous case only with the modified creation of SliceRule.
+On the previous case there are too many dependencies, this option is suitable mainly for small projects / architectures / slices. With an increase in the number of objects, it makes sense to introduce another type of display - a display with packages. It differs from the previous case only in the `NestSlicesInPackages` option, which nests the slices of a pattern in `package` blocks along the namespace they matched. A slice that does not come from a pattern, or whose types matched different namespaces before the capture group, is drawn on its own under its name.

```cs
string pattern = "ArchUnitNET.(**)";
-GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern);
+GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern);
Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build();
+GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true};
string path = "diagram.puml";
-PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch)).WriteToFile(path);
+PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch), g).WriteToFile(path);
```
### 5.3 LimitDependencies-mod
@@ -258,9 +259,9 @@ The previous case still shows a large number of connections. For maximum simplif

```cs
string pattern = "ArchUnitNET.(**)";
-GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern);
+GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern);
Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build();
-GenerationOptions g = new GenerationOptions(){LimitDependencies = true};
+GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true, LimitDependencies = true};
string path = "diagram.puml";
@@ -298,9 +299,9 @@ To enable an C4plantUML-style view of diagrams, set in the GenerationOptions fla

```cs
string pattern = "ArchUnitNET.(*).(*).(*)";
-GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern);
+GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern);
Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build();
-GenerationOptions g = new GenerationOptions(){C4Style = true};
+GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true, C4Style = true};
string path = "diagram.puml";
@@ -308,20 +309,31 @@ PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetOb
```
### 5.6 Focus-mod
-Focus mod allows you to show all dependencies on the selected package or out of the package.
+Focus mod allows you to show all dependencies on the selected package or out of the package. With `NestSlicesInPackages`, slices are known by their fully qualified path, so the package to focus on is fully qualified as well.

```cs
string pattern = "ArchUnitNET.(**)";
-string focusOnThisPackage = "ArchUnitNET.Fluent.Syntax.Elements"
-GivenSlices sliceRule = SliceRuleDefinition.Slices().MatchingWithPackages(pattern);
+string focusOnThisPackage = "ArchUnitNET.Fluent.Syntax.Elements";
+GivenSlices sliceRule = SliceRuleDefinition.Slices().Matching(pattern);
Architecture arch = new ArchLoader().LoadAssembly(typeof(ArchUnitNET.Domain.Architecture).Assembly).Build();
+GenerationOptions g = new GenerationOptions(){NestSlicesInPackages = true};
string path = "diagram.puml";
-PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch), focusOnThisPackage).WriteToFile(path);
+PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch), focusOnThisPackage, g).WriteToFile(path);
```
+### 5.7 Migrating from MatchingWithPackages
+`MatchingWithPackages` is obsolete and will be removed in a future release. It creates the same slices
+as `Matching`, with the same names and violations, and only nests them in diagrams. Replace
+`MatchingWithPackages(pattern)` with `Matching(pattern)` and draw the slices with
+`new GenerationOptions { NestSlicesInPackages = true }`.
+
+Failure messages of former `MatchingWithPackages` rules no longer start a slice's name with the
+namespace before the first capture group, and their frozen violations, which were stored under those
+longer names, have to be frozen again once.
+
## 6. Further Reading and Examples
A complete overview of all available methods can be found [here](additional.md).