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 }