Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions ArchUnitNET/Domain/PlantUml/Export/PlantUmlDiagram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ public string GetPlantUmlString(RenderOptions renderOptions)
/// </summary>
/// <remarks>
/// A slice is identified in the diagram by its name, but not every name is a valid id.
/// PlantUML rejects "[Inner] as App.Outer+Inner" and "Container(App.Orders.*.Http, ...)"
/// as syntax errors, and slice names can contain such characters: SlicedBy names slices
/// after type names such as "Outer+Inner" or "List`1", and a hand-built SliceIdentifier
/// can be called anything. No character that a namespace cannot contain works in their
/// place either, so the id is made from the name with those characters replaced by "_",
/// and suffixed until it clashes with no other name in the diagram. The name itself stays
/// the label, and slices whose names are valid ids keep them.
/// PlantUML rejects "[Inner] as App.Outer+Inner" and "Container(Slice Orders, ...)" as
/// syntax errors, and slice names can contain such characters: every slice description
/// starts with "Slice " and joins several parts with " - ", SlicedBy names slices after
/// type names such as "Outer+Inner" or "List`1", and a hand-built SliceIdentifier can be
/// called anything. No character that a namespace cannot contain works in their place
/// either, so the id is made from the name with those characters replaced by "_", and
/// suffixed until it clashes with no other name in the diagram. The name itself stays the
/// label, and slices whose names are valid ids keep them.
///
/// A valid name can still be taken by a package: a nested slice named after its own
/// namespace, such as "App.Orders", would share its id with the package "App.Orders" it
Expand Down
39 changes: 38 additions & 1 deletion ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,14 @@ public PlantUmlFileBuilder WithDependenciesFrom(
/// <c>package</c> of the deepest namespace that contains all of its types. With
/// <see cref="GenerationOptions.LimitDependencies"/>, only the arrows between slices whose
/// types share an equally deep namespace are drawn.</param>
/// <exception cref="ArgumentException">Two slices would be drawn under the same name.</exception>
public PlantUmlFileBuilder WithDependenciesFrom(
IEnumerable<Slice> slices,
GenerationOptions generationOptions = null
)
{
_sliceList = slices.Distinct().ToList();
AssertDistinctNames();
RemovePatternInappropriateSlices();
AssignCommonNamespaces();

Expand Down Expand Up @@ -315,14 +317,15 @@ var parent in parentNamespaces.Where(parentName =>
/// <param name="generationOptions">Only <see cref="GenerationOptions.NestSlicesInPackages"/>
/// is read; the defaults if <c>null</c>.</param>
/// <exception cref="ArgumentException">The package is empty, a single dot or a namespace
/// that no slice lies in.</exception>
/// that no slice lies in, or two slices would be drawn under the same name.</exception>
public PlantUmlFileBuilder WithDependenciesFromFocusOn(
IEnumerable<Slice> slices,
string package,
GenerationOptions generationOptions = null
)
{
_sliceList = slices.Distinct().ToList();
AssertDistinctNames();

switch (package)
{
Expand Down Expand Up @@ -509,6 +512,40 @@ private void ReplaceCirclesWithAppropriateDependencyType()
}
}

/// <summary>
/// Rejects two slices with the same name, which the diagram would draw as one node. Only
/// slices with different parts can share one, when a part contains " - ", as
/// <c>["A - B"]</c> and <c>["A", "B"]</c> are both named "Slice A - B".
/// </summary>
private void AssertDistinctNames()
{
var slicesByName = new Dictionary<string, Slice>();
foreach (var slice in _sliceList)
{
if (slicesByName.TryGetValue(slice.Description, out var other))
{
throw new ArgumentException(
"The slices with parts "
+ FormatParts(other)
+ " and "
+ FormatParts(slice)
+ " would both be drawn as \""
+ slice.Description
+ "\"."
);
}

slicesByName.Add(slice.Description, slice);
}
}

private static string FormatParts(Slice slice)
{
return "["
+ string.Join(", ", slice.Identifier.Parts.Select(part => "\"" + part + "\""))
+ "]";
}

/// <summary>
/// The slices that a dependency starts or ends at, in the order of the slice list.
/// </summary>
Expand Down
102 changes: 87 additions & 15 deletions ArchUnitNET/Domain/SliceIdentifier.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,103 @@
namespace ArchUnitNET.Domain
using System;
using System.Collections.Generic;
using System.Linq;

namespace ArchUnitNET.Domain
{
public class SliceIdentifier : StringIdentifier, IHasDescription
/// <summary>
/// Identifies a slice by its parts: for a slice pattern, the values its capture groups
/// captured, in group order. Two identifiers are the same slice exactly when their parts are
/// equal and both are ignored or both are not.
/// </summary>
public class SliceIdentifier : IHasDescription
{
public static readonly SliceIdentifierComparer Comparer = new SliceIdentifierComparer();
public readonly bool Ignored;

private SliceIdentifier(
string identifier,
IReadOnlyList<string> parts,
bool ignored,
int? countOfAsteriskInPattern = null
)
: base(identifier)
{
Parts = parts;
Ignored = ignored;
CountOfAsteriskInPattern = countOfAsteriskInPattern;
}

public string Description => Identifier;
/// <summary>
/// What identifies the slice: for a slice pattern, the values its capture groups
/// captured, in group order. "App.(**)" over "App.Orders.Web" gives
/// <c>[Orders.Web]</c>.
/// </summary>
public IReadOnlyList<string> Parts { get; }

/// <summary>
/// The slice's name, built from its parts: "Slice " followed by the parts joined with
/// " - ", e.g. "Slice Orders - Http". It depends on the parts only, so equal slices always
/// have the same name.
/// </summary>
public string Description => "Slice " + string.Join(" - ", Parts);

public readonly int? CountOfAsteriskInPattern;

public static SliceIdentifier Of(string identifier, int? countOfAsteriskInPattern = null)
/// <summary>
/// Creates an identifier for a slice with a single part, the same slice as
/// <c>Of(new[] { part })</c>.
/// </summary>
/// <param name="part">
/// The slice's only part: a custom <c>SliceAssignment</c> that returns identifiers with the
/// same part for two types puts them into the same slice.
/// </param>
/// <param name="countOfAsteriskInPattern">
/// The number of "(*)" groups in the pattern, if the slice comes from one.
/// </param>
public static SliceIdentifier Of(string part, int? countOfAsteriskInPattern = null)
{
return new SliceIdentifier(identifier, false, countOfAsteriskInPattern);
return new SliceIdentifier(CheckParts(new[] { part }), false, countOfAsteriskInPattern);
}

/// <summary>
/// Creates an identifier for the slice with the given parts. Identifiers with equal parts,
/// in the same order, belong to the same slice.
/// </summary>
/// <param name="parts">The parts, at least one; none of them may be <c>null</c>.</param>
/// <exception cref="ArgumentException">There are no parts, or one of them is null.</exception>
public static SliceIdentifier Of(IEnumerable<string> parts)
{
return new SliceIdentifier(CheckParts(parts), false);
}

public static SliceIdentifier Ignore()
{
return new SliceIdentifier("Ignored", true);
return new SliceIdentifier(new[] { "Ignored" }, true);
}

private static IReadOnlyList<string> CheckParts(IEnumerable<string> parts)
{
if (parts == null)
{
throw new ArgumentNullException(nameof(parts));
}

var list = parts.ToList();
if (list.Count == 0)
{
throw new ArgumentException("A slice needs at least one part.", nameof(parts));
}

if (list.Any(part => part == null))
{
throw new ArgumentException("A slice part can't be null.", nameof(parts));
}

return list.AsReadOnly();
}

/// <summary>
/// Is true when the two SliceIdentifiers belong to the same slice
/// Whether the two identifiers belong to the same slice: both ignored, or both not
/// ignored with the same parts.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool CompareTo(SliceIdentifier other)
{
if (other == null)
Expand All @@ -43,12 +106,12 @@ public bool CompareTo(SliceIdentifier other)
}

return Ignored && other.Ignored
|| !Ignored && !other.Ignored && Identifier == other.Identifier;
|| !Ignored && !other.Ignored && Parts.SequenceEqual(other.Parts);
}

private bool Equals(SliceIdentifier other)
{
return Identifier == other.Identifier && Ignored == other.Ignored;
return Ignored == other.Ignored && Parts.SequenceEqual(other.Parts);
}

public override bool Equals(object obj)
Expand All @@ -70,10 +133,19 @@ public override int GetHashCode()
{
unchecked
{
var hashCode = 397 ^ Identifier.GetHashCode();
hashCode = (hashCode * 397) ^ Ignored.GetHashCode();
var hashCode = Ignored.GetHashCode();
foreach (var part in Parts)
{
hashCode = (hashCode * 397) ^ part.GetHashCode();
}

return hashCode;
}
}

public override string ToString()
{
return Description;
}
}
}
6 changes: 3 additions & 3 deletions ArchUnitNET/Fluent/Slices/SlicesShould.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Architecture architecture
}

var cycleIdentifier = new EnumerableIdentifier(
cycle.Contents.Select(slice => slice.Identifier)
cycle.Contents.Select(slice => new StringIdentifier(slice.Description))
);
yield return new EvaluationResult(
cycle,
Expand All @@ -106,7 +106,7 @@ Architecture architecture
else
{
var slicesIdentifier = new EnumerableIdentifier(
slicesList.Select(slice => slice.Identifier)
slicesList.Select(slice => new StringIdentifier(slice.Description))
);
yield return new EvaluationResult(
slicesList,
Expand Down Expand Up @@ -166,7 +166,7 @@ Architecture architecture

yield return new EvaluationResult(
slice,
slice.Identifier,
new StringIdentifier(slice.Description),
passed,
description,
archRule,
Expand Down
12 changes: 6 additions & 6 deletions ArchUnitNETTests/ArchUnitNET/Storage/CustomPathFrozenRules.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
{
"ArchRuleDescription": "Slices matching \"SlicesTestAssembly.MultipleSubnamespaces.(**)\" should not depend on each other",
"Violations": [
"Slice3",
"Slice3.Group1",
"Slice2",
"Slice2.Service",
"Slice1",
"Slice1.Service"
"Slice Slice3",
"Slice Slice3.Group1",
"Slice Slice2",
"Slice Slice2.Service",
"Slice Slice1",
"Slice Slice1.Service"
]
}
]
12 changes: 6 additions & 6 deletions ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@
{
"ArchRuleDescription": "Slices matching \"SlicesTestAssembly.MultipleSubnamespaces.(**)\" should not depend on each other",
"Violations": [
"Slice3",
"Slice3.Group1",
"Slice2",
"Slice2.Service",
"Slice1",
"Slice1.Service"
"Slice Slice3",
"Slice Slice3.Group1",
"Slice Slice2",
"Slice Slice2.Service",
"Slice Slice1",
"Slice Slice1.Service"
]
},
{
Expand Down
12 changes: 6 additions & 6 deletions ArchUnitNETTests/ArchUnitNET/Storage/FrozenRules.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
<Violation>ArchUnitNETTests.Fluent.FreezeTests+Violation</Violation>
</FrozenRule>
<FrozenRule ArchRule="Slices matching &quot;SlicesTestAssembly.MultipleSubnamespaces.(**)&quot; should not depend on each other">
<Violation>Slice3</Violation>
<Violation>Slice3.Group1</Violation>
<Violation>Slice2</Violation>
<Violation>Slice2.Service</Violation>
<Violation>Slice1</Violation>
<Violation>Slice1.Service</Violation>
<Violation>Slice Slice3</Violation>
<Violation>Slice Slice3.Group1</Violation>
<Violation>Slice Slice2</Violation>
<Violation>Slice Slice2.Service</Violation>
<Violation>Slice Slice1</Violation>
<Violation>Slice Slice1.Service</Violation>
</FrozenRule>
</FrozenRules>
Loading
Loading