1 import Base 2 import ../../../general/rose/type.bend as R 3 import ../../../general/rose/ops.bend as RO 4 import ../../../general/compound/type.bend as C 5 import ../../../general/compound/ops.bend as CO 6 import ../../../general/compound/edge/type.bend as E 7 import ../type.bend as A 8 import ../ops.bend as O 9 import ../system/deployment/type.bend as D 10 import ./type.bend as Vw 11 12 # ---- one element as a D2 box, in C4's colors: people dark blue, what the view is about blue, 13 # everything around it gray ---- 14 def level_name(l: A.Level) -> String: 15 match l: 16 case A.PersonLevel{}: 17 "Person" 18 case A.SystemLevel{}: 19 "Software System" 20 case A.ContainerLevel{}: 21 "Container" 22 case A.ComponentLevel{}: 23 "Component" 24 case A.ExternalLevel{}: 25 "Software System" 26 27 def kind_line(+l: A.Level, +t: String) -> String: 28 Bool.pick(String, String.is_empty(t), "[" ++ level_name(l) ++ "]", "[" ++ level_name(l) ++ ": " ++ t ++ "]") 29 30 def is_person(l: A.Level) -> Bool: 31 match l: 32 case A.PersonLevel{}: 33 True{} 34 case _: 35 False{} 36 37 def focus_fill(l: A.Level) -> String: 38 match l: 39 case A.PersonLevel{}: 40 "#08427B" 41 case A.SystemLevel{}: 42 "#1168BD" 43 case A.ContainerLevel{}: 44 "#438DD5" 45 case A.ComponentLevel{}: 46 "#85BBF0" 47 case A.ExternalLevel{}: 48 "#999999" 49 50 def fill(+l: A.Level, focus: Bool) -> String: 51 match focus: 52 case False{}: 53 Bool.pick(String, is_person(l), "#08427B", "#999999") 54 case True{}: 55 focus_fill(l) 56 57 def box(+path: String, e: A.Element, +focus: Bool) -> String: 58 match e: 59 case A.Element{n, d, t, +l}: 60 path ++ ": |md\n **" ++ n ++ "**\n\n " ++ kind_line(l, t) ++ "\n\n " ++ d ++ "\n| {shape: " ++ Bool.pick(String, is_person(l), "c4-person", "rectangle") ++ "; style.fill: \"" ++ fill(l, focus) ++ "\"; style.stroke: \"" ++ fill(l, focus) ++ "\"; style.font-color: \"#FFFFFF\"}\n" 61 62 def element(+k: String, +rs: List<&2, R.Rose>) -> A.Element: 63 Maybe.default(&2, A.Element, RO.find(A.Element, k, rs), A.Element{k, "", "", A.ExternalLevel{}}) 64 65 # D2 names are the keys, prefixed so none is a D2 keyword (like near). 66 def boxes(ks: List<&2, String>, +prefix: String, +focus: Bool, +rs: List<&2, R.Rose>) -> String: 67 match ks: 68 case []: 69 "" 70 case +k <> rest: 71 box(prefix ++ "e_" ++ k, element(k, rs), focus) ++ boxes(rest, prefix, focus, rs) 72 73 # ---- which keys a view shows ---- 74 def keep_unless(c: Bool, x: String, rest: List<&2, String>) -> List<&2, String>: 75 match c: 76 case True{}: 77 rest 78 case False{}: 79 x <> rest 80 81 def without(+k: String, xs: List<&2, String>) -> List<&2, String>: 82 match xs: 83 case []: 84 [] 85 case +x <> rest: 86 keep_unless(String.eq(k, x), x, without(k, rest)) 87 88 # The keys the view is about, drawn inside its boundary. 89 def inner(+a: A.Architecture, v: Vw.View) -> List<&2, String>: 90 match v: 91 case Vw.Context{}: 92 [O.system_key(a)] 93 case Vw.Containers{}: 94 O.container_keys(O.containers(a)) 95 case Vw.Components{c}: 96 O.components_of(c, O.containers(a)) 97 case Vw.Deployment{}: 98 [] 99 case Vw.Dynamic{_}: 100 [] 101 102 # The keys the view may show: its own, the rest of the system at the level above, and the environment. 103 def visible(+a: A.Architecture, v: Vw.View) -> List<&2, String>: 104 match v: 105 case Vw.Context{}: 106 O.system_key(a) <> O.environment_keys(a) 107 case Vw.Containers{}: 108 List.append(&2, String, O.container_keys(O.containers(a)), O.environment_keys(a)) 109 case Vw.Components{+c}: 110 List.append(&2, String, O.components_of(c, O.containers(a)), List.append(&2, String, without(c, O.container_keys(O.containers(a))), O.environment_keys(a))) 111 case Vw.Deployment{}: 112 [] 113 case Vw.Dynamic{_}: 114 [] 115 116 def boundary(+a: A.Architecture, v: Vw.View) -> String: 117 match v: 118 case Vw.Context{}: 119 "" 120 case Vw.Containers{}: 121 O.system_key(a) 122 case Vw.Components{c}: 123 c 124 case Vw.Deployment{}: 125 "" 126 case Vw.Dynamic{_}: 127 O.system_key(a) 128 129 def ename(e: A.Element) -> String: 130 match e: 131 case A.Element{n, _, _, _}: 132 n 133 134 def title(+a: A.Architecture, +rs: List<&2, R.Rose>, v: Vw.View) -> String: 135 match v: 136 case Vw.Context{}: 137 "System context: " ++ ename(element(O.system_key(a), rs)) 138 case Vw.Containers{}: 139 "Containers: " ++ ename(element(O.system_key(a), rs)) 140 case Vw.Components{c}: 141 "Components: " ++ ename(element(c, rs)) 142 case Vw.Deployment{}: 143 "Deployment: " ++ ename(element(O.system_key(a), rs)) 144 case Vw.Dynamic{_}: 145 "Dynamic: " ++ ename(element(O.system_key(a), rs)) 146 147 # ---- drawing ---- 148 def header(+t: String) -> String: 149 "direction: down\ntitle: \"" ++ t ++ "\" {near: top-center; shape: text; style.font-size: 28; style.bold: true}\n" 150 151 def roots(c: C.Compound) -> List<&2, R.Rose>: 152 match c: 153 case C.Compound{rs, _}: 154 rs 155 156 def keep_edge(c: Bool, e: E.Edge, rest: List<&2, E.Edge>) -> List<&2, E.Edge>: 157 match c: 158 case True{}: 159 e <> rest 160 case False{}: 161 rest 162 163 # The edges with an end among the view's own keys. 164 def touching(es: List<&2, E.Edge>, +ins: List<&2, String>) -> List<&2, E.Edge>: 165 match es: 166 case []: 167 [] 168 case e <> rest: 169 match e: 170 case E.Edge{+x, +y, l, t}: 171 keep_edge(CO.has(ins, x) || CO.has(ins, y), E.Edge{x, y, l, t}, touching(rest, ins)) 172 173 def keep_new(c: Bool, +k: String, rest: List<&2, String>) -> List<&2, String>: 174 match c: 175 case True{}: 176 k <> rest 177 case False{}: 178 rest 179 180 # The keys around the view's own: whatever its edges reach, once each. 181 def around(es: List<&2, E.Edge>, +ins: List<&2, String>, +seen: List<&2, String>) -> List<&2, String>: 182 match es: 183 case []: 184 [] 185 case e <> rest: 186 match e: 187 case E.Edge{+x, +y, _, _}: 188 keep_new(Bool.not(CO.has(ins, x) || CO.has(seen, x)), x, 189 keep_new(Bool.not(CO.has(ins, y) || CO.has(x <> seen, y) || String.eq(x, y)), y, around(rest, ins, x <> y <> seen))) 190 191 def path(+k: String, +ins: List<&2, String>, +b: String) -> String: 192 Bool.pick(String, CO.has(ins, k) && Bool.not(String.is_empty(b)), "e_" ++ b ++ ".e_" ++ k, "e_" ++ k) 193 194 def label(+d: String, +t: String) -> String: 195 "\"" ++ d ++ Bool.pick(String, String.is_empty(t), "", "\\n[" ++ t ++ "]") ++ "\"" 196 197 def edge_lines(es: List<&2, E.Edge>, +ins: List<&2, String>, +b: String) -> String: 198 match es: 199 case []: 200 "" 201 case e <> rest: 202 match e: 203 case E.Edge{x, y, d, t}: 204 path(x, ins, b) ++ " -> " ++ path(y, ins, b) ++ ": " ++ label(d, t) ++ "\n" ++ edge_lines(rest, ins, b) 205 206 def inner_boxes(+ins: List<&2, String>, +b: String, +rs: List<&2, R.Rose>) -> String: 207 Bool.pick(String, String.is_empty(b), boxes(ins, "", True{}, rs), 208 "e_" ++ b ++ ": \"" ++ ename(element(b, rs)) ++ "\" {style.fill: \"transparent\"; style.stroke: \"#444444\"; style.stroke-dash: 4}\n" ++ boxes(ins, "e_" ++ b ++ ".", True{}, rs)) 209 210 def draw(+t: String, +ins: List<&2, String>, +b: String, +rs: List<&2, R.Rose>, +es: List<&2, E.Edge>) -> String: 211 header(t) ++ inner_boxes(ins, b, rs) ++ boxes(around(es, ins, []), "", False{}, rs) ++ edge_lines(es, ins, b) 212 213 # Context, containers and components: the view's keys, and the architecture's edges lifted to what shows. 214 def static_view(+a: A.Architecture, +v: Vw.View) -> String: 215 +c = O.to_compound(a) 216 +ins = inner(a, v) 217 draw(title(a, roots(c), v), ins, boundary(a, v), roots(c), touching(CO.lift(A.Element, c, visible(a, v)), ins)) 218 219 # ---- dynamic: numbered steps between elements as they are ---- 220 def step_edges(ss: List<&2, Vw.Step>, +n: Nat) -> List<&2, E.Edge>: 221 match ss: 222 case []: 223 [] 224 case s <> rest: 225 match s: 226 case Vw.Step{x, y, d, t}: 227 E.Edge{x, y, Nat.show(n) ++ ". " ++ d, t} <> step_edges(rest, Nat.add(n, 1n)) 228 229 # The steps' ends inside the system, once each. 230 def inside(es: List<&2, E.Edge>, +sys: List<&2, String>, +seen: List<&2, String>) -> List<&2, String>: 231 match es: 232 case []: 233 [] 234 case e <> rest: 235 match e: 236 case E.Edge{+x, +y, _, _}: 237 keep_new(CO.has(sys, x) && Bool.not(CO.has(seen, x)), x, 238 keep_new(CO.has(sys, y) && Bool.not(CO.has(x <> seen, y)), y, inside(rest, sys, x <> y <> seen))) 239 240 def dynamic_view(+a: A.Architecture, ss: List<&2, Vw.Step>) -> String: 241 +c = O.to_compound(a) 242 +es = step_edges(ss, 1n) 243 +sys = List.append(&2, String, O.container_keys(O.containers(a)), O.all_component_keys(O.containers(a))) 244 draw(title(a, roots(c), Vw.Dynamic{[]}), inside(es, sys, []), O.system_key(a), roots(c), es) 245 246 # ---- deployment: the machines, nested, with the containers running on them ---- 247 def node_kind(+t: String) -> String: 248 Bool.pick(String, String.is_empty(t), "[Deployment Node]", "[Deployment Node: " ++ t ++ "]") 249 250 def instance_boxes(ks: List<&2, String>, +p: String, +rs: List<&2, R.Rose>) -> String: 251 match ks: 252 case []: 253 "" 254 case +k <> rest: 255 box(p ++ "e_" ++ k, element(k, rs), True{}) ++ instance_boxes(rest, p, rs) 256 257 def node_lines(ns: List<&2, D.Node>, +prefix: String, +rs: List<&2, R.Rose>) -> String: 258 match ns: 259 case []: 260 "" 261 case n <> rest: 262 match n: 263 case D.Node{+k, name, +t, _, _, _, kids, ins}: 264 prefix ++ "d_" ++ k ++ ": \"" ++ name ++ "\\n" ++ node_kind(t) ++ "\" {style.fill: \"#FFFFFF\"; style.stroke: \"#888888\"; style.font-color: \"#000000\"}\n" ++ 265 instance_boxes(ins, prefix ++ "d_" ++ k ++ ".", rs) ++ node_lines(kids, prefix ++ "d_" ++ k ++ ".", rs) ++ node_lines(rest, prefix, rs) 266 267 # Where each instance is drawn: its container's key and its D2 path. 268 type Placed is Data: 269 Placed{key: String, path: String} 270 271 def instance_places(ks: List<&2, String>, +p: String) -> List<&2, Placed>: 272 match ks: 273 case []: 274 [] 275 case +k <> rest: 276 Placed{k, p ++ "e_" ++ k} <> instance_places(rest, p) 277 278 def places(ns: List<&2, D.Node>, +prefix: String) -> List<&2, Placed>: 279 match ns: 280 case []: 281 [] 282 case n <> rest: 283 match n: 284 case D.Node{+k, _, _, _, _, _, kids, ins}: 285 List.append(&2, Placed, instance_places(ins, prefix ++ "d_" ++ k ++ "."), 286 List.append(&2, Placed, places(kids, prefix ++ "d_" ++ k ++ "."), places(rest, prefix))) 287 288 def to_each(ps: List<&2, Placed>, +from: String, +y: String, +d: String, +t: String, +rest: List<&2, E.Edge>) -> List<&2, E.Edge>: 289 match ps: 290 case []: 291 rest 292 case p <> more: 293 match p: 294 case Placed{pk, +pp}: 295 keep_edge(String.eq(pk, y) && Bool.not(String.eq(from, pp)), E.Edge{from, pp, d, t}, to_each(more, from, y, d, t, rest)) 296 297 def from_here(c: Bool, +all: List<&2, Placed>, +pp: String, +y: String, +d: String, +t: String, +tail: List<&2, E.Edge>) -> List<&2, E.Edge>: 298 match c: 299 case True{}: 300 to_each(all, pp, y, d, t, tail) 301 case False{}: 302 tail 303 304 def from_each(ps: List<&2, Placed>, +all: List<&2, Placed>, +x: String, +y: String, +d: String, +t: String, +rest: List<&2, E.Edge>) -> List<&2, E.Edge>: 305 match ps: 306 case []: 307 rest 308 case p <> more: 309 match p: 310 case Placed{pk, +pp}: 311 from_here(String.eq(pk, x), all, pp, y, d, t, from_each(more, all, x, y, d, t, rest)) 312 313 # Each container edge, drawn between every pair of its ends' instances. 314 def instance_edges(es: List<&2, E.Edge>, +ps: List<&2, Placed>) -> List<&2, E.Edge>: 315 match es: 316 case []: 317 [] 318 case e <> rest: 319 match e: 320 case E.Edge{x, y, d, t}: 321 from_each(ps, ps, x, y, d, t, instance_edges(rest, ps)) 322 323 def path_lines(es: List<&2, E.Edge>) -> String: 324 match es: 325 case []: 326 "" 327 case e <> rest: 328 match e: 329 case E.Edge{x, y, d, t}: 330 x ++ " -> " ++ y ++ ": " ++ label(d, t) ++ "\n" ++ path_lines(rest) 331 332 def deployment_view(+a: A.Architecture) -> String: 333 +c = O.to_compound(a) 334 +ns = O.nodes(a) 335 header(title(a, roots(c), Vw.Deployment{})) ++ node_lines(ns, "", roots(c)) ++ 336 path_lines(instance_edges(CO.lift(A.Element, c, O.container_keys(O.containers(a))), places(ns, ""))) 337 338 # ---- a view as D2 ---- 339 def d2(+a: A.Architecture, v: Vw.View) -> String: 340 match v: 341 case Vw.Context{}: 342 static_view(a, Vw.Context{}) 343 case Vw.Containers{}: 344 static_view(a, Vw.Containers{}) 345 case Vw.Components{c}: 346 static_view(a, Vw.Components{c}) 347 case Vw.Deployment{}: 348 deployment_view(a) 349 case Vw.Dynamic{ss}: 350 dynamic_view(a, ss)