1 module uml;
2 
3 import graph;
4 import std.range;
5 import std.stdio;
6 
7 Dependency[] read(R)(R input)
8 {
9     import std.array : appender;
10 
11     auto output = appender!(Dependency[]);
12 
13     read(input, output);
14     return output.data;
15 }
16 
17 private void read(Input, Output)(Input input, auto ref Output output)
18 {
19     import std.algorithm : endsWith, startsWith;
20     import std.conv : to;
21     import std.regex : matchFirst, regex;
22 
23     enum arrow = `(?P<arrow><?\.+(left|right|up|down|le?|ri?|up?|do?|\[.*?\])*\.*>?)`;
24     enum pattern = regex(`(?P<lhs>\w+(.\w+)*)\s*` ~ arrow ~ `\s*(?P<rhs>\w+(.\w+)*)`);
25 
26     foreach (line; input)
27     {
28         auto captures = line.matchFirst(pattern);
29 
30         if (captures)
31         {
32             const lhs = captures["lhs"].to!string;
33             const rhs = captures["rhs"].to!string;
34 
35             if (captures["arrow"].endsWith(">"))
36                 output.put(Dependency(lhs, rhs));
37             if (captures["arrow"].startsWith("<"))
38                 output.put(Dependency(rhs, lhs));
39         }
40     }
41 }
42 
43 /// reads Plant-UML dependencies
44 unittest
45 {
46     assert(read(only("a .> b")) == [Dependency("a", "b")]);
47     assert(read(only("a <. b")) == [Dependency("b", "a")]);
48     assert(read(only("a <.> b")) == [Dependency("a", "b"), Dependency("b", "a")]);
49     assert(read(only("a.[#red]>b")) == [Dependency("a", "b")]);
50     assert(read(only("a.[#red]le>b")) == [Dependency("a", "b")]);
51 }
52 
53 void write(Output)(auto ref Output output, const Dependency[] dependencies)
54 {
55     import std.algorithm : sort;
56 
57     auto writer = writer(formatter(output));
58 
59     output.put("@startuml\n");
60     foreach (element; dependencies.elements.sort)
61         writer.put(element.split('.'));
62     writer.close;
63     output.put('\n');
64     foreach (dependency; dependencies)
65     {
66         output.put(dependency.client);
67         output.put(" ..> ");
68         output.put(dependency.supplier);
69         output.put('\n');
70     }
71     output.put("@enduml\n");
72 }
73 
74 /// writes Plant-UML package diagram
75 unittest
76 {
77     import std.array : appender;
78     import std.string : outdent, stripLeft;
79 
80     auto output = appender!string;
81     const dependencies = [Dependency("a", "b")];
82 
83     output.write(dependencies);
84 
85     const expected = `
86         @startuml
87         package a {}
88         package b {}
89 
90         a ..> b
91         @enduml
92         `;
93 
94     assert(output.data == outdent(expected).stripLeft, output.data);
95 }
96 
97 auto writer(Formatter)(Formatter formatter)
98 {
99     return Writer!Formatter(formatter);
100 }
101 
102 struct Writer(Formatter)
103 {
104     Formatter formatter;
105 
106     string[] prev = null;
107 
108     void put(string[] path)
109     in
110     {
111         assert(!path.empty);
112     }
113     do
114     {
115         import std.algorithm : commonPrefix;
116 
117         auto prefix = commonPrefix(this.prev, path);
118 
119         close(this.prev.length - prefix.length);
120         this.prev = path.dup;
121         path.popFrontN(prefix.length);
122         put(prefix, path);
123     }
124 
125     void put(string[] prefix, string[] path)
126     in
127     {
128         assert(!path.empty);
129     }
130     do
131     {
132         import std.format : format;
133         import std.string : join;
134 
135         const name = path.front;
136 
137         prefix ~= name;
138         path.popFront;
139         if (prefix.length == 1)
140             this.formatter.open(format!"package %s {"(name));
141         else
142             this.formatter.open(format!"package %s as %s {"(name, prefix.join('.')));
143         if (!path.empty)
144             put(prefix, path);
145     }
146 
147     void close()
148     {
149         close(this.prev.length);
150         this.prev = null;
151     }
152 
153     void close(size_t n)
154     {
155         foreach (_; 0 .. n)
156             this.formatter.close("}");
157     }
158 
159 }
160 
161 auto formatter(Output)(auto ref Output output)
162 {
163     return Formatter!Output(output);
164 }
165 
166 struct Formatter(Output)
167 {
168     Output output;
169 
170     size_t indent = 0;
171 
172     bool pending = false;
173 
174     void open(string s)
175     {
176         if (this.pending)
177             this.output.put("\n");
178         indentation;
179         this.output.put(s);
180         ++this.indent;
181         this.pending = true;
182     }
183 
184     void close(string s)
185     {
186         --this.indent;
187         if (!this.pending)
188             indentation;
189         this.output.put(s);
190         this.output.put("\n");
191         this.pending = false;
192     }
193 
194     void indentation()
195     {
196         foreach (_; 0 .. this.indent)
197             this.output.put("    ");
198     }
199 }
200 
201 /// writes nested packages
202 unittest
203 {
204     import std.array : appender;
205     import std.string : outdent, stripLeft;
206 
207     auto output = appender!string;
208     auto writer = writer(formatter(output));
209 
210     writer.put(["a", "b", "x"]);
211     writer.put(["a", "b", "y"]);
212     writer.put(["a", "z"]);
213     writer.close;
214 
215     const expected = `
216         package a {
217             package b as a.b {
218                 package x as a.b.x {}
219                 package y as a.b.y {}
220             }
221             package z as a.z {}
222         }
223         `;
224 
225     assert(output.data == outdent(expected).stripLeft);
226 }