1 module uml;
2 
3 import model;
4 import std.algorithm;
5 import std.range;
6 import std.stdio;
7 import std.typecons;
8 version (unittest) import unit_threaded;
9 
10 Dependency[] read(R)(R input)
11 {
12     import std.array : appender;
13 
14     auto output = appender!(Dependency[]);
15 
16     read(input, output);
17     return output.data;
18 }
19 
20 private void read(Input, Output)(Input input, auto ref Output output)
21 {
22     import std.conv : to;
23     import std.regex : matchFirst, regex;
24 
25     enum arrow = `(?P<arrow><?\.+(left|right|up|down|le?|ri?|up?|do?|\[.*?\])*\.*>?)`;
26     enum pattern = regex(`(?P<lhs>\w+(.\w+)*)\s*` ~ arrow ~ `\s*(?P<rhs>\w+(.\w+)*)`);
27 
28     foreach (line; input)
29     {
30         auto captures = line.matchFirst(pattern);
31 
32         if (captures)
33         {
34             const lhs = captures["lhs"].to!string;
35             const rhs = captures["rhs"].to!string;
36 
37             if (captures["arrow"].endsWith(">"))
38                 output.put(Dependency(lhs, rhs));
39             if (captures["arrow"].startsWith("<"))
40                 output.put(Dependency(rhs, lhs));
41         }
42     }
43 }
44 
45 @("read PlantUML dependencies")
46 unittest
47 {
48     read(only("a .> b")).should.be == [Dependency("a", "b")];
49     read(only("a <. b")).should.be == [Dependency("b", "a")];
50     read(only("a <.> b")).should.be == [Dependency("a", "b"), Dependency("b", "a")];
51     read(only("a.[#red]>b")).should.be == [Dependency("a", "b")];
52     read(only("a.[#red]le>b")).should.be == [Dependency("a", "b")];
53 }
54 
55 void write(Output)(auto ref Output output, Dependency[] dependencies)
56 {
57     Package hierarchy;
58 
59     dependencies.each!(dependency => hierarchy.add(dependency));
60 
61     output.put("@startuml\n");
62     hierarchy.write(output);
63     output.put("@enduml\n");
64 }
65 
66 @("write PlantUML package diagram")
67 unittest
68 {
69     import std.array : appender;
70     import std..string : outdent, stripLeft;
71 
72     auto output = appender!string;
73     auto dependencies = [Dependency("a", "b")];
74 
75     output.write(dependencies);
76 
77     const expected = `
78         @startuml
79         package a {}
80         package b {}
81 
82         a ..> b
83         @enduml
84         `;
85 
86     output.data.should.be == outdent(expected).stripLeft;
87 }
88 
89 @("place internal dependencies inside the package")
90 unittest
91 {
92     import std.array : appender;
93     import std..string : outdent, stripLeft;
94 
95     auto output = appender!string;
96     auto dependencies = [Dependency("a", "a.b"), Dependency("a.b", "a.c")];
97 
98     output.write(dependencies);
99 
100     const expected = `
101         @startuml
102         package a {
103             package b as a.b {}
104             package c as a.c {}
105 
106             a.b ..> a.c
107         }
108 
109         a ..> a.b
110         @enduml
111         `;
112 
113     output.data.should.be == outdent(expected).stripLeft;
114 }
115 
116 private struct Package
117 {
118     string[] path;
119 
120     Package[string] subpackages;
121 
122     Dependency[] dependencies;
123 
124     void add(Dependency dependency)
125     {
126         const clientPath = dependency.client.names;
127         const supplierPath = dependency.supplier.names;
128         const path = commonPrefix(clientPath.dropBackOne, supplierPath.dropBackOne);
129 
130         addPackage(clientPath);
131         addPackage(supplierPath);
132         addDependency(path, dependency);
133     }
134 
135     void addPackage(const string[] path, size_t index = 0)
136     {
137         if (path[index] !in subpackages)
138             subpackages[path[index]] = Package(path[0 .. index + 1].dup);
139         if (index + 1 < path.length)
140             subpackages[path[index]].addPackage(path, index + 1);
141     }
142 
143     void addDependency(const string[] path, Dependency dependency)
144     {
145         if (path.empty)
146             dependencies ~= dependency;
147         else
148             subpackages[path.front].addDependency(path.dropOne, dependency);
149     }
150 
151     void write(Output)(auto ref Output output, size_t level = 0)
152     {
153         import std.format : formattedWrite;
154 
155         void indent()
156         {
157             foreach (_; 0 .. level)
158                 output.put("    ");
159         }
160 
161         foreach (subpackage; subpackages.keys.sort.map!(key => subpackages[key]))
162         {
163             indent;
164             if (subpackage.path.length == 1)
165                 output.formattedWrite!"package %s {"(subpackage.path.join('.'));
166             else
167                 output.formattedWrite!"package %s as %s {"(subpackage.path.back, subpackage.path.join('.'));
168 
169             if (!subpackage.subpackages.empty || !subpackage.dependencies.empty)
170             {
171                 output.put('\n');
172                 subpackage.write(output, level + 1);
173                 indent;
174             }
175             output.put("}\n");
176         }
177         if (!dependencies.empty)
178             output.put('\n');
179         foreach (dependency; dependencies.sort)
180         {
181             indent;
182             output.formattedWrite!"%s ..> %s\n"(dependency.client, dependency.supplier);
183         }
184     }
185 }