archsight 0.4.1 → 0.4.3
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 +64 -0
- data/docs/modeling.md +33 -10
- data/lib/archsight/cli.rb +2 -1
- data/lib/archsight/database.rb +4 -3
- data/lib/archsight/diagram/cli.rb +11 -1
- data/lib/archsight/diagram/errors.rb +1 -0
- data/lib/archsight/diagram/includes.rb +168 -0
- data/lib/archsight/diagram/parser/ast.rb +5 -0
- data/lib/archsight/diagram/parser/table_layout.rb +2 -0
- data/lib/archsight/diagram/parser.rb +58 -16
- data/lib/archsight/diagram.rb +20 -4
- data/lib/archsight/export/confluence/storage.rb +6 -1
- data/lib/archsight/helpers/diagram_blocks.rb +34 -19
- data/lib/archsight/helpers/diagram_includes.rb +67 -0
- data/lib/archsight/helpers.rb +1 -0
- data/lib/archsight/linter/structure.rb +4 -1
- data/lib/archsight/linter.rb +10 -2
- data/lib/archsight/references.rb +4 -3
- data/lib/archsight/resources/application_component.rb +18 -0
- data/lib/archsight/resources/application_service.rb +9 -0
- data/lib/archsight/resources/technology_device.rb +56 -4
- data/lib/archsight/resources/technology_service.rb +16 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/json_helpers.rb +3 -2
- data/lib/archsight/web/api/openapi/spec.yaml +9 -2
- data/lib/archsight/web/api/page_helpers.rb +1 -1
- data/lib/archsight/web/api/routes.rb +4 -3
- data/lib/archsight/web/application.rb +2 -1
- data/lib/archsight/web/public/vue/{DocPage-C2ubTXH7.js → DocPage-L0GzDFiV.js} +1 -1
- data/lib/archsight/web/public/vue/{EditorPage-Dso6PLMn.js → EditorPage-B3iNbh1u.js} +1 -1
- data/lib/archsight/web/public/vue/{ErrorPage-DnZ6YJLi.js → ErrorPage-Lro5vwhy.js} +1 -1
- data/lib/archsight/web/public/vue/{GraphView-CTl03Yre.js → GraphView-BRKwxa--.js} +1 -1
- data/lib/archsight/web/public/vue/{HomePage-C339nihQ.js → HomePage-DKgI1DXr.js} +2 -2
- data/lib/archsight/web/public/vue/{InstanceRouter-ZaCcjlIX.css → InstanceRouter-CJSkGdBG.css} +1 -1
- data/lib/archsight/web/public/vue/InstanceRouter-CX6qxVTm.js +1 -0
- data/lib/archsight/web/public/vue/{KindList-C0oaM3CF.js → KindList-C556gf23.js} +1 -1
- data/lib/archsight/web/public/vue/{PageView-OZB6G9Wu.js → PageView-DAqHjL1x.js} +1 -1
- data/lib/archsight/web/public/vue/{QueryError-C2xLkCEm.js → QueryError-DvspMrsh.js} +1 -1
- data/lib/archsight/web/public/vue/{ResourceList-DooWFwQP.js → ResourceList-BZOYa68X.js} +1 -1
- data/lib/archsight/web/public/vue/{SearchResults-BK-HjFnv.js → SearchResults-DaaOPG3t.js} +1 -1
- data/lib/archsight/web/public/vue/{WikiPage-B2FYmeHL.js → WikiPage-Bfx92Iig.js} +1 -1
- data/lib/archsight/web/public/vue/index-B_YGrWD8.css +1 -0
- data/lib/archsight/web/public/vue/{index-CLuLTE5I.js → index-DMhBow5t.js} +2 -2
- data/lib/archsight/web/public/vue.html +2 -2
- metadata +18 -16
- data/lib/archsight/web/public/vue/InstanceRouter-DFW7tOrD.js +0 -1
- data/lib/archsight/web/public/vue/index-BLSE5DSv.css +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66e3a2aa9223a9583edf06749e12bafe346bfd562570d299795f2334510ed36a
|
|
4
|
+
data.tar.gz: 52a7997e394247a86da162fb4015d97b755b2ac2fe69cfadd758f6d3e140c33b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: adc6fb4fad180ae14920ecfbccebfb242c9b3d48bc6bfb196b645de67f4dc490e4922274175fa9f942cfe4d80a327b8a4cf171ad9ccc1edd195c5d283c636bba
|
|
7
|
+
data.tar.gz: e82af637b926e46db4b61c23269d8f5b630222b78f6e169d413b21aa2f12858cbc80cacbc8325ab1d550174e11fedc8ca1780489d025c3dabe5c1943d1fd887b
|
data/docs/diagram.md
CHANGED
|
@@ -43,6 +43,7 @@ archsight diagram three_tier.asd --relation=all # also show control/data-flow
|
|
|
43
43
|
archsight diagram overview.asd --theme=compact # denser spacing + smaller fonts (or cozy, tight)
|
|
44
44
|
archsight diagram overview.asd --legend=bottom # legend below (or right/left/top/none; default auto)
|
|
45
45
|
archsight diagram *.asd # render every file to its own .svg
|
|
46
|
+
archsight diagram app.asd --resources=model/ # resolve include \"file://...\" paths from this directory
|
|
46
47
|
archsight diagram --format *.asd # re-indent the sources in place
|
|
47
48
|
archsight diagram --format --check *.asd # list unformatted sources, exit 1 if any (CI)
|
|
48
49
|
```
|
|
@@ -298,6 +299,69 @@ no broken styling. Library users pass a `resolver:` to `Diagram.render`
|
|
|
298
299
|
(any `call(reference)` returning a URL, `nil` for a resource that exists but has nowhere to link to (the node stays as it is), `:missing` or `:ambiguous`, see
|
|
299
300
|
`Archsight::Helpers::ResourceResolver`) and, to collect failures, `unresolved: []`.
|
|
300
301
|
|
|
302
|
+
## Including diagrams
|
|
303
|
+
|
|
304
|
+
`include "<path>"` splices another diagram in at that position, so a part
|
|
305
|
+
that several diagrams share (a platform, a network, a team's services) is
|
|
306
|
+
written once:
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
group "app" {
|
|
310
|
+
component "web" { label "Web server" }
|
|
311
|
+
stack {
|
|
312
|
+
include "archsight://ApplicationComponent/Platform:Database"
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
include "file://shared/network.asd"
|
|
316
|
+
web -> db
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The options of an `include` go in braces after the path (`namespace`, see below).
|
|
320
|
+
|
|
321
|
+
| Path | Included diagram |
|
|
322
|
+
|------|------------------|
|
|
323
|
+
| `archsight://<Kind>/<name>` | the `architecture/diagram` annotation of that resource |
|
|
324
|
+
| `file://<path>` | an `.asd` file, relative to the resources directory (also in nested includes) |
|
|
325
|
+
|
|
326
|
+
A path that leaves the resources directory (`..`, absolute paths, symlinks
|
|
327
|
+
pointing out) or is not an `.asd` file is refused. An `include` stands
|
|
328
|
+
wherever a `layer` or `stack` can: at the top level or inside a container
|
|
329
|
+
(not inside a leaf like `component`, and not together with `columns`). Included
|
|
330
|
+
diagrams may include others (up to 8 levels, 100 includes in all; a cycle is an
|
|
331
|
+
error). Nodes are one namespace across all of it, so an id defined twice is an
|
|
332
|
+
error, and an edge in either part may name a node of the other.
|
|
333
|
+
|
|
334
|
+
### Including a diagram more than once
|
|
335
|
+
|
|
336
|
+
Ids are one namespace, so including the same diagram twice would define
|
|
337
|
+
every node twice. A `namespace` puts `<namespace>.` in front of every id the
|
|
338
|
+
included diagram defines (nodes and dataflows, and the references to them inside
|
|
339
|
+
it):
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
include "archsight://ApplicationComponent/Platform:Server" { namespace "a" }
|
|
343
|
+
include "archsight://ApplicationComponent/Platform:Server" { namespace "b" }
|
|
344
|
+
a.web -> b.db
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
A node without a `label` keeps showing its own id (`web`, not `a.web`), and the same
|
|
348
|
+
id from different includes no longer clashes. A reference inside the included diagram to an id it
|
|
349
|
+
does not define names a node of the including diagram and stays as it is. Namespaces
|
|
350
|
+
nest: a diagram included with `namespace "o"` that itself includes one with `namespace "in"` has
|
|
351
|
+
`o.in.…` ids. A namespace is a name of letters, digits, `_` and `-`, joined by dots.
|
|
352
|
+
|
|
353
|
+
Dataflows and `theme`/`legend`/`ranks` apply to the whole diagram: an included
|
|
354
|
+
diagram holding them keeps them when it is included at the top level, and they are
|
|
355
|
+
ignored, with a warning, when it is included inside a container. The warning is
|
|
356
|
+
written to the server log and listed by `archsight lint`.
|
|
357
|
+
|
|
358
|
+
A missing resource or file, a bad path or a broken included diagram fails the
|
|
359
|
+
diagram with an error that names the include (`archsight lint` reports it). Standalone
|
|
360
|
+
`archsight diagram` takes `file://` paths relative to `--resources DIR` (default:
|
|
361
|
+
the directory of the input) and has no `archsight://`. Library users pass `includes:` to
|
|
362
|
+
`Diagram.render`, any `call(path)` returning the source or `:missing`, `:forbidden` or `:invalid`
|
|
363
|
+
(see `Archsight::Helpers::DiagramIncludes`), and `warnings: []` to collect the warnings.
|
|
364
|
+
|
|
301
365
|
## Diagrams in markdown
|
|
302
366
|
|
|
303
367
|
A fenced code block tagged `asd` in any markdown the web UI renders (docs,
|
data/docs/modeling.md
CHANGED
|
@@ -36,7 +36,7 @@ There are two families of verbs:
|
|
|
36
36
|
|
|
37
37
|
| Family | Verbs | Written on | Points to |
|
|
38
38
|
|--------|-------|------------|-----------|
|
|
39
|
-
| Responsibility and provider (down) | `performedBy`, `ownedBy`, `executedBy`, `servedBy`, `guidedBy`, `maintainedBy`, `contributedBy`, `hasConcern`, `representedBy`, `realizedThrough`, `exposes`, `connects`, `mitigatedBy`, `influences`, `triggers`, `affects`, `assesses`, `contains`, `closedBy`, `compares` | the element that needs, uses or is concerned with something | what provides it or is responsible for it |
|
|
39
|
+
| Responsibility and provider (down) | `performedBy`, `ownedBy`, `operatedBy`, `executedBy`, `servedBy`, `guidedBy`, `maintainedBy`, `contributedBy`, `hasConcern`, `representedBy`, `realizedThrough`, `exposes`, `provisions`, `connects`, `mitigatedBy`, `influences`, `triggers`, `affects`, `assesses`, `contains`, `closedBy`, `compares` | the element that needs, uses or is concerned with something | what provides it or is responsible for it |
|
|
40
40
|
| Realization (up) | `realizes`, `partiallyRealizes`, `plans`, `satisfies`, `evidencedBy` (on the evidenced element), `provides` | the concrete element | the abstract element it answers to (requirement, goal, capability, plateau) |
|
|
41
41
|
|
|
42
42
|
Rules:
|
|
@@ -154,7 +154,7 @@ TechnologySystemSoftware, TechnologyService, TechnologyArtifact). The same princ
|
|
|
154
154
|
| Location | In which region or country is it? | A place on a map, not a building; holds facilities | the place ("Location:Germany:Frankfurt") |
|
|
155
155
|
| TechnologyFacility | In which building or site does hardware stand? | Has an address and can be visited; can be nested (campus, hall) | the site ("Facility:Frankfurt:HallA") |
|
|
156
156
|
| TechnologyEquipment | Which physical thing can you touch? | Racks, fabrics, fibre routes, carrier circuits; no software of its own | the thing and its place ("Equipment:Link:Frankfurt-Madrid") |
|
|
157
|
-
| TechnologyDevice | Which single machine stands in the room? | One server, switch, router, firewall or appliance with a serial number
|
|
157
|
+
| TechnologyDevice | Which single machine stands in the room? | One server, switch, router, firewall or appliance with a serial number (it is cabled), or one laptop, workstation or phone of a person or team | the host ("Device:Frankfurt:Server01") |
|
|
158
158
|
| TechnologyCommunicationNetwork | What carries data between nodes? | Has a network role (`network/type`); exists beyond any single link | role and scope ("Network:Management:Frankfurt") |
|
|
159
159
|
| TechnologyNode | On which machine does software run? | A VM, host role or logical group of machines with a lifecycle of its own | the host or host role ("Node:Frankfurt:Web01") |
|
|
160
160
|
| TechnologySystemSoftware | What software hosts other software or data? | An operating system, database cluster, routing stack, platform | the product and role ("Tech:Routing:EVPN") |
|
|
@@ -166,8 +166,8 @@ TechnologySystemSoftware, TechnologyService, TechnologyArtifact). The same princ
|
|
|
166
166
|
1. Is it a country, metro or region? Location.
|
|
167
167
|
2. Is it a building, campus, hall or site that can be visited? TechnologyFacility.
|
|
168
168
|
3. Is it a physical thing (rack, switch fabric, fibre, circuit) without software of its own? TechnologyEquipment.
|
|
169
|
-
4. Is it one physical machine (server, switch, router, firewall, appliance) whose room
|
|
170
|
-
TechnologyDevice.
|
|
169
|
+
4. Is it one physical machine (server, switch, router, firewall, appliance, laptop, workstation, phone) whose room,
|
|
170
|
+
cabling or operating system matters? TechnologyDevice.
|
|
171
171
|
5. Does it carry data between nodes, with a role such as underlay, overlay, management, public or VPN?
|
|
172
172
|
TechnologyCommunicationNetwork.
|
|
173
173
|
6. Is it a machine (virtual, or a host role) that software runs on? TechnologyNode.
|
|
@@ -201,7 +201,8 @@ TechnologySystemSoftware, TechnologyService, TechnologyArtifact). The same princ
|
|
|
201
201
|
10. One `TechnologyDevice` per physical machine, `locatedAt` its room (a TechnologyFacility of `facility/type: room`
|
|
202
202
|
the data center is `composedOf`). A group of machines with one role stays a logical `TechnologyNode` that is
|
|
203
203
|
`realizedBy` its devices; a rack or hardware block is TechnologyEquipment `composedOf` them; a virtual chassis or
|
|
204
|
-
stack is a device `composedOf` its members.
|
|
204
|
+
stack is a device `composedOf` its members. A client device (`device/type` laptop, desktop, workstation,
|
|
205
|
+
thin-client, tablet, mobile) moves and needs no room; it may be `locatedAt` an office.
|
|
205
206
|
11. A network is `realizedBy` the devices that build it (switches, routers) and `connects` the devices attached to it.
|
|
206
207
|
Both are written on the network, so a switch attached to the network it builds closes no cycle.
|
|
207
208
|
12. Cables are `connectedTo` between devices, declared once on one of the two ends. Cabling runs both ways by nature
|
|
@@ -213,6 +214,17 @@ TechnologySystemSoftware, TechnologyService, TechnologyArtifact). The same princ
|
|
|
213
214
|
`servedBy` its team and `composedOf` its devices, which then need no team of their own. Both relations point
|
|
214
215
|
down to the team, which points to nothing further. On a TechnologyFacility, `servedBy` stays the operator of
|
|
215
216
|
the site.
|
|
217
|
+
14. A client device (developer workstation, laptop, phone) is a `TechnologyDevice` with a client `device/type` and
|
|
218
|
+
`device/os`, `device/osVersion`, `device/arch`, `device/ownership`, `device/managed` and `device/encryption`; the
|
|
219
|
+
vendor and product are in `device/model` (also for a hardened workstation that runs the user's operating system
|
|
220
|
+
virtualized beside other isolated sessions, which is a `workstation`). Model the individual machine when it is
|
|
221
|
+
inventoried, otherwise one `TechnologyNode` per client platform `realizedBy` the devices. The operating system is
|
|
222
|
+
`TechnologySystemSoftware` `realizedBy` the node, the image it is installed from an artifact the node is
|
|
223
|
+
`assignedTo`; each isolated session of a hardened workstation can be a node of its own. Network access is written
|
|
224
|
+
on the node (`servedBy` the networks it uses: the office, the VPN) or, for a single named machine, on the network
|
|
225
|
+
(`connects`), never on the device, because a network is `realizedBy` its switches and the reverse arrow would
|
|
226
|
+
close a cycle. A machine that reaches no network (air-gapped) has neither relation. Who uses it is the team or role
|
|
227
|
+
(`servedBy`), not a person; groups of machines are tags and the end of support is `lifecycle/endOfLifeAt`.
|
|
216
228
|
|
|
217
229
|
**Checked by `archsight lint`**
|
|
218
230
|
|
|
@@ -224,7 +236,7 @@ As above, these are **warnings** in the "Modeling warnings" section and never fa
|
|
|
224
236
|
| `TechnologyCommunicationNetwork 'Network:Overlay:EVPN' is an overlay network served by no network` | 3 |
|
|
225
237
|
| `TechnologyFacility 'Facility:Madrid:DC' is located at no Location` | where |
|
|
226
238
|
| `TechnologyEquipment 'Equipment:Rack01' is located at no TechnologyFacility` | where |
|
|
227
|
-
| `TechnologyDevice 'Device:Server01' is located at no TechnologyFacility` | 10 |
|
|
239
|
+
| `TechnologyDevice 'Device:Server01' is located at no TechnologyFacility` (not for client device types) | 10 |
|
|
228
240
|
| `TechnologyDevice 'Device:Switch01' is connectedTo itself` | 12 |
|
|
229
241
|
| `TechnologyDevice 'Device:Router01' is a router that realizes no TechnologyCommunicationNetwork` | 11 |
|
|
230
242
|
| `TechnologyDevice 'Device:a:server-3' is servedBy 'Kernel:Team', not by the team of its equipment ('Compute:Team')` | 13 |
|
|
@@ -547,7 +559,7 @@ Which kind to pick is in [Choosing the Kind: Infrastructure](#choosing-the-kind-
|
|
|
547
559
|
| `TechnologyCommunicationNetwork` | `realizedBy` | `TechnologyEquipment` (the fabric), `TechnologySystemSoftware` (routing stack, VPN software) |
|
|
548
560
|
| `TechnologyCommunicationNetwork` | `realizedBy` | `ApplicationComponent`, `ApplicationService` (software that builds the network: an SDN agent, a network operator) |
|
|
549
561
|
| `TechnologyCommunicationNetwork` | `realizedBy` | `TechnologyDevice` (the switches and routers that build it) |
|
|
550
|
-
| `TechnologyCommunicationNetwork` | `connects` | `TechnologyDevice` (the devices attached to it) |
|
|
562
|
+
| `TechnologyCommunicationNetwork` | `connects` | `TechnologyDevice` (the devices attached to it, a named workstation behind a VPN) |
|
|
551
563
|
| `TechnologyNode` | `servedBy` | `TechnologyCommunicationNetwork` |
|
|
552
564
|
| `ApplicationInterface` | `servedBy` | `TechnologyCommunicationNetwork` (the networks on which it can be reached) |
|
|
553
565
|
|
|
@@ -593,7 +605,7 @@ center is not a computational resource.
|
|
|
593
605
|
| Location | For a country, metro or region |
|
|
594
606
|
| TechnologyFacility | For a data center, colocation, office or edge site (`facility/type`), also a campus or hall inside one |
|
|
595
607
|
| TechnologyEquipment | Optional: racks, network fabrics, physical clusters inside a facility (`equipment/type`) |
|
|
596
|
-
| TechnologyDevice | Optional: one physical machine, `locatedAt` its room (`facility/type: room`) |
|
|
608
|
+
| TechnologyDevice | Optional: one physical machine, `locatedAt` its room (`facility/type: room`); a client device (laptop, workstation, phone) needs no room |
|
|
597
609
|
|
|
598
610
|
Relations are written on the element that is placed (see [Direction of Relations](#direction-of-relations)):
|
|
599
611
|
|
|
@@ -604,6 +616,8 @@ Relations are written on the element that is placed (see [Direction of Relations
|
|
|
604
616
|
| `TechnologyFacility` | `composedOf` | `TechnologyFacility` (nested sites, the rooms of a data center) |
|
|
605
617
|
| `TechnologyFacility` | `servedBy` | `BusinessActor` (the operator) |
|
|
606
618
|
| `TechnologyDevice`, `TechnologyEquipment`, `TechnologyNode`, `TechnologyCommunicationNetwork` | `ownedBy` | `BusinessActor`, `BusinessRole` (the team that operates it) |
|
|
619
|
+
| `ApplicationComponent`, `ApplicationService`, `TechnologyService` | `operatedBy` | `BusinessActor`, `BusinessRole` (the team that runs it in production) |
|
|
620
|
+
| `ApplicationComponent`, `ApplicationService`, `TechnologyService` | `maintainedBy` | `BusinessActor` (the team that builds and changes it) |
|
|
607
621
|
| `TechnologyDevice`, `TechnologyEquipment`, `TechnologyNode` | `servedBy` | `BusinessActor` (the team it is dedicated to, e.g. a test partition) |
|
|
608
622
|
|
|
609
623
|
`locatedAt` with many targets is the point: a service that runs in three data centers is **one** service with three
|
|
@@ -688,13 +702,19 @@ depend on each other, so `dependsOn` is exempt from the cycle check. In `example
|
|
|
688
702
|
|
|
689
703
|
### Where a Component Runs
|
|
690
704
|
|
|
691
|
-
|
|
705
|
+
These relations say what something runs on or builds; each answers a different question:
|
|
692
706
|
|
|
693
707
|
| Relation | Means | Example |
|
|
694
708
|
|----------|-------|---------|
|
|
695
709
|
| ApplicationComponent `servedBy technologyServices` | the platform service the component is deployed on | `Payments:API` runs in `Payments:Cluster` |
|
|
696
710
|
| ApplicationComponent `servedBy technologySystemSoftware` | the runtime it runs on | a Kubernetes distribution, a JVM |
|
|
697
711
|
| ApplicationService `servedBy technologyServices` | a platform service the service as a whole uses | a managed database, the clusters of its own components |
|
|
712
|
+
| ApplicationComponent `provisions technologyServices` | the platform service the component creates and keeps in its desired state | an operator that builds the clusters of a product |
|
|
713
|
+
|
|
714
|
+
`provisions` is written on the component, like every relation, so it points down to what the component creates: the
|
|
715
|
+
service page lists its provisioner as incoming, and the impact of a provisioner is its outgoing relations (the
|
|
716
|
+
services it builds). It is not a runtime dependency: a provisioned service keeps running while its provisioner is
|
|
717
|
+
down, only changes stop. `suppliedBy` on the service stays for what it is made of (the system software).
|
|
698
718
|
|
|
699
719
|
Deployment importers (Argo CD, Helm, Cluster API) know every cluster a deployable runs in and write the relation per
|
|
700
720
|
component. Keep the service-level relation to the clusters of the service's own components: a sidecar or agent that
|
|
@@ -997,7 +1017,7 @@ is a sidebar filter and query term (`lifecycle/status == "deprecated"`).
|
|
|
997
1017
|
|------------|---------|
|
|
998
1018
|
| `lifecycle/status` | The state above |
|
|
999
1019
|
| `lifecycle/activeAt`, `lifecycle/deprecatedAt`, `lifecycle/sunsetAt`, `lifecycle/endOfLifeAt` | When it happened or will happen (ISO 8601 date or time, checked by `archsight lint`) |
|
|
1000
|
-
| `lifecycle/replacedBy` | The resource that replaces it: a name for a resource of the same kind, `Kind/Name` for another kind (a component replaced by an `ApplicationService/...`). Shown as a link, `archsight lint` checks that it exists |
|
|
1020
|
+
| `lifecycle/replacedBy` | The resource that replaces it: a name for a resource of the same kind, `Kind/Name` for another kind (a component replaced by an `ApplicationService/...`). Shown as a link behind the status, `archsight lint` checks that it exists |
|
|
1001
1021
|
| `lifecycle/reason` | Why, in markdown |
|
|
1002
1022
|
|
|
1003
1023
|
```yaml
|
|
@@ -1010,6 +1030,9 @@ metadata:
|
|
|
1010
1030
|
lifecycle/replacedBy: ApplicationService/ExportV2
|
|
1011
1031
|
```
|
|
1012
1032
|
|
|
1033
|
+
The detail page shows the status, the replacement as a link and, for a `deprecated`, `sunset` or `end-of-life`
|
|
1034
|
+
resource, the number of resources that still refer to it (a link to its Incoming relations) on one line.
|
|
1035
|
+
|
|
1013
1036
|
## Common Patterns
|
|
1014
1037
|
|
|
1015
1038
|
### Microservice
|
data/lib/archsight/cli.rb
CHANGED
|
@@ -279,6 +279,7 @@ module Archsight
|
|
|
279
279
|
option :theme, type: :string, desc: "Spacing/font-size theme (default: the file's own `theme` statement)"
|
|
280
280
|
option :legend, type: :string, desc: "Legend placement (default: the file's own `legend` statement, else auto)"
|
|
281
281
|
option :profile, aliases: "-p", type: :boolean, desc: "Print stage timing stats after each render"
|
|
282
|
+
option :resources, aliases: "-r", type: :string, desc: "Directory that `include \"file://...\"` paths are relative to (default: the input's directory)"
|
|
282
283
|
option :format, type: :boolean, desc: "Re-indent the inputs in place instead of rendering them (-o: write a single input there)"
|
|
283
284
|
option :check, type: :boolean, desc: "With --format: only list the inputs that are not formatted, exit 1 if any"
|
|
284
285
|
def diagram(*inputs)
|
|
@@ -295,7 +296,7 @@ module Archsight
|
|
|
295
296
|
|
|
296
297
|
status = diagram::CLI.new.run_with(
|
|
297
298
|
inputs: inputs, output: options[:output], watch: options[:watch], profile: options[:profile],
|
|
298
|
-
format: options[:format], check: options[:check],
|
|
299
|
+
format: options[:format], check: options[:check], resources: options[:resources],
|
|
299
300
|
style: options[:style], theme: options[:theme], legend: options[:legend],
|
|
300
301
|
relation_filter: if relation.nil?
|
|
301
302
|
diagram::Relations::DEFAULT_FILTER
|
data/lib/archsight/database.rb
CHANGED
|
@@ -109,11 +109,12 @@ module Archsight
|
|
|
109
109
|
matches.first if matches.one?
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
#
|
|
112
|
+
# Annotation values across all instances of a kind with the number of instances using each,
|
|
113
|
+
# most used first (ties by value): [[value, count], ...]
|
|
113
114
|
def annotation_values(kind, annotation)
|
|
114
115
|
instances = instances_by_kind(kind).values
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
counts = instances.flat_map { |inst| Array(annotation.value_for(inst)).compact.uniq }.tally
|
|
117
|
+
counts.sort_by { |value, count| [-count, value.to_s] }
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
# Get filterable annotations with their values for a kind (excludes empty)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
require_relative "."
|
|
5
5
|
require_relative "formatter"
|
|
6
|
+
require_relative "../helpers/diagram_includes"
|
|
6
7
|
|
|
7
8
|
module Archsight
|
|
8
9
|
module Diagram
|
|
@@ -79,6 +80,8 @@ module Archsight
|
|
|
79
80
|
opts.on("-p", "--profile", "Print stage timing stats after each render") do
|
|
80
81
|
options[:profile] = true
|
|
81
82
|
end
|
|
83
|
+
opts.on("--resources=DIR", "Directory that include \"file://...\" paths are relative to " \
|
|
84
|
+
"(default: the input file's directory)") { |v| options[:resources] = v }
|
|
82
85
|
opts.on("--format", "Re-indent the inputs in place instead of rendering them (-o: write a single input there)") do
|
|
83
86
|
options[:format] = true
|
|
84
87
|
end
|
|
@@ -155,7 +158,9 @@ module Archsight
|
|
|
155
158
|
total_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) if profile
|
|
156
159
|
|
|
157
160
|
svg = Archsight::Diagram.render(File.read(input), relation_filter: options[:relation_filter], profile: stats,
|
|
158
|
-
style: options[:style], theme: options[:theme], legend: options[:legend]
|
|
161
|
+
style: options[:style], theme: options[:theme], legend: options[:legend],
|
|
162
|
+
includes: includes_for(input, options), warnings: warnings = [])
|
|
163
|
+
warnings.each { |message| warn "archsight-diagram: #{input}: #{message}" }
|
|
159
164
|
|
|
160
165
|
write_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) if profile
|
|
161
166
|
File.write(output, svg)
|
|
@@ -172,6 +177,11 @@ module Archsight
|
|
|
172
177
|
1
|
|
173
178
|
end
|
|
174
179
|
|
|
180
|
+
# Standalone, `include` reads `file://` paths only (there is no model for `archsight://`)
|
|
181
|
+
def includes_for(input, options)
|
|
182
|
+
Helpers::DiagramIncludes.new(nil, resources_dir: options[:resources] || File.dirname(File.expand_path(input)))
|
|
183
|
+
end
|
|
184
|
+
|
|
175
185
|
def print_stats(stats, total_start)
|
|
176
186
|
total = Process.clock_gettime(Process::CLOCK_MONOTONIC) - total_start
|
|
177
187
|
parts = stats.map { |key, seconds| "#{key}=#{format("%.1fms", seconds * 1000)}" }
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
module Diagram
|
|
5
|
+
# Expands `include "<path>"` statements: each one is replaced, at its position, by the statements of the
|
|
6
|
+
# diagram it names. The renderer knows nothing about where diagrams live; the caller supplies a loader
|
|
7
|
+
# (`archsight://` and `file://` paths, see `Helpers::DiagramIncludes`).
|
|
8
|
+
#
|
|
9
|
+
# `loader.call(path)` returns the source of the included diagram, or `:missing` / `:forbidden` / `:invalid`.
|
|
10
|
+
# Included sources may include others. An include inside a container takes blocks and edges only: a
|
|
11
|
+
# `dataflow` or a `theme`/`legend`/`ranks` setting belongs to the whole diagram, so it is left out there
|
|
12
|
+
# and reported in `Result#warnings`.
|
|
13
|
+
# `include "x" { namespace "cs" }` puts `cs.` in front of every id the included diagram defines (nodes and
|
|
14
|
+
# dataflows, and the references to them inside it), so the same diagram can be included more than once;
|
|
15
|
+
# a node without a `label` keeps showing its unprefixed id. References to ids the diagram does not define
|
|
16
|
+
# are left alone: they name nodes of the including diagram.
|
|
17
|
+
# Edges need no such care, they are resolved by id once the whole graph is built, so a fragment may link to
|
|
18
|
+
# nodes of the diagram that includes it and the other way round.
|
|
19
|
+
class Includes
|
|
20
|
+
MAX_DEPTH = 8
|
|
21
|
+
MAX_INCLUDES = 100
|
|
22
|
+
REASONS = { missing: "not found", forbidden: "is not allowed", invalid: "is not a valid include path" }.freeze
|
|
23
|
+
|
|
24
|
+
# The expanded statements, the sources that were loaded (`{ path => source }`, in order) and what was
|
|
25
|
+
# left out of them (warning messages).
|
|
26
|
+
Result = Struct.new(:statements, :sources, :warnings, keyword_init: true)
|
|
27
|
+
|
|
28
|
+
# @param statements [Array] parsed statements of the main source
|
|
29
|
+
# @param loader [#call, nil]
|
|
30
|
+
# @raise [IncludeError]
|
|
31
|
+
def self.expand(statements, loader)
|
|
32
|
+
new(loader).expand(statements)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.includes?(statements)
|
|
36
|
+
statements.any? { |s| s.is_a?(AST::Include) || (s.is_a?(AST::Block) && includes?(s.children)) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(loader)
|
|
40
|
+
@loader = loader
|
|
41
|
+
@sources = {}
|
|
42
|
+
@warnings = []
|
|
43
|
+
@count = 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def expand(statements)
|
|
47
|
+
Result.new(statements: splice(statements, [], top: true), sources: @sources, warnings: @warnings)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def splice(statements, stack, top:)
|
|
53
|
+
statements.flat_map do |stmt|
|
|
54
|
+
case stmt
|
|
55
|
+
when AST::Include then included(stmt, stack, top: top)
|
|
56
|
+
when AST::Block
|
|
57
|
+
stmt.children = splice(stmt.children, stack, top: false)
|
|
58
|
+
[stmt]
|
|
59
|
+
else [stmt]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def included(stmt, stack, top:)
|
|
65
|
+
path = stmt.path
|
|
66
|
+
raise IncludeError, "include cycle: #{(stack + [path]).join(" -> ")} (line #{stmt.line})" if stack.include?(path)
|
|
67
|
+
raise IncludeError, "include #{path.inspect} (line #{stmt.line}) is nested deeper than #{MAX_DEPTH} levels" if stack.length >= MAX_DEPTH
|
|
68
|
+
|
|
69
|
+
@count += 1
|
|
70
|
+
raise IncludeError, "too many includes (more than #{MAX_INCLUDES})" if @count > MAX_INCLUDES
|
|
71
|
+
|
|
72
|
+
source = load(path, stmt.line)
|
|
73
|
+
@sources[path] = source
|
|
74
|
+
parsed = parse(path, source)
|
|
75
|
+
parsed = drop_whole_diagram_statements(path, parsed, stmt.line) unless top
|
|
76
|
+
statements = splice(parsed, stack + [path], top: top)
|
|
77
|
+
stmt.namespace ? Namespacer.new(stmt.namespace).apply(statements) : statements
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def load(path, line)
|
|
81
|
+
raise IncludeError, "include #{path.inspect} (line #{line}) is not available here" unless @loader
|
|
82
|
+
|
|
83
|
+
result = @loader.call(path)
|
|
84
|
+
return result if result.is_a?(String)
|
|
85
|
+
|
|
86
|
+
reason = REASONS.fetch(result) { raise ArgumentError, "include loader returned #{result.inspect} for #{path.inspect}" }
|
|
87
|
+
raise IncludeError, "include #{path.inspect} #{reason} (line #{line})"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def parse(path, source)
|
|
91
|
+
Parser.parse(source, anon_scope: "i#{@count}_")
|
|
92
|
+
rescue ParseError, GraphError => e
|
|
93
|
+
raise IncludeError, "in include #{path.inspect}: #{e.message}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Dataflows and settings apply to the whole diagram, so inside a container they are ignored (with a warning)
|
|
97
|
+
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.each do |s|
|
|
100
|
+
what = s.is_a?(AST::DataFlow) ? "dataflow #{s.id.inspect}" : "#{s.key} setting"
|
|
101
|
+
@warnings << "include #{path.inspect} (line #{line}): the #{what} (line #{s.line}) is ignored, it only applies when included at the top level"
|
|
102
|
+
end
|
|
103
|
+
kept
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Puts `<namespace>.` in front of the ids a list of statements defines, and of the references to them
|
|
107
|
+
class Namespacer
|
|
108
|
+
def initialize(namespace)
|
|
109
|
+
@prefix = "#{namespace}."
|
|
110
|
+
@defined = {}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def apply(statements)
|
|
114
|
+
collect(statements)
|
|
115
|
+
statements.each { |stmt| rename(stmt) }
|
|
116
|
+
statements
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# Anonymous layers/stacks have ids no one can reference and that are unique already
|
|
122
|
+
def collect(statements)
|
|
123
|
+
statements.each do |stmt|
|
|
124
|
+
case stmt
|
|
125
|
+
when AST::Block
|
|
126
|
+
@defined[stmt.id] = true unless stmt.anonymous?
|
|
127
|
+
collect(stmt.children)
|
|
128
|
+
when AST::DataFlow then @defined[stmt.id] = true
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def rename(stmt)
|
|
134
|
+
case stmt
|
|
135
|
+
when AST::Block then rename_block(stmt)
|
|
136
|
+
when AST::Edge
|
|
137
|
+
stmt.from = ref(stmt.from)
|
|
138
|
+
stmt.to = ref(stmt.to)
|
|
139
|
+
when AST::DataFlow then rename_dataflow(stmt)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def rename_block(block)
|
|
144
|
+
unless block.anonymous?
|
|
145
|
+
block.attrs["label"] ||= block.id # the label defaults to the id
|
|
146
|
+
block.id = ref(block.id)
|
|
147
|
+
end
|
|
148
|
+
block.children.each { |child| rename(child) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def rename_dataflow(flow)
|
|
152
|
+
flow.attrs["label"] ||= flow.id
|
|
153
|
+
flow.id = ref(flow.id)
|
|
154
|
+
flow.hops.map! { |hop| hop.is_a?(AST::DataFlowGroup) ? rename_group(hop) : ref(hop) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def rename_group(group)
|
|
158
|
+
group.branches.each { |branch| branch.hops.map! { |hop| ref(hop) } }
|
|
159
|
+
group
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def ref(id)
|
|
163
|
+
@defined[id] ? "#{@prefix}#{id}" : id
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -43,6 +43,11 @@ module Archsight
|
|
|
43
43
|
# own attrs at graph-resolution time).
|
|
44
44
|
DataFlowBranch = Struct.new(:hops, :attrs, :line, keyword_init: true)
|
|
45
45
|
|
|
46
|
+
# `include "<path>" [{ namespace "cs" }]` -- another diagram spliced in at this position (see `Includes`).
|
|
47
|
+
# Parsed like any statement, replaced by the included statements before the graph is built, so nothing
|
|
48
|
+
# downstream sees it. `namespace`, if any, puts `<namespace>.` in front of every id the included diagram defines.
|
|
49
|
+
Include = Struct.new(:path, :namespace, :line, keyword_init: true)
|
|
50
|
+
|
|
46
51
|
# A top-level diagram-wide `key "value"` setting -- currently only
|
|
47
52
|
# `theme "compact"` (see `Theme`).
|
|
48
53
|
Setting = Struct.new(:key, :value, :line, keyword_init: true)
|
|
@@ -20,6 +20,8 @@ module Archsight
|
|
|
20
20
|
|
|
21
21
|
raise ParseError, "#{label} at line #{line}: columns must be a positive integer, got #{raw.inspect}" unless raw.match?(/\A[1-9]\d*\z/)
|
|
22
22
|
|
|
23
|
+
raise ParseError, "#{label} at line #{line}: columns cannot be combined with include (the included blocks are not known yet)" if children.any?(AST::Include)
|
|
24
|
+
|
|
23
25
|
blocks, edges = children.partition { |c| c.is_a?(AST::Block) }
|
|
24
26
|
count = [raw.to_i, blocks.length].min
|
|
25
27
|
return children if blocks.empty?
|
|
@@ -14,9 +14,11 @@ module Archsight
|
|
|
14
14
|
#
|
|
15
15
|
# Grammar (informal):
|
|
16
16
|
# program := statement*
|
|
17
|
-
# statement := block | dataflow | edge | setting
|
|
17
|
+
# statement := block | dataflow | edge | setting | include
|
|
18
|
+
# include := "include" STRING ["{" "namespace" STRING "}"] [";"]
|
|
19
|
+
# -- top level, or where a container may hold blocks and edges
|
|
18
20
|
# setting := ("theme" | "legend" | "ranks") STRING [";"] -- top level only
|
|
19
|
-
# block := (CONTAINER_KEYWORD | LEAF_KEYWORD) [STRING] "{" (attr | block | edge)* "}"
|
|
21
|
+
# block := (CONTAINER_KEYWORD | LEAF_KEYWORD) [STRING] "{" (attr | block | edge | include)* "}"
|
|
20
22
|
# -- the id is required except for ANONYMOUS_KEYWORDS
|
|
21
23
|
# dataflow := "dataflow" STRING "{" (hop | hop_group | attr)* "}"
|
|
22
24
|
# -- needs >= 2 hops (per branch, if a hop_group is present);
|
|
@@ -45,6 +47,7 @@ module Archsight
|
|
|
45
47
|
# `layer`/`stack` may be used without an id, purely as a layout hint
|
|
46
48
|
# (see AST::Block#anonymous) — a `group`/`boundary` always renders a
|
|
47
49
|
# visible box, so an unnamed one wouldn't make sense the same way.
|
|
50
|
+
NAMESPACE = /\A[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*\z/
|
|
48
51
|
ANONYMOUS_KEYWORDS = %w[layer stack].freeze
|
|
49
52
|
SETTING_KEYWORDS = %w[theme legend ranks].freeze
|
|
50
53
|
EDGE_ARROWS = {
|
|
@@ -55,13 +58,16 @@ module Archsight
|
|
|
55
58
|
|
|
56
59
|
attr_reader :cursor
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
# `anon_scope` namespaces the ids of anonymous layers/stacks, so the statements of an included
|
|
62
|
+
# source (parsed on its own) never collide with those of the including one.
|
|
63
|
+
def self.parse(source, anon_scope: nil)
|
|
64
|
+
new(source, anon_scope: anon_scope).parse
|
|
60
65
|
end
|
|
61
66
|
|
|
62
|
-
def initialize(source)
|
|
67
|
+
def initialize(source, anon_scope: nil)
|
|
63
68
|
@cursor = TokenCursor.new(Lexer.new(source).tokenize)
|
|
64
69
|
@anon_counter = 0
|
|
70
|
+
@anon_scope = anon_scope
|
|
65
71
|
end
|
|
66
72
|
|
|
67
73
|
def parse
|
|
@@ -75,6 +81,8 @@ module Archsight
|
|
|
75
81
|
def statement
|
|
76
82
|
if check_block_keyword?
|
|
77
83
|
block
|
|
84
|
+
elsif include_directive?
|
|
85
|
+
include_statement
|
|
78
86
|
elsif cursor.check(:ident, "dataflow") && cursor.peek_type(1) == :string
|
|
79
87
|
DataflowParser.new(cursor).parse
|
|
80
88
|
elsif cursor.check(:ident) && EDGE_ARROWS.key?(cursor.peek_type(1))
|
|
@@ -108,22 +116,16 @@ module Archsight
|
|
|
108
116
|
children = []
|
|
109
117
|
|
|
110
118
|
parse_attrs_until_rbrace(attrs, label, expected: "an attribute, a nested block, an edge") do
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
elsif cursor.check(:ident) && EDGE_ARROWS.key?(cursor.peek_type(1))
|
|
115
|
-
children << edge
|
|
116
|
-
true
|
|
117
|
-
else
|
|
118
|
-
false
|
|
119
|
-
end
|
|
119
|
+
child = container_child
|
|
120
|
+
children << child if child
|
|
121
|
+
child
|
|
120
122
|
end
|
|
121
123
|
|
|
122
124
|
cursor.expect(:rbrace, "expected '}' to close #{label}")
|
|
123
125
|
|
|
124
126
|
if LEAF_KEYWORDS.include?(kind_token.value) && !children.empty?
|
|
125
127
|
raise ParseError, "#{label} at line #{kind_token.line} cannot " \
|
|
126
|
-
"contain nested blocks or
|
|
128
|
+
"contain nested blocks, edges or includes (did you mean 'group'?)"
|
|
127
129
|
end
|
|
128
130
|
|
|
129
131
|
raise ParseError, "#{label} at line #{kind_token.line}: columns only applies to containers" if LEAF_KEYWORDS.include?(kind_token.value) && attrs.key?("columns")
|
|
@@ -133,6 +135,46 @@ module Archsight
|
|
|
133
135
|
AST::Block.new(kind: kind, id: id, anonymous: anonymous, attrs: attrs, children: children, line: kind_token.line)
|
|
134
136
|
end
|
|
135
137
|
|
|
138
|
+
# The nested block, edge or include at the cursor, nil if it is something else (an attribute)
|
|
139
|
+
def container_child
|
|
140
|
+
if check_block_keyword?
|
|
141
|
+
block
|
|
142
|
+
elsif include_directive?
|
|
143
|
+
include_statement
|
|
144
|
+
elsif cursor.check(:ident) && EDGE_ARROWS.key?(cursor.peek_type(1))
|
|
145
|
+
edge
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# `include "<path>"` -- `include -> x` is still an edge, so a node may be named "include"
|
|
150
|
+
def include_directive?
|
|
151
|
+
cursor.check(:ident, "include") && cursor.peek_type(1) == :string
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def include_statement
|
|
155
|
+
keyword = cursor.advance
|
|
156
|
+
path = cursor.advance
|
|
157
|
+
namespace = include_options(path.value) if cursor.check(:lbrace)
|
|
158
|
+
cursor.consume(:semi)
|
|
159
|
+
AST::Include.new(path: path.value, namespace: namespace, line: keyword.line)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# `{ namespace "cs" }` after an include: the only option
|
|
163
|
+
def include_options(path)
|
|
164
|
+
cursor.advance
|
|
165
|
+
options = {}
|
|
166
|
+
until cursor.check(:rbrace)
|
|
167
|
+
line = cursor.current.line
|
|
168
|
+
key, value = cursor.check(:ident) ? attr : cursor.expect(:ident, "expected 'namespace' in the options of include #{path.inspect}")
|
|
169
|
+
raise ParseError, "unknown option '#{key}' of include #{path.inspect} at line #{line} (only 'namespace')" unless key == "namespace"
|
|
170
|
+
raise ParseError, "the namespace #{value.inspect} of include #{path.inspect} at line #{line} must be a name like \"cs\" or \"a.b\"" unless value.match?(NAMESPACE)
|
|
171
|
+
|
|
172
|
+
store_attr(options, key, value, line, "include #{path.inspect}")
|
|
173
|
+
end
|
|
174
|
+
cursor.expect(:rbrace, "expected '}' to close the options of include #{path.inspect}")
|
|
175
|
+
options["namespace"]
|
|
176
|
+
end
|
|
177
|
+
|
|
136
178
|
# `theme "compact"`, `legend "right"` -- `theme -> x` is still an edge
|
|
137
179
|
# (an arrow, not a string, follows the id), so a node may still be
|
|
138
180
|
# named "theme" or "legend".
|
|
@@ -145,7 +187,7 @@ module Archsight
|
|
|
145
187
|
|
|
146
188
|
def next_anonymous_id
|
|
147
189
|
@anon_counter += 1
|
|
148
|
-
"__asd_anon_#{@anon_counter}"
|
|
190
|
+
"__asd_anon_#{@anon_scope}#{@anon_counter}"
|
|
149
191
|
end
|
|
150
192
|
|
|
151
193
|
# What a human should see identifying this block in an error message
|