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
78 changes: 74 additions & 4 deletions ArchUnitNET/Domain/Namespace.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,83 @@
using System.Collections.Generic;
using System.Linq;
using ArchUnitNET.Domain.Dependencies;

namespace ArchUnitNET.Domain
{
public class Namespace : Slice, IHasName
/// <summary>
/// A namespace of the loaded architecture together with the types declared directly in it.
/// Namespaces are equal exactly when their full names are.
/// </summary>
public class Namespace : IHasName, IHasDescription, IHasDependencies
{
private readonly string _name;
private readonly List<IType> _types;

public Namespace(string name, IEnumerable<IType> types)
: base(SliceIdentifier.Of(name), types) { }
{
_name = name;
_types = types.ToList();
}

/// <summary>The full name of this namespace.</summary>
public string Name => _name;

/// <summary>The full name of this namespace.</summary>
public string FullName => _name;

/// <summary>The full name of this namespace.</summary>
public string Description => _name;

/// <summary>The types declared directly in this namespace.</summary>
public IEnumerable<IType> Types => _types;

/// <summary>The classes declared directly in this namespace.</summary>
public IEnumerable<Class> Classes => Types.OfType<Class>();

/// <summary>The interfaces declared directly in this namespace.</summary>
public IEnumerable<Interface> Interfaces => Types.OfType<Interface>();

/// <summary>Every dependency going out of this namespace's types, including internal ones.</summary>
public List<ITypeDependency> Dependencies =>
Types.SelectMany(type => type.Dependencies).ToList();

/// <summary>Every dependency pointing at this namespace's types.</summary>
public List<ITypeDependency> 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;
}
}
}
216 changes: 138 additions & 78 deletions ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -222,22 +224,104 @@ public PlantUmlFileBuilder WithDependenciesFrom(

if (!generationOptions.IncludeNodesWithoutDependencies)
{
nodes = RemoveNodesNotContainedInDependencies(nodes);
RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description);
}

var nodeElements = HandleNodes(nodes);

if (!generationOptions.LimitDependencies)
{
RemoveDuplicateDependenciesWhenShowingPackages();
ReplaceCirclesWithAppropriateDependencyType();
}
RemoveDuplicatedArrowsIfExist();

_diagram.AddElements(
nodeElements.OrderBy(element =>
element is PlantUmlNamespace @namespace ? @namespace.Name.Length : -1
_diagram.AddElements(nodes.Values);
_diagram.AddElements(_dependencies);

return this;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="namespaces">The namespaces to draw, e.g. <see cref="Architecture.Namespaces"/>.</param>
/// <param name="generationOptions">Filters and layout options; the defaults if <c>null</c>.
/// <see cref="GenerationOptions.C4Style"/> does not apply to namespaces.</param>
public PlantUmlFileBuilder WithDependenciesFrom(
IEnumerable<Namespace> 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<Namespace, IPlantUmlElement>();

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<string>();
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<PlantUmlNamespace>()
.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);

Expand Down Expand Up @@ -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)
{
Expand All @@ -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,
Expand All @@ -326,7 +412,7 @@ string package

if (!generationOptions.IncludeNodesWithoutDependencies)
{
nodes = RemoveNodesNotContainedInDependencies(nodes);
RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description);
}

RemoveDuplicateDependenciesWhenShowingPackages();
Expand Down Expand Up @@ -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))
Expand All @@ -381,72 +461,52 @@ string package
return this;
}

private Dictionary<Slice, IPlantUmlElement> RemoveNodesNotContainedInDependencies(
Dictionary<Slice, IPlantUmlElement> nodes
/// <summary>
/// Removes the nodes whose name no dependency starts or ends at.
/// </summary>
private void RemoveNodesNotContainedInDependencies<T>(
Dictionary<T, IPlantUmlElement> nodes,
Func<T, string> nameOf
)
{
var namesInDependencies = new HashSet<string>(
_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<IPlantUmlElement> HandleNodes(
Dictionary<Slice, IPlantUmlElement> nodes
/// <summary>
/// The candidates, other than <paramref name="origin"/> itself, that one of the origin's
/// dependencies passing <see cref="GenerationOptions.DependencyFilter"/> points into.
/// </summary>
private static IEnumerable<T> SelectDependencies<T>(
T origin,
IEnumerable<T> candidates,
Func<T, string> nameOf,
Func<T, IEnumerable<IType>> 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<string>();
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<PlantUmlNamespace>()
.All(element => element.Name != namespcName)
)
)
{
nodeElements.Add(new PlantUmlNamespace(namespc));
}
}
}

return nodeElements;
}

private IEnumerable<Slice> 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)
{
Expand Down
2 changes: 1 addition & 1 deletion ArchUnitNET/Loader/TypeProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,7 @@ internal static void AddTypesToNamespaces(IEnumerable<IType> types)
{
foreach (var type in types)
{
((List<IType>)type.Namespace.Types).Add(type);
type.Namespace.AddType(type);
}
}
}
Expand Down
Loading
Loading