diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDependency.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDependency.cs
index 50530a951..8191b4b41 100644
--- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDependency.cs
+++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDependency.cs
@@ -51,24 +51,13 @@ public string GetPlantUmlString(RenderOptions renderOptions = null)
+ Environment.NewLine;
case DependencyType.OneToPackage:
- return "["
- + Origin
- + "] -[#red]> "
- + GetChildNamespace(Target)
- + Environment.NewLine;
+ return "[" + Origin + "] -[#red]> " + Target + Environment.NewLine;
case DependencyType.PackageToOne:
- return GetChildNamespace(Origin)
- + " -[#blue]> ["
- + Target
- + "]"
- + Environment.NewLine;
+ return Origin + " -[#blue]> [" + Target + "]" + Environment.NewLine;
case DependencyType.PackageToPackage:
- return GetChildNamespace(Origin)
- + " -[#green]> "
- + GetChildNamespace(Target)
- + Environment.NewLine;
+ return Origin + " -[#green]> " + Target + Environment.NewLine;
case DependencyType.OneToOneCompact:
if (OriginCountOfDots() == TargetCountOfDots())
@@ -93,10 +82,7 @@ public string GetPlantUmlString(RenderOptions renderOptions = null)
&& (OriginCountOfDots() == 0 || HaveSameParentNamespace(Origin, Target))
)
{
- return GetChildNamespace(Origin)
- + " ..> "
- + GetChildNamespace(Target)
- + Environment.NewLine;
+ return Origin + " ..> " + Target + Environment.NewLine;
}
return "";
@@ -119,7 +105,7 @@ public string GetPlantUmlString(RenderOptions renderOptions = null)
if (tmp != Origin && HaveSameParentNamespace(tmp, Origin))
{
- return Origin + " --> " + GetChildNamespace(tmp) + Environment.NewLine;
+ return Origin + " --> " + tmp + Environment.NewLine;
}
}
else
@@ -132,7 +118,7 @@ public string GetPlantUmlString(RenderOptions renderOptions = null)
if (tmp != Target && HaveSameParentNamespace(tmp, Target))
{
- return GetChildNamespace(tmp) + " -> " + Target + Environment.NewLine;
+ return tmp + " -> " + Target + Environment.NewLine;
}
}
return "";
@@ -147,9 +133,6 @@ public string GetPlantUmlString(RenderOptions renderOptions = null)
private static string GetParentNamespace(string ns) =>
ns.Remove(ns.LastIndexOf(".", StringComparison.Ordinal));
- private static string GetChildNamespace(string ns) =>
- ns.Remove(0, ns.LastIndexOf(".", StringComparison.Ordinal) + 1);
-
private static bool HaveSameParentNamespace(string origin, string target) =>
(GetParentNamespace(origin) == GetParentNamespace(target));
diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDiagram.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDiagram.cs
index bdc6996a7..6d808cd65 100644
--- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDiagram.cs
+++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlDiagram.cs
@@ -29,20 +29,48 @@ public string GetPlantUmlString(RenderOptions renderOptions)
)
.AppendLine();
result.AppendLine("HIDE_STEREOTYPE()").AppendLine();
- result.Append(
- PlantUmlElements
- .OrderBy(element => element.GetType() != typeof(PlantUmlNamespace))
- .ThenBy(element => element.GetType() != typeof(PlantUmlSlice))
- .ThenBy(element => element.GetType() != typeof(PlantUmlClass))
- .ThenBy(element => element.GetType() != typeof(PlantUmlInterface))
- .Aggregate(
- "",
- (umlString, umlElement) =>
- umlString + umlElement.GetPlantUmlString(renderOptions)
- )
- );
+ var orderedElements = PlantUmlElements
+ .OrderBy(element => element.GetType() != typeof(PlantUmlNamespace))
+ .ThenBy(element => element.GetType() != typeof(PlantUmlSlice))
+ .ThenBy(element => element.GetType() != typeof(PlantUmlClass))
+ .ThenBy(element => element.GetType() != typeof(PlantUmlInterface));
+ foreach (var element in MergeNestedSlices(orderedElements))
+ {
+ result.Append(element.GetPlantUmlString(renderOptions));
+ }
result.AppendLine("@enduml");
return result.ToString();
}
+
+ ///
+ /// Replaces all nested slices that share a root package with a single tree for that
+ /// package, placed where its first slice was. All other elements keep their position.
+ ///
+ private static IEnumerable MergeNestedSlices(
+ IEnumerable elements
+ )
+ {
+ var mergedElements = new List();
+ var sliceTrees = new Dictionary();
+ foreach (var element in elements)
+ {
+ if (!(element is PlantUmlSlice slice) || !slice.IsNested)
+ {
+ mergedElements.Add(element);
+ }
+ else if (sliceTrees.TryGetValue(slice.RootPackage, out var tree))
+ {
+ tree.Add(slice);
+ }
+ else
+ {
+ tree = new PlantUmlSliceTree(slice);
+ sliceTrees.Add(slice.RootPackage, tree);
+ mergedElements.Add(tree);
+ }
+ }
+
+ return mergedElements;
+ }
}
}
diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs
index ad4aa0405..236adf340 100644
--- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs
+++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSlice.cs
@@ -1,6 +1,5 @@
-using System;
-using System.ComponentModel.Design;
-using System.Diagnostics;
+using System.Collections.Generic;
+using System.Linq;
using System.Text;
namespace ArchUnitNET.Domain.PlantUml.Export
@@ -38,111 +37,99 @@ public void UseS4Style()
C4Style = true;
}
- public string GetPlantUmlString(RenderOptions renderOptions)
- {
- var result = C4Style ? BuildStringC4Style() : BuildString();
+ internal bool IsNested => Namespace != null;
- if (Hyperlink != null)
+ internal bool IsC4Style => C4Style;
+
+ internal string SliceColor => Color;
+
+ ///
+ /// The outermost package this slice is nested in, which is its whole namespace.
+ ///
+ internal string RootPackage => Namespace.Remove(Namespace.Length - 1);
+
+ ///
+ /// The packages this slice is nested in, outermost first. The first entry is the whole
+ /// namespace of the slice, the others are the segments between it and the leaf.
+ ///
+ internal IReadOnlyList PackagePath
+ {
+ get
{
- result.Append(" [[" + Hyperlink + "]] ");
+ var path = new List { RootPackage };
+ var segments = _name.Remove(0, Namespace.Length).Split('.');
+ path.AddRange(segments.Take(segments.Length - 1));
+ return path;
}
-
- return result.AppendLine().ToString();
}
- private StringBuilder BuildString()
+ ///
+ /// The label of the component inside its innermost package, or an empty string if the
+ /// slice only stands for that package.
+ ///
+ internal string Leaf
{
- var result = new StringBuilder();
- if (Namespace != null)
+ get
{
- result.Append("package " + Namespace.Remove(Namespace.Length - 1));
var name = _name.Remove(0, Namespace.Length);
- var iter = 1;
- while (name.Contains("."))
- {
- var dotPattern = name.IndexOf(".", StringComparison.Ordinal);
- result.AppendLine(" {");
- result.Append("package " + name.Remove(dotPattern));
- name = name.Remove(0, dotPattern + 1);
- iter++;
- }
+ return name.Substring(name.LastIndexOf('.') + 1);
+ }
+ }
- if (name != "")
- {
- result.AppendLine(" {");
- result.Append("[" + name + "] as " + _name);
- if (Color != null)
- {
- result.AppendLine(" #" + Color);
- }
- else
- {
- result.AppendLine();
- }
- }
- else if (Color != null)
- {
- result.AppendLine(" #" + Color + " {");
- }
- else
- {
- result.AppendLine(" {");
- }
+ public string GetPlantUmlString(RenderOptions renderOptions)
+ {
+ if (IsNested)
+ {
+ return new PlantUmlSliceTree(this).GetPlantUmlString(renderOptions);
+ }
- for (var i = iter; i > 0; i--)
- {
- result.AppendLine("}");
- }
+ var result = new StringBuilder();
+ if (C4Style)
+ {
+ result.Append("Container(" + _name + ", " + _name + ")");
+ AppendHyperlink(result);
}
else
{
result.Append("[" + _name + "]");
+ AppendHyperlink(result);
if (Color != null)
{
result.Append(" #" + Color);
}
}
- return result;
+ return result.AppendLine().ToString();
}
- private StringBuilder BuildStringC4Style()
+ internal string GetLeafString()
{
var result = new StringBuilder();
- if (Namespace == null)
+ if (C4Style)
{
- result.Append("Container(" + _name + ", " + _name + ")");
- return result;
+ result.Append("Container(" + _name + ", " + Leaf + ")");
+ AppendHyperlink(result);
}
-
- var namespc = Namespace.Remove(Namespace.Length - 1);
- result.Append("Boundary(" + namespc + ", " + namespc + ") ");
- var name = _name.Remove(0, Namespace.Length);
- var iter = 1;
- while (name.Contains("."))
+ else
{
- var dotPattern = name.IndexOf(".", StringComparison.Ordinal);
- result.AppendLine(" {");
- result.Append(
- "Boundary(" + name.Remove(dotPattern) + ", " + name.Remove(dotPattern) + ") "
- );
- name = name.Remove(0, dotPattern + 1);
- iter++;
+ result.Append("[" + Leaf + "] as " + _name);
+ AppendHyperlink(result);
+ if (Color != null)
+ {
+ result.Append(" #" + Color);
+ }
}
- result.AppendLine(" {");
- if (name != "")
- {
- result.Append("Container(" + _name + ", " + name + ")");
- result.AppendLine();
- }
+ return result.ToString();
+ }
- for (var i = iter; i > 0; i--)
+ // PlantUML only accepts a link in front of the color, not after it.
+ private void AppendHyperlink(StringBuilder result)
+ {
+ if (Hyperlink != null)
{
- result.AppendLine("}");
+ result.Append(" [[" + Hyperlink + "]] ");
}
-
- return result;
}
}
}
diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSliceTree.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSliceTree.cs
new file mode 100644
index 000000000..6c9ac6e76
--- /dev/null
+++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlSliceTree.cs
@@ -0,0 +1,132 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace ArchUnitNET.Domain.PlantUml.Export
+{
+ ///
+ /// A package (or C4 boundary) that holds nested slices, rendered as a single block.
+ ///
+ ///
+ /// Every nested slice carries the full chain of packages it lives in. Rendering each slice on
+ /// its own would re-open its parent packages once per slice. PlantUML merges those re-opened
+ /// packages, but the source then reads nothing like a hand-written diagram. Collecting the
+ /// slices into a tree first opens every package exactly once.
+ ///
+ /// PlantUML identifies a package by its name, not by where it is nested, so two packages
+ /// called "Domain" under different parents would be drawn as one. Every package is therefore
+ /// labelled with its own segment but identified by its full path, which is also the name the
+ /// dependencies use to point at it.
+ ///
+ internal class PlantUmlSliceTree : IPlantUmlElement
+ {
+ private readonly string _name;
+ private readonly string _path;
+ private readonly bool _c4Style;
+ private string _color;
+
+ // Child packages and components in the order they were first seen, so the output keeps
+ // the order in which the slices were added.
+ private readonly List