archsight 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/docs/diagram.md +101 -4
- data/lib/archsight/diagram/graph/attributes.rb +59 -6
- data/lib/archsight/diagram/graph/builder.rb +101 -3
- data/lib/archsight/diagram/graph/edge.rb +7 -1
- data/lib/archsight/diagram/graph/node.rb +29 -1
- data/lib/archsight/diagram/graph/port.rb +25 -0
- data/lib/archsight/diagram/graph.rb +18 -1
- data/lib/archsight/diagram/includes.rb +22 -4
- data/lib/archsight/diagram/layout/alignment.rb +138 -0
- data/lib/archsight/diagram/layout/expander.rb +13 -0
- data/lib/archsight/diagram/layout/ports.rb +182 -0
- data/lib/archsight/diagram/layout/rank_arranger.rb +27 -1
- data/lib/archsight/diagram/layout/stacker.rb +3 -2
- data/lib/archsight/diagram/layout.rb +15 -5
- data/lib/archsight/diagram/legend/inventory.rb +52 -4
- data/lib/archsight/diagram/legend/row.rb +44 -0
- data/lib/archsight/diagram/parser/ast.rb +9 -0
- data/lib/archsight/diagram/parser.rb +56 -5
- data/lib/archsight/diagram/renderer/edge_renderer.rb +1 -1
- data/lib/archsight/diagram/renderer/node_renderer.rb +35 -1
- data/lib/archsight/diagram/renderer/path_renderer.rb +2 -2
- data/lib/archsight/diagram/renderer/stylesheet.rb +10 -1
- data/lib/archsight/diagram/renderer/text_renderer.rb +1 -0
- data/lib/archsight/diagram/renderer.rb +21 -5
- data/lib/archsight/diagram/routing/edge_routing.rb +106 -5
- data/lib/archsight/diagram/support/markdown_text.rb +2 -2
- data/lib/archsight/diagram/support/text_metrics.rb +7 -3
- data/lib/archsight/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8c3016b6533ac9d58b36bbf89d5bb34b6f62abe9579bd091f22e735b53cf3c9
|
|
4
|
+
data.tar.gz: 00e6854efc963f6c8916b0b16f8ffd2d4462ab5ccd6e2f84b1eb1d906e479d2a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9dbeaf9d8f3adf87ee96139436a145e888fbde2ac09feedfe7ddc3835583aa3a5e9d37c7a9e768ba7bb2e44935d2e81723d55af8cc0af7cd0531a0389689162e
|
|
7
|
+
data.tar.gz: 21d812087816f97bd171e5cdc30aea5ca81fec27ecab2e677817081305a721000a2ac604429c0be78d59170ed21ec01a79aa9f54492986ea81ce3e99ff5fd66f
|
data/docs/diagram.md
CHANGED
|
@@ -153,18 +153,25 @@ hint for near misses), never silently ignored.
|
|
|
153
153
|
| `resource` | nodes, containers | a resource name (`Archsight:Web`) or `Kind/Name` (`ApplicationService/Archsight:Web`); see *Linking to resources* |
|
|
154
154
|
| `extend` | nodes, containers | `true`, `false`; containers also `height`, and `width` on a direct child of a `layer` |
|
|
155
155
|
| `width` | `text` | characters per line (positive integer); wraps the label |
|
|
156
|
-
| `anchor` | containers | `top`, `bottom`, `left`, `right`, `center` |
|
|
156
|
+
| `anchor` | containers; leaves with `no-extend` | `top`, `bottom`, `left`, `right`, `center` |
|
|
157
|
+
| `badge` | nodes, ports | the id of a top-level `badge` definition (see *Badges and line styles*) |
|
|
158
|
+
| `legend` | nodes, containers | the text of the node's tint row in the legend (see *Legend*) |
|
|
159
|
+
| `align` | nodes, containers | `[edge] id` of another node to line up with, e.g. `top worker`; up to one horizontal and one vertical entry, comma-separated (see *Aligning across containers*) |
|
|
157
160
|
|
|
158
161
|
`extend "height"` on a direct child of a `layer` grows it to its tallest sibling. Inside such a box (also through a `boundary` around a `stack`), a `stack` child carrying `extend "height"` takes the extra height and moves the ranks below it down, so e.g. a footer rank lines up across the columns. The child can be a `layer` too; its own `extend "height"` children then fill the layer's new height.
|
|
159
162
|
|
|
160
163
|
`extend "width"` is the same for the other axis. When a `layer` is made wider than its content (for example it sits in a `stack` next to a wider sibling), its children normally stay together as one block placed by `anchor`. A direct child of the layer with `extend "width"` instead takes the spare width and pushes the siblings after it to the right edge, so `main` can sit at the left edge and `aside` at the right edge, and the `aside` columns line up across stacked layers. Several children with `extend "width"` share the spare width equally. The child's own content stays at its `anchor` (`left` by default) at its natural size. The attribute is an error anywhere but on a direct child of a `layer`; edges to boxes inside the pushed siblings follow their new positions.
|
|
161
164
|
|
|
162
165
|
`anchor` says where a container's content sits when its box grows: `bottom` (or `top`) when it is made taller, `right` (or `left`) when it is made wider, `center` for the middle. Default: top for `extend "height"`, left for `extend "width"`, centered otherwise. It is not inherited, and has no effect on a box whose room is taken by a stack child with `extend "height"`.
|
|
166
|
+
|
|
167
|
+
`no-extend` on a leaf keeps it at its own size inside a wider `stack` (which otherwise stretches every rank to the widest one), centered there; `anchor "left"`/`"right"` puts it at that edge instead (an error without `no-extend`).
|
|
163
168
|
| `shape` | leaves | `rectangle`, `circle`, `cylinder`, `pipe`, `actor`, `file`, `module` |
|
|
164
169
|
| `gap` | containers | pixels (`40`), a percentage (`150%`) or a signed one (`+20%`, `-50%`) |
|
|
170
|
+
| `padding` | containers | the room between the box and its content, same values as `gap` (default: the theme's) |
|
|
165
171
|
| `ranks` | containers | `auto`, `on`, `down`, `right`, `off` |
|
|
166
172
|
| `columns` | containers | a positive integer |
|
|
167
173
|
| `style` | edges | `straight`, `orthogonal` |
|
|
174
|
+
| `line` | edges | `solid` (default), `dashed`, `dotted`: the stroke only, the relation keeps its arrowhead |
|
|
168
175
|
| `relation` | edges | `dependency`, `implements`, `control`, `data` |
|
|
169
176
|
| `color` | dataflows | a hex color such as `#1a56db` |
|
|
170
177
|
|
|
@@ -182,8 +189,10 @@ empty id, and an edge from a node to itself are errors too.
|
|
|
182
189
|
Any label anywhere (node, container, edge, dataflow) supports:
|
|
183
190
|
|
|
184
191
|
- A small inline markdown-lite: `**bold**`, `*italic*`, `__underline__`,
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
`` `code` `` (monospace, for commands, file names and ports), and
|
|
193
|
+
`[text](url)` for a link on just that run of text (independent of
|
|
194
|
+
a node-level `link` — a label can have both, or just one). Runs don't
|
|
195
|
+
nest: `` **`x`** `` shows the backticks.
|
|
187
196
|
- Multi-line text, written either with an explicit `\n` escape
|
|
188
197
|
(`label "Line one\nLine two"`) or by literally spanning multiple lines
|
|
189
198
|
in the source; in the latter case each line's leading/trailing
|
|
@@ -214,6 +223,89 @@ Edge attributes:
|
|
|
214
223
|
you pass `--relation=`, though layout always accounts for every edge
|
|
215
224
|
regardless of what's shown.
|
|
216
225
|
|
|
226
|
+
### Ports
|
|
227
|
+
|
|
228
|
+
A port is a small labelled tab on a leaf's border -- a TCP port, a socket,
|
|
229
|
+
a file a process listens on -- that edges attach to instead of the leaf as a
|
|
230
|
+
whole. Declare it inside the leaf and name it in an edge as `leaf.port`:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
component "mgr" {
|
|
234
|
+
label "**manager**"
|
|
235
|
+
port "tcp" { label "tcp :8075"; side "top" }
|
|
236
|
+
port "sock" { label "unix mgr.sock"; side "top"; badge "beta" }
|
|
237
|
+
}
|
|
238
|
+
client -> mgr.tcp { label "requests" }
|
|
239
|
+
agent -> mgr.sock
|
|
240
|
+
mgr.sock -> logger
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- `label` is drawn in monospace (default: the id), `side` is `top`,
|
|
244
|
+
`bottom`, `left` or `right`, `at` is `left`, `center`, `right` (`top`,
|
|
245
|
+
`center`, `bottom` on a left/right side) or a percentage of the side
|
|
246
|
+
(`25%`), and `badge` names a badge (see below).
|
|
247
|
+
- Without `side`, the port goes on the side facing what its edges connect
|
|
248
|
+
it to (`top` without edges). Without `at`, it slides along its side to
|
|
249
|
+
line up with what its edges connect it to (the mean of their centers),
|
|
250
|
+
unless that spans the whole side; ports without a preference share the
|
|
251
|
+
side evenly in declaration order. Tabs on one side never overlap.
|
|
252
|
+
- The leaf grows to hold its tabs, and a `stack` leaves room between two
|
|
253
|
+
ranks for tabs sticking out of the sides they face.
|
|
254
|
+
- An edge to a port ends on the tab's outer side and never runs through
|
|
255
|
+
its own leaf; several edges on one tab are spread over it. Tabs are
|
|
256
|
+
obstacles to every other line, so no line ends on top of one.
|
|
257
|
+
- Ports need a box-shaped leaf (`rectangle`, `file`, `module` shapes). A
|
|
258
|
+
port id is local to its leaf and has no `.`. `leaf.port` resolves to a
|
|
259
|
+
node with that whole id first (ids of a namespaced include contain dots),
|
|
260
|
+
and an unknown port is an error listing the leaf's ports. Dataflow hops
|
|
261
|
+
name nodes, not ports.
|
|
262
|
+
|
|
263
|
+
### Badges and line styles
|
|
264
|
+
|
|
265
|
+
Top-level definitions give the legend its text for a marker:
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
badge "beta" { tint "orange"; label "beta channel only" }
|
|
269
|
+
line "dashed" { label "read only at start" }
|
|
270
|
+
|
|
271
|
+
component "agent" { badge "beta" }
|
|
272
|
+
config -> mgr { line "dashed"; label "read at start" }
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- `badge "id" { tint; label }` defines a dot that nodes and ports carry
|
|
276
|
+
with `badge "id"` (inside a node's top-right corner, at a port tab's right
|
|
277
|
+
end). Each badge used gets a legend row.
|
|
278
|
+
- `line "dashed" { label }` (or `"dotted"`) gives that line style its
|
|
279
|
+
legend row; without the definition, `line "dashed"` edges still draw
|
|
280
|
+
dashed but have no row. A diagram with ports gets a "Port" row.
|
|
281
|
+
- An included diagram's definitions count at the top level; the same
|
|
282
|
+
definition twice is fine, two different ones with one id are an error.
|
|
283
|
+
Inside a container-level include they are ignored with a warning.
|
|
284
|
+
|
|
285
|
+
### Aligning across containers
|
|
286
|
+
|
|
287
|
+
`align "<edge> other"` moves a node (or container) until that edge of it
|
|
288
|
+
lines up with the same edge of `other`, which may sit in a different
|
|
289
|
+
container -- like the align buttons of a drawing editor:
|
|
290
|
+
|
|
291
|
+
| edge | lines up |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `left`, `center`, `right` | the left edges, centers, right edges (horizontally) |
|
|
294
|
+
| `top`, `middle`, `bottom` | the top edges, centers, bottom edges (vertically) |
|
|
295
|
+
|
|
296
|
+
A bare id means `center` (`align "api"` puts a node under `api`).
|
|
297
|
+
One horizontal and one vertical entry can be combined with a comma:
|
|
298
|
+
`align "left db, top worker"`.
|
|
299
|
+
|
|
300
|
+
It only uses the room its container already has and never grows one.
|
|
301
|
+
Where the container packs along that axis (a `layer` for left/right, a
|
|
302
|
+
`stack` for up/down) the node's siblings keep their order and are pushed
|
|
303
|
+
along only when they would come closer than the sibling gap (plus half a
|
|
304
|
+
port tab on each facing side); elsewhere the node moves within its
|
|
305
|
+
container's inner box (an anonymous `layer`/`stack` lends it the room of
|
|
306
|
+
its own container on its packing axis). It stops as close as it gets. A
|
|
307
|
+
leaf a `stack` stretches has no room to move sideways; give it `no-extend`.
|
|
308
|
+
|
|
217
309
|
### Dataflows
|
|
218
310
|
|
|
219
311
|
`dataflow "id" { hop "..."; hop "..."; ...; color "#hex"; label "..." }`
|
|
@@ -269,7 +361,12 @@ isn't affected.
|
|
|
269
361
|
### Legend
|
|
270
362
|
|
|
271
363
|
A legend is added automatically, listing only the shapes, relations,
|
|
272
|
-
boundary styles
|
|
364
|
+
boundary styles, dataflows, ports, badges and defined line styles actually used.
|
|
365
|
+
A tint set on a leaf gets a row named after the leaves carrying it (the first
|
|
366
|
+
line of each label, without markup, joined with commas), or by the `legend`
|
|
367
|
+
text of one of them; a tint whose leaves all sit inside a container of the
|
|
368
|
+
same tint gets none, the container's row explains it. A tinted container's
|
|
369
|
+
row reads `Boundary <label>` (or `Group <label>`), unless it has a `legend` text. It's left out entirely for a
|
|
273
370
|
plain diagram of components and dependency edges.
|
|
274
371
|
|
|
275
372
|
It's laid out like the rest of the drawing: a `layer` of `stack`s (its
|
|
@@ -24,15 +24,25 @@ module Archsight
|
|
|
24
24
|
# container. Keep these tables in step with the readers in
|
|
25
25
|
# `Graph::Node`/`Graph::Edge`/`Graph::DataFlow` and with `docs/diagram.md`.
|
|
26
26
|
module Attributes
|
|
27
|
-
NODE = %w[label tint link resource extend].freeze
|
|
27
|
+
NODE = %w[label tint link resource extend badge align legend].freeze
|
|
28
28
|
LEAF_KINDS = %i[component application api database queue actor file text].freeze
|
|
29
|
-
|
|
29
|
+
# `anchor` places a leaf that keeps its own size (`extend "false"`) inside a wider stack.
|
|
30
|
+
LEAF = (NODE + %w[shape anchor]).freeze
|
|
30
31
|
# A `text` has no box to restyle (no `shape`), but may wrap at `width` characters.
|
|
31
32
|
TEXT = (NODE + %w[width]).freeze
|
|
32
33
|
WIDTH_FORMAT = /\A[1-9]\d*\z/
|
|
33
|
-
CONTAINER = (NODE + %w[gap ranks columns anchor]).freeze
|
|
34
|
-
EDGE = %w[label style relation tint].freeze
|
|
34
|
+
CONTAINER = (NODE + %w[gap padding ranks columns anchor]).freeze
|
|
35
|
+
EDGE = %w[label style relation tint line].freeze
|
|
35
36
|
DATAFLOW = %w[label color].freeze
|
|
37
|
+
PORT = %w[label side at badge].freeze
|
|
38
|
+
BADGE_DEFINITION = %w[label tint].freeze
|
|
39
|
+
LINE_DEFINITION = %w[label].freeze
|
|
40
|
+
|
|
41
|
+
LINE_STYLES = %w[solid dashed dotted].freeze
|
|
42
|
+
PORT_SIDES = %w[top bottom left right].freeze
|
|
43
|
+
# A named spot on the port's side, or a percentage of its length (from the left or top).
|
|
44
|
+
PORT_AT = /\A(left|center|right|top|bottom|(100|[1-9]?\d)(\.\d+)?%)\z/
|
|
45
|
+
PORT_SHAPES = %w[rectangle file module].freeze
|
|
36
46
|
|
|
37
47
|
EDGE_STYLES = %w[straight orthogonal].freeze
|
|
38
48
|
LEAF_EXTEND = %w[true false].freeze
|
|
@@ -73,10 +83,53 @@ module Archsight
|
|
|
73
83
|
when "style" then check_value!(key, value, EDGE_STYLES, where, ast_edge.line)
|
|
74
84
|
when "relation" then check_value!(key, value, Relations.names, where, ast_edge.line)
|
|
75
85
|
when "tint" then check_value!(key, value, Tints.names, where, ast_edge.line)
|
|
86
|
+
when "line" then check_value!(key, value, LINE_STYLES, where, ast_edge.line)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# A port's attrs, and that its leaf has a box shape to put a tab on.
|
|
92
|
+
def check_port!(ast_port, node)
|
|
93
|
+
where = "port #{ast_port.id.inspect} of #{node.kind} #{node.id.inspect}"
|
|
94
|
+
unless node.leaf? && node.kind != :text && PORT_SHAPES.include?(node.shape)
|
|
95
|
+
raise GraphError, "#{where} (line #{ast_port.line}): ports need a box-shaped leaf (shape #{PORT_SHAPES.join(", ")}), " \
|
|
96
|
+
"not #{node.kind == :text ? "a text" : "shape #{node.shape.inspect}"}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
ast_port.attrs.each do |key, value|
|
|
100
|
+
check_name!(key, PORT, where, ast_port.line)
|
|
101
|
+
case key
|
|
102
|
+
when "label" then check_label!(value, where, ast_port.line)
|
|
103
|
+
when "side" then check_value!(key, value, PORT_SIDES, where, ast_port.line)
|
|
104
|
+
when "at" then check_port_at!(value, ast_port.attrs["side"], where, ast_port.line)
|
|
76
105
|
end
|
|
77
106
|
end
|
|
78
107
|
end
|
|
79
108
|
|
|
109
|
+
def check_port_at!(value, side, where, line)
|
|
110
|
+
check_format!("at", value, PORT_AT, "left, center, right, top, bottom or a percentage (25%)", where, line)
|
|
111
|
+
along = side.nil? || %w[top bottom].include?(side) ? %w[left right] : %w[top bottom]
|
|
112
|
+
return unless (%w[left right top bottom] - along).include?(value)
|
|
113
|
+
|
|
114
|
+
raise GraphError, "at #{value.inspect} on #{where} (line #{line}) does not lie along its #{side || "top"} side; expected #{along.join(", ")}, center or a percentage"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# A top-level `badge`/`line` definition.
|
|
118
|
+
def check_definition!(definition)
|
|
119
|
+
where = "#{definition.kind} #{definition.id.inspect}"
|
|
120
|
+
allowed = definition.kind == :badge ? BADGE_DEFINITION : LINE_DEFINITION
|
|
121
|
+
if definition.kind == :line && !(LINE_STYLES - ["solid"]).include?(definition.id)
|
|
122
|
+
raise GraphError, "unknown line style #{definition.id.inspect} (line #{definition.line}); expected dashed or dotted" \
|
|
123
|
+
"#{suggestion(definition.id, LINE_STYLES - ["solid"])}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
definition.attrs.each do |key, value|
|
|
127
|
+
check_name!(key, allowed, where, definition.line)
|
|
128
|
+
check_label!(value, where, definition.line) if key == "label"
|
|
129
|
+
check_value!(key, value, Tints.names, where, definition.line) if key == "tint"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
80
133
|
# A dataflow's own attrs and each of its branches'.
|
|
81
134
|
def check_dataflow!(ast_dataflow)
|
|
82
135
|
where = "dataflow #{ast_dataflow.id.inspect}"
|
|
@@ -119,13 +172,13 @@ module Archsight
|
|
|
119
172
|
def check_node_value!(key, value, block, where, ranks_modes)
|
|
120
173
|
line = block.line
|
|
121
174
|
case key
|
|
122
|
-
when "label" then check_label!(value, where, line)
|
|
175
|
+
when "label", "legend" then check_label!(value, where, line)
|
|
123
176
|
when "shape" then check_value!(key, value, Representers.names, where, line)
|
|
124
177
|
when "tint" then check_value!(key, value, Tints.names, where, line)
|
|
125
178
|
when "ranks" then check_value!(key, value, ranks_modes, where, line)
|
|
126
179
|
when "extend" then check_value!(key, value, leaf?(block) ? LEAF_EXTEND : CONTAINER_EXTEND, where, line)
|
|
127
180
|
when "anchor" then check_value!(key, value, ANCHORS, where, line)
|
|
128
|
-
when "gap" then check_format!(key, value, GAP_FORMAT, "a pixel count (40), a percentage (150%) or a signed one (+20%, -50%)", where, line)
|
|
181
|
+
when "gap", "padding" then check_format!(key, value, GAP_FORMAT, "a pixel count (40), a percentage (150%) or a signed one (+20%, -50%)", where, line)
|
|
129
182
|
when "width" then check_format!(key, value, WIDTH_FORMAT, "a positive number of characters per line", where, line)
|
|
130
183
|
when "link" then check_link!(value, where, line)
|
|
131
184
|
when "resource" then check_resource!(value, block, where, line)
|
|
@@ -10,6 +10,9 @@ module Archsight
|
|
|
10
10
|
raise GraphError, "#{block.kind} has an empty id (line #{block.line})" if block.id.strip.empty?
|
|
11
11
|
|
|
12
12
|
Attributes.check_node!(block, ranks_modes: RANKS_MODES)
|
|
13
|
+
if Attributes.leaf?(block) && block.attrs.key?("anchor") && block.attrs["extend"] != "false"
|
|
14
|
+
raise GraphError, "anchor on #{block.kind} #{block.id.inspect} (line #{block.line}) only places a leaf that keeps its own size: add no-extend"
|
|
15
|
+
end
|
|
13
16
|
raise GraphError, "extend \"width\" on #{block.kind} #{block.id.inspect} (line #{block.line}) only applies to a direct child of a layer" if block.attrs["extend"] == "width" && !parent&.layer?
|
|
14
17
|
|
|
15
18
|
node = Node.for(id: block.id, kind: block.kind, attrs: block.attrs, children: [], parent: parent,
|
|
@@ -22,12 +25,82 @@ module Archsight
|
|
|
22
25
|
node.children << build_node(child, node, raw_edges)
|
|
23
26
|
when AST::Edge
|
|
24
27
|
raw_edges << child
|
|
28
|
+
when AST::Port
|
|
29
|
+
add_port(node, child)
|
|
25
30
|
end
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
node
|
|
29
34
|
end
|
|
30
35
|
|
|
36
|
+
def add_port(node, ast_port)
|
|
37
|
+
Attributes.check_port!(ast_port, node)
|
|
38
|
+
if (first = node.port(ast_port.id))
|
|
39
|
+
raise GraphError, "duplicate port #{ast_port.id.inspect} on #{node.id.inspect} (line #{ast_port.line}, first defined at line #{first.line})"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
node.ports << Port.new(id: ast_port.id, node: node, attrs: ast_port.attrs, line: ast_port.line)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A top-level `badge "id" { ... }` / `line "dashed" { ... }`: each id once per kind.
|
|
46
|
+
def add_definition(ast_definition)
|
|
47
|
+
Attributes.check_definition!(ast_definition)
|
|
48
|
+
key = [ast_definition.kind, ast_definition.id]
|
|
49
|
+
if (first = @definitions[key])
|
|
50
|
+
return if first.attrs == ast_definition.attrs # the same definition, e.g. from two included diagrams
|
|
51
|
+
|
|
52
|
+
raise GraphError, "#{ast_definition.kind} #{ast_definition.id.inspect} already defined at line #{first.line} (line #{ast_definition.line})"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@definitions[key] = Definition.new(kind: ast_definition.kind, id: ast_definition.id, attrs: ast_definition.attrs, line: ast_definition.line)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# `badge "id"` on nodes and ports names a definition, `align "id"` another node.
|
|
59
|
+
def check_references!
|
|
60
|
+
@nodes_by_id.each_value do |node|
|
|
61
|
+
check_badge!(node.badge, "#{node.kind} #{node.id.inspect}", node.line) if node.badge
|
|
62
|
+
node.ports.each { |port| check_badge!(port.badge, "port #{port.ref.inspect}", port.line) if port.badge }
|
|
63
|
+
check_align!(node) if node.align
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def check_badge!(id, where, line)
|
|
68
|
+
return if @definitions.key?([:badge, id])
|
|
69
|
+
|
|
70
|
+
known = @definitions.values.select { |d| d.kind == :badge }.map(&:id)
|
|
71
|
+
hint = known.empty? ? " (define it at the top level: badge #{id.inspect} { tint \"orange\"; label \"...\" })" : Attributes.suggestion(id, known)
|
|
72
|
+
raise GraphError, "unknown badge #{id.inspect} on #{where} (line #{line})#{hint}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def check_align!(node)
|
|
76
|
+
where = "#{node.kind} #{node.id.inspect} (line #{node.line})"
|
|
77
|
+
entries = node.alignments
|
|
78
|
+
raise GraphError, "align #{node.align.inspect} on #{where} has an empty entry" if entries.any? { |_, _, id| id.empty? }
|
|
79
|
+
|
|
80
|
+
entries.group_by(&:first).each_value do |same_axis|
|
|
81
|
+
next if same_axis.length == 1
|
|
82
|
+
|
|
83
|
+
raise GraphError, "align #{node.align.inspect} on #{where} lines up #{same_axis.map { |_, e, _| e }.join(" and ")} on one axis; " \
|
|
84
|
+
"use one of left/center/right and one of top/middle/bottom"
|
|
85
|
+
end
|
|
86
|
+
entries.each { |_, _, id| check_align_target!(node, id, where) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def check_align_target!(node, id, where)
|
|
90
|
+
target = @nodes_by_id[id]
|
|
91
|
+
unless target
|
|
92
|
+
word, rest = id.split(/\s+/, 2)
|
|
93
|
+
if rest
|
|
94
|
+
raise GraphError, "unknown edge #{word.inspect} in align #{node.align.inspect} on #{where}; expected one of " \
|
|
95
|
+
"#{Node::ALIGN_EDGES.keys.join(", ")}#{Attributes.suggestion(word, Node::ALIGN_EDGES.keys)}"
|
|
96
|
+
end
|
|
97
|
+
raise GraphError, "align #{node.align.inspect} on #{where} names no node #{id.inspect}#{Attributes.suggestion(id, @nodes_by_id.keys)}"
|
|
98
|
+
end
|
|
99
|
+
return unless target.ancestors.include?(node) || node.ancestors.include?(target)
|
|
100
|
+
|
|
101
|
+
raise GraphError, "align #{node.align.inspect} on #{where} names #{target == node ? "the node itself" : "a node it is nested with"}"
|
|
102
|
+
end
|
|
103
|
+
|
|
31
104
|
# A top-level `theme "..."`/`legend "..."` statement: each may appear
|
|
32
105
|
# once, with one of its own allowed values.
|
|
33
106
|
def apply_setting(setting)
|
|
@@ -49,13 +122,38 @@ module Archsight
|
|
|
49
122
|
end
|
|
50
123
|
|
|
51
124
|
def resolve_edge(ast_edge)
|
|
52
|
-
from =
|
|
53
|
-
to =
|
|
54
|
-
|
|
125
|
+
from, from_port = endpoint(ast_edge.from)
|
|
126
|
+
to, to_port = endpoint(ast_edge.to)
|
|
127
|
+
raise GraphError, "connects #{from.id.inspect} to itself, which has no line to draw" if from == to
|
|
128
|
+
|
|
129
|
+
Edge.new(from: from, to: to, attrs: ast_edge.attrs, direction: ast_edge.direction, line: ast_edge.line,
|
|
130
|
+
from_port: from_port, to_port: to_port)
|
|
55
131
|
rescue GraphError => e
|
|
56
132
|
raise GraphError, "#{e.message} (edge at line #{ast_edge.line})"
|
|
57
133
|
end
|
|
58
134
|
|
|
135
|
+
# An edge end: a node id, or `node.port` when no node has that whole id (namespaced ids contain dots too).
|
|
136
|
+
def endpoint(ref)
|
|
137
|
+
return [@nodes_by_id[ref], nil] if @nodes_by_id.key?(ref)
|
|
138
|
+
|
|
139
|
+
owner = port_owner(ref)
|
|
140
|
+
return [node(ref), nil] unless owner
|
|
141
|
+
|
|
142
|
+
port_id = ref.delete_prefix("#{owner.id}.")
|
|
143
|
+
port = owner.port(port_id)
|
|
144
|
+
return [owner, port] if port
|
|
145
|
+
|
|
146
|
+
known = owner.ports.map(&:id)
|
|
147
|
+
listed = known.empty? ? "it has no ports" : "its ports: #{known.join(", ")}"
|
|
148
|
+
raise GraphError, "unknown port #{port_id.inspect} on #{owner.id.inspect} (#{listed})#{Attributes.suggestion(port_id, known)}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The node `node.port` names, `nil` if the part before the last dot is no node
|
|
152
|
+
def port_owner(ref)
|
|
153
|
+
owner_id, dot, = ref.rpartition(".")
|
|
154
|
+
dot.empty? ? nil : @nodes_by_id[owner_id]
|
|
155
|
+
end
|
|
156
|
+
|
|
59
157
|
def resolve_dataflow(ast_dataflow)
|
|
60
158
|
group_index = ast_dataflow.hops.index { |h| h.is_a?(AST::DataFlowGroup) }
|
|
61
159
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module Archsight
|
|
4
4
|
module Diagram
|
|
5
5
|
class Graph
|
|
6
|
-
|
|
6
|
+
# `from_port`/`to_port` are the `Graph::Port` an end attaches to (`a -> b.port`), `nil` for the node itself.
|
|
7
|
+
Edge = Struct.new(:from, :to, :attrs, :direction, :line, :from_port, :to_port, keyword_init: true) do
|
|
7
8
|
def label
|
|
8
9
|
attrs["label"]
|
|
9
10
|
end
|
|
@@ -16,6 +17,11 @@ module Archsight
|
|
|
16
17
|
attrs.fetch("relation", "dependency")
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
# `solid` (the relation's own stroke), `dashed` or `dotted`.
|
|
21
|
+
def line_style
|
|
22
|
+
attrs.fetch("line", "solid")
|
|
23
|
+
end
|
|
24
|
+
|
|
19
25
|
# The explicit `tint` attr, if any -- `nil` when unset, meaning
|
|
20
26
|
# "use this relation's own fixed color" rather than falling back to
|
|
21
27
|
# some default tint the way `Node#effective_tint` does (an edge
|
|
@@ -165,6 +165,30 @@ module Archsight
|
|
|
165
165
|
attrs["link"]
|
|
166
166
|
end
|
|
167
167
|
|
|
168
|
+
# The leaf's `port "id" { ... }` tabs, in declaration order (see `Graph::Port`).
|
|
169
|
+
def ports = (@ports ||= [])
|
|
170
|
+
def port(id) = ports.find { |p| p.id == id }
|
|
171
|
+
|
|
172
|
+
# The id of the top-level `badge` definition this node shows, if any.
|
|
173
|
+
def badge = attrs["badge"]
|
|
174
|
+
|
|
175
|
+
# The explicit text of this node's legend row (`legend "..."`), if any.
|
|
176
|
+
def legend_label = attrs["legend"]
|
|
177
|
+
|
|
178
|
+
# The raw `align` attr (see `Layout::Alignment`), if any.
|
|
179
|
+
def align = attrs["align"]
|
|
180
|
+
|
|
181
|
+
# `align "top a, left b"` as `[[axis, edge, id], ...]`; a bare id means `center`. The first word is an
|
|
182
|
+
# edge only when an id follows it, so a node named "top" can still be aligned to.
|
|
183
|
+
def alignments
|
|
184
|
+
return [] unless align
|
|
185
|
+
|
|
186
|
+
align.split(",").map(&:strip).map do |entry|
|
|
187
|
+
word, rest = entry.split(/\s+/, 2)
|
|
188
|
+
rest && Node::ALIGN_EDGES.key?(word) ? [Node::ALIGN_EDGES[word], word, rest.strip] : [:x, "center", entry]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
168
192
|
# Why this node's `resource` reference couldn't be resolved (see
|
|
169
193
|
# `ResourceLinks`), or `nil` when it has none or it resolved.
|
|
170
194
|
def broken_link
|
|
@@ -174,7 +198,8 @@ module Archsight
|
|
|
174
198
|
# The padding and title band `Layout` reserves inside a container's
|
|
175
199
|
# box -- none for an anonymous `layer`/`stack`, which never draws a
|
|
176
200
|
# box of its own; `Legend::Frame` has its own fixed ones.
|
|
177
|
-
|
|
201
|
+
# `padding` (same syntax as `gap`) overrides the theme's on a named container.
|
|
202
|
+
def layout_padding(theme) = anonymous? ? 0.0 : GapSpec.parse(attrs["padding"]).apply(theme.padding)
|
|
178
203
|
def layout_title_height(theme) = anonymous? ? 0.0 : theme.title_height
|
|
179
204
|
|
|
180
205
|
# See `Legend::Entry`, sized by `Layout` from its row instead of from
|
|
@@ -267,6 +292,9 @@ module Archsight
|
|
|
267
292
|
def main_axis = Axis::HEIGHT
|
|
268
293
|
end
|
|
269
294
|
|
|
295
|
+
# Which edge of another node an `align` entry lines a node up with, and on which axis.
|
|
296
|
+
Node::ALIGN_EDGES = { "left" => :x, "center" => :x, "right" => :x, "top" => :y, "middle" => :y, "bottom" => :y }.freeze
|
|
297
|
+
|
|
270
298
|
Node::KIND_CLASSES = {
|
|
271
299
|
component: Component, application: Application, api: Api, database: Database,
|
|
272
300
|
queue: Queue, actor: Actor, file: FileNode, text: TextNode, group: Group, boundary: Boundary, layer: Layer, stack: Stack
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
module Diagram
|
|
5
|
+
class Graph
|
|
6
|
+
# A labelled tab on a leaf's border (`port "id" { ... }`) that edges attach to with
|
|
7
|
+
# `leaf.id`. `side`/`at` are the explicit placement, `nil` when left to `PortPlacement`.
|
|
8
|
+
Port = Struct.new(:id, :node, :attrs, :line, keyword_init: true) do
|
|
9
|
+
def label = attrs["label"] || id
|
|
10
|
+
def side = attrs["side"]
|
|
11
|
+
def at = attrs["at"]
|
|
12
|
+
def badge = attrs["badge"]
|
|
13
|
+
|
|
14
|
+
# `node.port`, as an edge names it
|
|
15
|
+
def ref = "#{node.id}.#{id}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# A top-level `badge "id" { tint; label }` or `line "style" { label }` (see `AST::Definition`).
|
|
19
|
+
Definition = Struct.new(:kind, :id, :attrs, :line, keyword_init: true) do
|
|
20
|
+
def label = attrs["label"] || id
|
|
21
|
+
def tint = attrs.fetch("tint", "gray")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative "style/relations"
|
|
|
8
8
|
require_relative "style/theme"
|
|
9
9
|
require_relative "legend/modes"
|
|
10
10
|
require_relative "graph/node"
|
|
11
|
+
require_relative "graph/port"
|
|
11
12
|
require_relative "graph/edge"
|
|
12
13
|
require_relative "graph/dataflow"
|
|
13
14
|
require_relative "graph/attributes"
|
|
@@ -30,7 +31,7 @@ module Archsight
|
|
|
30
31
|
SETTING_VALUES = { "theme" => -> { Theme.names }, "legend" => -> { Legend::MODES }, "ranks" => -> { RANKS_MODES } }.freeze
|
|
31
32
|
SETTING_IVARS = { "theme" => :@theme_name, "legend" => :@legend_mode, "ranks" => :@ranks_mode }.freeze
|
|
32
33
|
|
|
33
|
-
attr_reader :roots, :nodes_by_id, :edges, :dataflows, :theme_name, :legend_mode, :ranks_mode
|
|
34
|
+
attr_reader :roots, :nodes_by_id, :edges, :dataflows, :theme_name, :legend_mode, :ranks_mode, :definitions
|
|
34
35
|
|
|
35
36
|
def self.build(statements)
|
|
36
37
|
new(statements)
|
|
@@ -38,6 +39,7 @@ module Archsight
|
|
|
38
39
|
|
|
39
40
|
def initialize(statements)
|
|
40
41
|
@nodes_by_id = {}
|
|
42
|
+
@definitions = {}
|
|
41
43
|
@roots = []
|
|
42
44
|
raw_edges = []
|
|
43
45
|
raw_dataflows = []
|
|
@@ -52,8 +54,11 @@ module Archsight
|
|
|
52
54
|
raw_dataflows << stmt
|
|
53
55
|
when AST::Setting
|
|
54
56
|
apply_setting(stmt)
|
|
57
|
+
when AST::Definition
|
|
58
|
+
add_definition(stmt)
|
|
55
59
|
end
|
|
56
60
|
end
|
|
61
|
+
check_references!
|
|
57
62
|
|
|
58
63
|
raw_edges.each { |e| Attributes.check_edge!(e) }
|
|
59
64
|
raw_dataflows.each_with_object({}) do |df, seen|
|
|
@@ -70,9 +75,21 @@ module Archsight
|
|
|
70
75
|
|
|
71
76
|
def node(id)
|
|
72
77
|
@nodes_by_id.fetch(id) do
|
|
78
|
+
owner = port_owner(id)
|
|
79
|
+
raise GraphError, "#{id.inspect} is a port of #{owner.id.inspect}; only edges attach to ports" if owner&.port(id.delete_prefix("#{owner.id}."))
|
|
80
|
+
|
|
73
81
|
raise GraphError, "unknown id #{id.inspect}"
|
|
74
82
|
end
|
|
75
83
|
end
|
|
84
|
+
|
|
85
|
+
# The `badge "id"` definition, `nil` if there is none.
|
|
86
|
+
def badge_definition(id) = @definitions[[:badge, id]]
|
|
87
|
+
|
|
88
|
+
# The legend text a top-level `line "dashed" { label }` gives that line style, `nil` without one.
|
|
89
|
+
def line_definition(style) = @definitions[[:line, style]]
|
|
90
|
+
|
|
91
|
+
# Every port of every leaf, in declaration order.
|
|
92
|
+
def ports = @nodes_by_id.values.flat_map(&:ports)
|
|
76
93
|
end
|
|
77
94
|
end
|
|
78
95
|
end
|
|
@@ -93,11 +93,16 @@ module Archsight
|
|
|
93
93
|
raise IncludeError, "in include #{path.inspect}: #{e.message}"
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
# Dataflows and
|
|
96
|
+
# Dataflows, settings and badge/line definitions apply to the whole diagram, so inside a container they are
|
|
97
|
+
# ignored (with a warning)
|
|
97
98
|
def drop_whole_diagram_statements(path, parsed, line)
|
|
98
|
-
dropped, kept = parsed.partition { |s| s.is_a?(AST::DataFlow) || s.is_a?(AST::Setting) }
|
|
99
|
+
dropped, kept = parsed.partition { |s| s.is_a?(AST::DataFlow) || s.is_a?(AST::Setting) || s.is_a?(AST::Definition) }
|
|
99
100
|
dropped.each do |s|
|
|
100
|
-
what =
|
|
101
|
+
what = case s
|
|
102
|
+
when AST::DataFlow then "dataflow #{s.id.inspect}"
|
|
103
|
+
when AST::Definition then "#{s.kind} #{s.id.inspect}"
|
|
104
|
+
else "#{s.key} setting"
|
|
105
|
+
end
|
|
101
106
|
@warnings << "include #{path.inspect} (line #{line}): the #{what} (line #{s.line}) is ignored, it only applies when included at the top level"
|
|
102
107
|
end
|
|
103
108
|
kept
|
|
@@ -145,9 +150,18 @@ module Archsight
|
|
|
145
150
|
block.attrs["label"] ||= block.id # the label defaults to the id
|
|
146
151
|
block.id = ref(block.id)
|
|
147
152
|
end
|
|
153
|
+
block.attrs["align"] = rename_align(block.attrs["align"]) if block.attrs.key?("align")
|
|
148
154
|
block.children.each { |child| rename(child) }
|
|
149
155
|
end
|
|
150
156
|
|
|
157
|
+
# `top a, b` -> `top ns.a, ns.b`: the id of each entry, after its edge word if it has one
|
|
158
|
+
def rename_align(value)
|
|
159
|
+
value.split(",").map(&:strip).map do |entry|
|
|
160
|
+
word, rest = entry.split(/\s+/, 2)
|
|
161
|
+
rest && Graph::Node::ALIGN_EDGES.key?(word) ? "#{word} #{ref(rest.strip)}" : ref(entry)
|
|
162
|
+
end.join(", ")
|
|
163
|
+
end
|
|
164
|
+
|
|
151
165
|
def rename_dataflow(flow)
|
|
152
166
|
flow.attrs["label"] ||= flow.id
|
|
153
167
|
flow.id = ref(flow.id)
|
|
@@ -159,8 +173,12 @@ module Archsight
|
|
|
159
173
|
group
|
|
160
174
|
end
|
|
161
175
|
|
|
176
|
+
# A defined id, or `node.port` of a defined node
|
|
162
177
|
def ref(id)
|
|
163
|
-
|
|
178
|
+
return "#{@prefix}#{id}" if @defined[id]
|
|
179
|
+
|
|
180
|
+
owner, dot, = id.rpartition(".")
|
|
181
|
+
!dot.empty? && @defined[owner] ? "#{@prefix}#{id}" : id
|
|
164
182
|
end
|
|
165
183
|
end
|
|
166
184
|
end
|