diff --git a/src/deps.d b/src/deps.d index 902434f..d32f624 100644 --- a/src/deps.d +++ b/src/deps.d @@ -6,7 +6,34 @@ import std.stdio; import std.typecons; version (unittest) import unit_threaded; -alias Dependency = Tuple!(string, "client", string, "supplier"); +struct Element +{ + string name; + + Flag!"recursive" recursive; + + int opCmp(const ref Element other) const + { + return tuple(this.name, this.recursive).opCmp(tuple(other.name, other.recursive)); + } + + string toLabel() const + { + return this.recursive ? (this.name ~ ".*") : this.name; + } + + string toPackage() const + { + return this.recursive ? (this.name ~ ".all") : this.name; + } + + string toString() const + { + return toPackage; + } +} + +alias Dependency = Tuple!(Element, "client", Element, "supplier"); auto moduleDependencies(alias predicate)(File file) { @@ -14,7 +41,9 @@ auto moduleDependencies(alias predicate)(File file) return reader(file.byLine) .filter!predicate - .map!(dependency => Dependency(dependency.client.name, dependency.supplier.name)); + .map!(dependency => Dependency( + Element(dependency.client.name, No.recursive), + Element(dependency.supplier.name, No.recursive))); } auto reader(R)(R input) diff --git a/src/graph.d b/src/graph.d index cc265b1..0485d3a 100644 --- a/src/graph.d +++ b/src/graph.d @@ -1,12 +1,11 @@ module graph; +import deps : Dependency, Element; import std.algorithm; import std.range; import std.typecons; version (unittest) import unit_threaded; -alias Dependency = Tuple!(string, "client", string, "supplier"); - void write(Output)(auto ref Output output, const Dependency[] dependencies) { import std.format : formattedWrite; @@ -15,12 +14,12 @@ void write(Output)(auto ref Output output, const Dependency[] dependencies) output.put("node [shape=box];\n"); foreach (element; dependencies.elements) { - output.formattedWrite!(`"%s"`)(element); + output.formattedWrite!(`"%s"`)(element.name); output.put('\n'); } foreach (dependency; dependencies) { - output.formattedWrite!(`"%s" -> "%s"`)(dependency.client, dependency.supplier); + output.formattedWrite!`"%s" -> "%s"`(dependency.client, dependency.supplier); output.put('\n'); } output.put("}\n"); @@ -33,7 +32,7 @@ unittest import std.string : outdent, stripLeft; auto output = appender!string; - const dependencies = [Dependency("a", "b")]; + const dependencies = [Dependency(Element("a", No.recursive), Element("b", No.recursive))]; output.write(dependencies); @@ -51,7 +50,7 @@ unittest void transitiveClosure(ref Dependency[] dependencies) { - string[] elements = dependencies.elements; + Element[] elements = dependencies.elements; foreach (element; elements) foreach (client; elements) @@ -63,10 +62,10 @@ void transitiveClosure(ref Dependency[] dependencies) Dependency[] transitiveReduction(ref Dependency[] dependencies) { - bool[string] mark = null; + bool[Element] mark = null; Dependency[] cyclicDependencies = null; - void traverse(string node) + void traverse(Element node) { import std.array : array; @@ -101,26 +100,29 @@ Dependency[] transitiveReduction(ref Dependency[] dependencies) @("apply transitive reduction") unittest { - auto dependencies = [Dependency("a", "b"), Dependency("b", "c"), Dependency("a", "c")]; + auto dependencies = [dependency("a", "b"), dependency("b", "c"), dependency("a", "c")]; auto cyclicDependencies = transitiveReduction(dependencies); - dependencies.should.be == [Dependency("a", "b"), Dependency("b", "c")]; + dependencies.should.be == [dependency("a", "b"), dependency("b", "c")]; cyclicDependencies.shouldBeEmpty; } @("apply transitive reduction to cyclic dependencies") unittest { - auto dependencies = [Dependency("a", "b"), Dependency("b", "c"), Dependency("c", "a")]; + auto dependencies = [dependency("a", "b"), dependency("b", "c"), dependency("c", "a")]; auto cyclicDependencies = transitiveReduction(dependencies); - dependencies.should.be == [Dependency("a", "b"), Dependency("b", "c"), Dependency("c", "a")]; + dependencies.should.be == [dependency("a", "b"), dependency("b", "c"), dependency("c", "a")]; cyclicDependencies.sort.should.be == dependencies; } -string[] elements(in Dependency[] dependencies) +private alias dependency = (client, supplier) => + Dependency(Element(client, No.recursive), Element(supplier, No.recursive)); + +Element[] elements(in Dependency[] dependencies) { - string[] elements = null; + Element[] elements = null; foreach (dependency; dependencies) { diff --git a/src/main.d b/src/main.d index 6ad8399..5e1dcfa 100644 --- a/src/main.d +++ b/src/main.d @@ -13,6 +13,7 @@ import std.regex; import std.stdio; import std.typecons; import uml; +import util; void main(string[] args) { @@ -95,11 +96,33 @@ void main(string[] args) dependency = Dependency(client, supplier); else { - dependency = Dependency(client.packages, supplier.packages); - if (dependency.client.empty || dependency.supplier.empty || dependency.client == dependency.supplier) + dependency = Dependency( + Element(client.name.packages, No.recursive), + Element(supplier.name.packages, No.recursive)); + if (dependency.client.name.empty || dependency.supplier.name.empty || + dependency.client == dependency.supplier) continue; } - if (!targetDependencies.canFind(dependency)) + bool matchDependency(const Dependency targetDependency, const Dependency actualDependency) + { + bool matchPackage(bool recursive, string targetPackage, string actualPackage) + { + if (recursive) + { + return actualPackage.fqnStartsWith(targetPackage); + } + else + { + return actualPackage == targetPackage; + } + } + return matchPackage(targetDependency.client.recursive, targetDependency.client.name, + actualDependency.client.name) && + matchPackage(targetDependency.supplier.recursive, targetDependency.supplier.name, + actualDependency.supplier.name); + } + + if (!targetDependencies.any!(a => matchDependency(a, dependency))) { stderr.writefln("error: unintended dependency %s -> %s", client, supplier); success = false; @@ -118,11 +141,11 @@ void main(string[] args) { foreach (dependency; actualDependencies) { - const client = dependency.client.packages; - const supplier = dependency.supplier.packages; + const client = dependency.client.name.packages; + const supplier = dependency.supplier.name.packages; if (!client.empty && !supplier.empty && client != supplier) - dependencies_.add(Dependency(client, supplier)); + dependencies_.add(Dependency(Element(client, No.recursive), Element(supplier, No.recursive))); } } if (!transitive) diff --git a/src/uml.d b/src/uml.d index 147cd08..64493f4 100644 --- a/src/uml.d +++ b/src/uml.d @@ -1,5 +1,6 @@ module uml; +import deps : Dependency, Element; import graph; import std.algorithm; import std.range; @@ -18,11 +19,10 @@ Dependency[] read(R)(R input) private void read(Input, Output)(Input input, auto ref Output output) { - import std.conv : to; import std.regex : matchFirst, regex; enum arrow = `(?P?)`; - enum pattern = regex(`(?P\w+(.\w+)*)\s*` ~ arrow ~ `\s*(?P\w+(.\w+)*)`); + enum pattern = regex(`(?P\w+(\.\w+)*(\.\*)?)\s*` ~ arrow ~ `\s*(?P\w+(\.\w+)*(\.\*)?)`); foreach (line; input) { @@ -30,13 +30,20 @@ private void read(Input, Output)(Input input, auto ref Output output) if (captures) { - const lhs = captures["lhs"].to!string; - const rhs = captures["rhs"].to!string; + enum recursiveMarker = ".all"; + + const string lhs = captures["lhs"].idup; + const lhsRecursive = lhs.endsWith(recursiveMarker) ? Yes.recursive : No.recursive; + const lhsElement = Element(lhsRecursive ? lhs.dropBack(recursiveMarker.length) : lhs, lhsRecursive); + + const string rhs = captures["rhs"].idup; + const rhsRecursive = rhs.endsWith(recursiveMarker) ? Yes.recursive : No.recursive; + const rhsElement = Element(rhsRecursive ? rhs.dropBack(recursiveMarker.length) : rhs, rhsRecursive); if (captures["arrow"].endsWith(">")) - output.put(Dependency(lhs, rhs)); + output.put(Dependency(lhsElement, rhsElement)); if (captures["arrow"].startsWith("<")) - output.put(Dependency(rhs, lhs)); + output.put(Dependency(rhsElement, lhsElement)); } } } @@ -44,11 +51,12 @@ private void read(Input, Output)(Input input, auto ref Output output) @("read Plant-UML dependencies") unittest { - read(only("a .> b")).should.be == [Dependency("a", "b")]; - read(only("a <. b")).should.be == [Dependency("b", "a")]; - read(only("a <.> b")).should.be == [Dependency("a", "b"), Dependency("b", "a")]; - read(only("a.[#red]>b")).should.be == [Dependency("a", "b")]; - read(only("a.[#red]le>b")).should.be == [Dependency("a", "b")]; + read(only("a .> b")).should.be == [dependency("a", "b")]; + read(only("a <. b")).should.be == [dependency("b", "a")]; + read(only("a <.> b")).should.be == [dependency("a", "b"), dependency("b", "a")]; + read(only("a.all .> b.all")).should.be == [Dependency(Element("a", Yes.recursive), Element("b", Yes.recursive))]; + read(only("a.[#red]>b")).should.be == [dependency("a", "b")]; + read(only("a.[#red]le>b")).should.be == [dependency("a", "b")]; } void write(Output)(auto ref Output output, const Dependency[] dependencies) @@ -69,7 +77,7 @@ unittest import std.string : outdent, stripLeft; auto output = appender!string; - const dependencies = [Dependency("a", "b")]; + const dependencies = [dependency("a", "b")]; output.write(dependencies); @@ -92,7 +100,7 @@ unittest import std.string : outdent, stripLeft; auto output = appender!string; - const dependencies = [Dependency("a", "a.b"), Dependency("a.b", "a.c")]; + const dependencies = [dependency("a", "a.b"), dependency("a.b", "a.c")]; output.write(dependencies); @@ -112,31 +120,59 @@ unittest output.data.should.be == outdent(expected).stripLeft; } +@("use appropriate wildcard descriptions") +unittest +{ + import std.array : appender; + import std.string : outdent, stripLeft; + + auto output = appender!string; + const dependencies = [Dependency(Element("a", Yes.recursive), Element("b", Yes.recursive))]; + + output.write(dependencies); + + const expected = ` + @startuml + package a.* as a.all {} + package b.* as b.all {} + + a.all ..> b.all + @enduml + `; + + output.data.should.be == outdent(expected).stripLeft; +} + +private alias dependency = (client, supplier) => + Dependency(Element(client, No.recursive), Element(supplier, No.recursive)); + private struct Package { string[] path; + Flag!"recursive" recursive; + Package[string] subpackages; Dependency[] dependencies; void add(Dependency dependency) { - const clientPath = dependency.client.split('.'); - const supplierPath = dependency.supplier.split('.'); + const clientPath = dependency.client.name.split('.'); + const supplierPath = dependency.supplier.name.split('.'); const path = commonPrefix(clientPath.dropBackOne, supplierPath.dropBackOne); - addPackage(clientPath); - addPackage(supplierPath); + addPackage(clientPath, dependency.client.recursive); + addPackage(supplierPath, dependency.client.recursive); addDependency(path, dependency); } - void addPackage(const string[] path, size_t index = 0) + void addPackage(const string[] path, Flag!"recursive" recursive, size_t index = 0) { if (path[index] !in subpackages) - subpackages[path[index]] = Package(path[0 .. index + 1].dup); + subpackages[path[index]] = Package(path[0 .. index + 1].dup, recursive); if (index + 1 < path.length) - subpackages[path[index]].addPackage(path, index + 1); + subpackages[path[index]].addPackage(path, recursive, index + 1); } void addDependency(const string[] path, Dependency dependency) @@ -160,7 +196,13 @@ private struct Package foreach (subpackage; subpackages.keys.sort.map!(key => subpackages[key])) { indent; - if (subpackage.path.length == 1) + if (subpackage.recursive) + { + assert(subpackage.subpackages.empty && subpackage.dependencies.empty, + "recursive package must not contain subpackages"); + output.formattedWrite!"package %s.* as %s.all {"(subpackage.path.back, subpackage.path.join('.')); + } + else if (subpackage.path.length == 1) output.formattedWrite!"package %s {"(subpackage.path.join('.')); else output.formattedWrite!"package %s as %s {"(subpackage.path.back, subpackage.path.join('.')); @@ -178,7 +220,7 @@ private struct Package foreach (dependency; dependencies.sort) { indent; - output.formattedWrite!"%s ..> %s\n"(dependency.client, dependency.supplier); + output.formattedWrite!"%s ..> %s\n"(dependency.client.toPackage, dependency.supplier.toPackage); } } } diff --git a/src/util.d b/src/util.d new file mode 100644 index 0000000..c624541 --- /dev/null +++ b/src/util.d @@ -0,0 +1,10 @@ +module util; + +import std.string; + +bool fqnStartsWith(string haystack, string needle) +{ + import std.algorithm : splitter; + + return haystack.splitter(".").startsWith(needle.splitter(".")); +}