Add Archify | New Model… with three C4 examples and Open Model in the Explorer menu
- templates/models: C4 - Standard Web App, C4 - E-Shop, C4 - Event-Based Integration; each a complete model (main.archify, registry, context, containers, components) that validates - archify.newModel: pick an example and a folder name, copy it, name the model after the folder, open the model page - Explorer menu: New Model… on folders, Open Model on .archify and .archify-registry files; File → New File… lists New Model… - unit test validates every example end to end; integration test runs the command; README, CHANGELOG, docs/model.md
This commit is contained in:
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
- **Models:** a folder with a `main.archify` at its top is a model, a set of diagrams that belong together. Opening `main.archify` shows the model page: name and description, the overview diagram if the file has one, the diagrams grouped by folder (click to open), and the registry with where each element is used.
|
- **Models:** a folder with a `main.archify` at its top is a model, a set of diagrams that belong together. Opening `main.archify` shows the model page: name and description, the overview diagram if the file has one, the diagrams grouped by folder (click to open), and the registry with where each element is used.
|
||||||
- **Registry:** `*.archify-registry` files define shared elements once; diagrams draw them with `ref: <id>` plus position, and may override `label`, `sublabel` and `tag`. Hover, go-to-definition and completion on `ref`; unknown names, forbidden overrides, duplicate ids and broken registries in Problems. Registry edits re-render open diagrams.
|
- **Registry:** `*.archify-registry` files define shared elements once; diagrams draw them with `ref: <id>` plus position, and may override `label`, `sublabel` and `tag`. Hover, go-to-definition and completion on `ref`; unknown names, forbidden overrides, duplicate ids and broken registries in Problems. Registry edits re-render open diagrams.
|
||||||
- **Links:** `diagram:` on a registry element or a component points at the diagram that explains it; double-click the node (2D view) to open it.
|
- **Links:** `diagram:` on a registry element or a component points at the diagram that explains it; double-click the node (2D view) to open it.
|
||||||
- New command *Archify | Open Model*; *New Diagram… → Model (main.archify)* template; editor schema for registries and the new keys; example model in `examples/online-shop`.
|
- **New Model…:** right-click a folder and choose *Archify | New Model…* (also in *File → New File…* and the Command Palette) to start from one of three complete C4 examples: **C4 - Standard Web App**, **C4 - E-Shop**, **C4 - Event-Based Integration**. Each creates a model folder with `main.archify`, a registry and context, container and component diagrams, named after the folder you choose, and opens the model page.
|
||||||
|
- New commands *Archify | Open Model* (editor title, Command Palette and the Explorer menu of `.archify` and `.archify-registry` files); *New Diagram… → Model (main.archify)* template for an empty model; editor schema for registries and the new keys; example model in `examples/online-shop`.
|
||||||
|
|
||||||
## 0.6.2 — 2026-10-19
|
## 0.6.2 — 2026-10-19
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ New diagrams should be `.archify` files; everything below describes them unless
|
|||||||
|
|
||||||
The fastest start: right-click a folder in the Explorer and choose **Archify | New Diagram…** (also in *File → New File…* and the Command Palette). Pick a kind of diagram (Architecture, Workflow, Sequence, Data Flow, Lifecycle or Everyday Plan), give it a name, and a small working `.archify` file is created and opened. Change its YAML to fit your case.
|
The fastest start: right-click a folder in the Explorer and choose **Archify | New Diagram…** (also in *File → New File…* and the Command Palette). Pick a kind of diagram (Architecture, Workflow, Sequence, Data Flow, Lifecycle or Everyday Plan), give it a name, and a small working `.archify` file is created and opened. Change its YAML to fit your case.
|
||||||
|
|
||||||
|
For a whole set of diagrams, choose **Archify | New Model…** instead and pick one of the examples (C4 - Standard Web App, C4 - E-Shop, C4 - Event-Based Integration). A model folder with context, container and component diagrams and a shared registry is created and its model page opens. See [Models](#models).
|
||||||
|
|
||||||
To write one by hand:
|
To write one by hand:
|
||||||
|
|
||||||
1. Create a file ending in `.archify`, for example `hello.archify`:
|
1. Create a file ending in `.archify`, for example `hello.archify`:
|
||||||
@@ -62,6 +64,7 @@ The Archify CLI and the AI agents that use it write diagrams as JSON, named `nam
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **New diagram from a template**: *Archify | New Diagram…* creates a starter `.archify` file for any of the five diagram kinds, or an everyday plan, in the chosen folder.
|
- **New diagram from a template**: *Archify | New Diagram…* creates a starter `.archify` file for any of the five diagram kinds, or an everyday plan, in the chosen folder.
|
||||||
|
- **New model from an example**: *Archify | New Model…* creates a complete model folder from a C4 example (Standard Web App, E-Shop, Event-Based Integration).
|
||||||
- **Diagram editor**: `.archify` files open as the rendered diagram. **Show Source** opens the YAML beside it, and edits re-render live.
|
- **Diagram editor**: `.archify` files open as the rendered diagram. **Show Source** opens the YAML beside it, and edits re-render live.
|
||||||
- **Live updates**: the diagram re-renders as you edit, and keeps the focus and route you selected (`#focus=…`, `#route=…`).
|
- **Live updates**: the diagram re-renders as you edit, and keeps the focus and route you selected (`#focus=…`, `#route=…`).
|
||||||
- **HTML viewer**: right-click a rendered `.html` file and choose **Archify | Open in HTML Viewer**, or use *Reopen Editor With… → Archify HTML Viewer*. For Archify HTML already open as text, a button appears in the editor title. The viewer reloads when the file changes on disk.
|
- **HTML viewer**: right-click a rendered `.html` file and choose **Archify | Open in HTML Viewer**, or use *Reopen Editor With… → Archify HTML Viewer*. For Archify HTML already open as text, a button appears in the editor title. The viewer reloads when the file changes on disk.
|
||||||
@@ -70,7 +73,7 @@ The Archify CLI and the AI agents that use it write diagrams as JSON, named `nam
|
|||||||
- **Schema help**: completion and hover from Archify's own schemas, through the YAML extension for `.archify` files.
|
- **Schema help**: completion and hover from Archify's own schemas, through the YAML extension for `.archify` files.
|
||||||
- **Source links**: in source-backed diagrams, clicking a source reference opens that file and line range in the editor. If the file is not in the workspace, the web link opens instead.
|
- **Source links**: in source-backed diagrams, clicking a source reference opens that file and line range in the editor. If the file is not in the workspace, the web link opens instead.
|
||||||
- **Export**: the diagram's own Export menu (PNG, SVG, share card, …) saves through a VS Code save dialog.
|
- **Export**: the diagram's own Export menu (PNG, SVG, share card, …) saves through a VS Code save dialog.
|
||||||
- **Commands**: *Archify | New Diagram…*, *Archify | Render to HTML File…*, *Archify | Open in Browser*, *Archify | Refresh Preview*, *Archify | Show Source*.
|
- **Commands**: *Archify | New Diagram…*, *Archify | New Model…*, *Archify | Open Model*, *Archify | Render to HTML File…*, *Archify | Open in Browser*, *Archify | Refresh Preview*, *Archify | Show Source*.
|
||||||
|
|
||||||
## 3D view
|
## 3D view
|
||||||
|
|
||||||
@@ -116,7 +119,8 @@ online-shop/
|
|||||||
- **The model page.** Click `main.archify` and you get the model's page: its name and description, the overview diagram if the file has one, the diagrams of the model grouped by folder (click one to open it), and the registry with "used in N diagrams".
|
- **The model page.** Click `main.archify` and you get the model's page: its name and description, the overview diagram if the file has one, the diagrams of the model grouped by folder (click one to open it), and the registry with "used in N diagrams".
|
||||||
- **Shared elements.** A registry file defines an element once (`type`, `label`, `sublabel`, `brand`, `description`, …). A diagram draws it with `ref: api` plus `pos` and `size`; it may override `label`, `sublabel` and `tag`, and may still define unshared elements inline. Hover a `ref` for the description, F12 jumps to the registry, completion after `ref:` lists the elements, unknown names appear in Problems.
|
- **Shared elements.** A registry file defines an element once (`type`, `label`, `sublabel`, `brand`, `description`, …). A diagram draws it with `ref: api` plus `pos` and `size`; it may override `label`, `sublabel` and `tag`, and may still define unshared elements inline. Hover a `ref` for the description, F12 jumps to the registry, completion after `ref:` lists the elements, unknown names appear in Problems.
|
||||||
- **Links.** A registry element (or a component) with `diagram: 0300-components/api.archify` points at the diagram that explains it: double-click the node in the 2D view to open it.
|
- **Links.** A registry element (or a component) with `diagram: 0300-components/api.archify` points at the diagram that explains it: double-click the node in the 2D view to open it.
|
||||||
- **Commands.** *Archify | Open Model* opens the model page of the current file; *Archify | New Diagram… → Model* creates a `main.archify` from the folder name.
|
- **Starting one.** *Archify | New Model…* (right-click a folder) copies one of the shipped examples into a new folder, named as you choose: **C4 - Standard Web App** (single-page app, API, database, cache, storage, identity provider), **C4 - E-Shop** (web shop, API, worker, queue, payment and email providers) or **C4 - Event-Based Integration** (event bus, router, dead-letter queue, event store, consumers). Each has a registry and context, container and component diagrams. For an empty model, *Archify | New Diagram… → Model* creates just a `main.archify`.
|
||||||
|
- **Commands.** *Archify | Open Model* opens the model page of the current file, from the editor title, the Command Palette or the Explorer menu of a `.archify` or `.archify-registry` file.
|
||||||
|
|
||||||
Formats in detail: [docs/model.md](docs/model.md). A full example: [examples/online-shop](examples/online-shop).
|
Formats in detail: [docs/model.md](docs/model.md). A full example: [examples/online-shop](examples/online-shop).
|
||||||
|
|
||||||
|
|||||||
@@ -80,3 +80,7 @@ Registry `diagram` links and component `diagram` links are the same mechanism; t
|
|||||||
## Outside a model
|
## Outside a model
|
||||||
|
|
||||||
A diagram with `ref` components outside any model (no `main.archify` above it) reports "no registry found" and shows the component by its ref name with type `external`, so the picture still renders.
|
A diagram with `ref` components outside any model (no `main.archify` above it) reports "no registry found" and shows the component by its ref name with type `external`, so the picture still renders.
|
||||||
|
|
||||||
|
## Starting a model
|
||||||
|
|
||||||
|
*Archify | New Model…* (right-click a folder, *File → New File…*, Command Palette) offers three complete examples: **C4 - Standard Web App**, **C4 - E-Shop** and **C4 - Event-Based Integration**. Pick one and a folder name; the example is copied into that folder with `model.id`, `model.name` and the overview title taken from the folder name (an example kept under its own name is copied as is). Each example has `registry/{people,systems,containers}.archify-registry` and diagrams under `0100-context`, `0200-containers` and `0300-components`; every registry element is used by at least one diagram and every diagram validates.
|
||||||
|
|||||||
+23
-4
@@ -51,6 +51,11 @@
|
|||||||
"title": "Archify | New Diagram…",
|
"title": "Archify | New Diagram…",
|
||||||
"icon": "$(new-file)"
|
"icon": "$(new-file)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "archify.newModel",
|
||||||
|
"title": "Archify | New Model…",
|
||||||
|
"icon": "$(new-folder)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "archify.showPreview",
|
"command": "archify.showPreview",
|
||||||
"title": "Archify | Open Preview",
|
"title": "Archify | Open Preview",
|
||||||
@@ -171,19 +176,29 @@
|
|||||||
"group": "zzz_archify@1"
|
"group": "zzz_archify@1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "archify.showPreview",
|
"command": "archify.newModel",
|
||||||
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
"when": "explorerResourceIsFolder",
|
||||||
"group": "zzz_archify@2"
|
"group": "zzz_archify@2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "archify.renderToHtml",
|
"command": "archify.showPreview",
|
||||||
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
||||||
"group": "zzz_archify@3"
|
"group": "zzz_archify@3"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "archify.renderToHtml",
|
||||||
|
"when": "resourceFilename =~ /(\\.archify|\\.(architecture|workflow|sequence|dataflow|lifecycle)\\.json)$/",
|
||||||
|
"group": "zzz_archify@4"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "archify.openHtmlViewer",
|
"command": "archify.openHtmlViewer",
|
||||||
"when": "resourceExtname == .html",
|
"when": "resourceExtname == .html",
|
||||||
"group": "zzz_archify@2"
|
"group": "zzz_archify@3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "archify.openModel",
|
||||||
|
"when": "resourceFilename =~ /\\.archify(-registry)?$/",
|
||||||
|
"group": "zzz_archify@5"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"editor/title/context": [
|
"editor/title/context": [
|
||||||
@@ -197,6 +212,10 @@
|
|||||||
{
|
{
|
||||||
"command": "archify.newDiagram",
|
"command": "archify.newDiagram",
|
||||||
"group": "file"
|
"group": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "archify.newModel",
|
||||||
|
"group": "file"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import { MODEL_EXAMPLES, mainFromExample } from '../language/examples';
|
||||||
|
import { diagramName, invalidNameReason } from '../language/templates';
|
||||||
import { MAIN_FILE } from '../model/format';
|
import { MAIN_FILE } from '../model/format';
|
||||||
|
import { readText } from '../util/files';
|
||||||
import { DIAGRAM_EDITOR_VIEW_TYPE } from '../views/diagramView';
|
import { DIAGRAM_EDITOR_VIEW_TYPE } from '../views/diagramView';
|
||||||
import { CommandContext, targetUri } from './context';
|
import { CommandContext, exists, targetUri } from './context';
|
||||||
|
|
||||||
|
/** Folder the new model folder goes into: the clicked folder (or a file's folder), else a workspace folder. */
|
||||||
|
async function parentFolder(arg?: unknown): Promise<vscode.Uri | undefined> {
|
||||||
|
if (arg instanceof vscode.Uri) {
|
||||||
|
const stat = await vscode.workspace.fs.stat(arg);
|
||||||
|
return stat.type & vscode.FileType.Directory ? arg : vscode.Uri.joinPath(arg, '..');
|
||||||
|
}
|
||||||
|
const folders = vscode.workspace.workspaceFolders ?? [];
|
||||||
|
if (folders.length <= 1) return folders[0]?.uri;
|
||||||
|
return (await vscode.window.showWorkspaceFolderPick({ placeHolder: 'Folder for the new model' }))?.uri;
|
||||||
|
}
|
||||||
|
|
||||||
export function modelCommands(ctx: CommandContext): vscode.Disposable[] {
|
export function modelCommands(ctx: CommandContext): vscode.Disposable[] {
|
||||||
return [
|
return [
|
||||||
@@ -15,5 +29,42 @@ export function modelCommands(ctx: CommandContext): vscode.Disposable[] {
|
|||||||
}
|
}
|
||||||
await vscode.commands.executeCommand('vscode.openWith', vscode.Uri.joinPath(root, MAIN_FILE), DIAGRAM_EDITOR_VIEW_TYPE, vscode.ViewColumn.Active);
|
await vscode.commands.executeCommand('vscode.openWith', vscode.Uri.joinPath(root, MAIN_FILE), DIAGRAM_EDITOR_VIEW_TYPE, vscode.ViewColumn.Active);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// Creates a new model folder from one of the shipped examples and opens its model page.
|
||||||
|
vscode.commands.registerCommand('archify.newModel', async (arg?: unknown) => {
|
||||||
|
const picked = await vscode.window.showQuickPick(
|
||||||
|
MODEL_EXAMPLES.map((example) => ({ label: example.label, description: example.description, example })),
|
||||||
|
{ title: 'Archify | New Model', placeHolder: 'Example to start from' },
|
||||||
|
);
|
||||||
|
if (!picked) return;
|
||||||
|
const { example } = picked;
|
||||||
|
|
||||||
|
let parent = await parentFolder(arg);
|
||||||
|
if (!parent) {
|
||||||
|
const chosen = await vscode.window.showOpenDialog({ title: 'Archify | New Model', canSelectFiles: false, canSelectFolders: true, openLabel: 'Create model here' });
|
||||||
|
parent = chosen?.[0];
|
||||||
|
if (!parent) return;
|
||||||
|
}
|
||||||
|
const name = await vscode.window.showInputBox({
|
||||||
|
title: 'Archify | New Model',
|
||||||
|
prompt: `Folder name in ${vscode.workspace.asRelativePath(parent, true)}`,
|
||||||
|
value: example.folder,
|
||||||
|
validateInput: async (value) =>
|
||||||
|
invalidNameReason(value) ?? ((await exists(vscode.Uri.joinPath(parent, diagramName(value)))) ? `${diagramName(value)} already exists.` : undefined),
|
||||||
|
});
|
||||||
|
if (name === undefined) return;
|
||||||
|
const folderName = diagramName(name);
|
||||||
|
const target = vscode.Uri.joinPath(parent, folderName);
|
||||||
|
if (await exists(target)) {
|
||||||
|
vscode.window.showWarningMessage(`Archify: ${vscode.workspace.asRelativePath(target, true)} already exists.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = vscode.Uri.joinPath(ctx.extension.extensionUri, 'templates', 'models', example.folder);
|
||||||
|
await vscode.workspace.fs.copy(source, target, { overwrite: false });
|
||||||
|
const main = vscode.Uri.joinPath(target, MAIN_FILE);
|
||||||
|
await vscode.workspace.fs.writeFile(main, Buffer.from(mainFromExample(await readText(main), folderName, example), 'utf8'));
|
||||||
|
await vscode.commands.executeCommand('vscode.openWith', main, DIAGRAM_EDITOR_VIEW_TYPE);
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { parseDocument } from 'yaml';
|
||||||
|
import { modelIdFromName, titleFromName } from './templates';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example models shipped with the extension (templates/models/<folder>).
|
||||||
|
* "Archify | New Model…" offers them; each is a complete model folder:
|
||||||
|
* main.archify, registry/*.archify-registry and diagrams by level.
|
||||||
|
*/
|
||||||
|
export interface ModelExample {
|
||||||
|
/** Folder under templates/models and the default name of the new model folder. */
|
||||||
|
folder: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MODEL_EXAMPLES: readonly ModelExample[] = [
|
||||||
|
{ folder: 'c4-standard-web-app', label: 'C4 - Standard Web App', description: 'Single-page app, API, database, cache, file storage, identity provider' },
|
||||||
|
{ folder: 'c4-e-shop', label: 'C4 - E-Shop', description: 'Web shop, API, worker, queue, payment and email providers' },
|
||||||
|
{ folder: 'c4-event-based-integration', label: 'C4 - Event-Based Integration', description: 'Event bus, router, dead-letter queue, event store, consumers' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The example's main.archify for a new model folder. When the folder keeps
|
||||||
|
* the example's name the file is used as written; otherwise id, name and the
|
||||||
|
* overview title follow the folder name and the description is kept.
|
||||||
|
*/
|
||||||
|
export function mainFromExample(mainText: string, folderName: string, example: ModelExample): string {
|
||||||
|
if (folderName === example.folder) return mainText;
|
||||||
|
const doc = parseDocument(mainText);
|
||||||
|
doc.setIn(['model', 'id'], modelIdFromName(folderName));
|
||||||
|
doc.setIn(['model', 'name'], titleFromName(folderName));
|
||||||
|
if (doc.hasIn(['meta', 'title'])) doc.setIn(['meta', 'title'], `${titleFromName(folderName)} — overview`);
|
||||||
|
return doc.toString({ lineWidth: 0, flowCollectionPadding: false });
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: System context
|
||||||
|
output: context.html
|
||||||
|
components:
|
||||||
|
- ref: customer
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: support
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: shop
|
||||||
|
pos: [280, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: psp
|
||||||
|
pos: [520, 60]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: email
|
||||||
|
pos: [520, 260]
|
||||||
|
size: [150, 60]
|
||||||
|
connections:
|
||||||
|
- { from: customer, to: shop, label: buys, variant: emphasis }
|
||||||
|
- { from: support, to: shop, label: manages orders }
|
||||||
|
- { from: shop, to: psp, label: charges, variant: security }
|
||||||
|
- { from: shop, to: email, label: sends mail }
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Containers
|
||||||
|
output: containers.html
|
||||||
|
components:
|
||||||
|
- ref: customer
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: web
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: api
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: cache
|
||||||
|
pos: [670, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db
|
||||||
|
pos: [670, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: queue
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: worker
|
||||||
|
pos: [670, 460]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Online Shop
|
||||||
|
wraps: [web, api, cache, db, queue, worker]
|
||||||
|
connections:
|
||||||
|
- { from: customer, to: web, label: HTTPS, variant: emphasis }
|
||||||
|
- { from: web, to: api, label: JSON }
|
||||||
|
- { from: api, to: cache, label: read }
|
||||||
|
- { from: api, to: db, label: SQL }
|
||||||
|
- { from: api, to: queue, label: enqueue, labelAt: [560, 290] }
|
||||||
|
- { from: queue, to: worker, label: poll }
|
||||||
|
- { from: worker, to: db, label: SQL, labelAt: [735, 420] }
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: API components
|
||||||
|
output: api.html
|
||||||
|
components:
|
||||||
|
- ref: web
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: auth
|
||||||
|
type: security
|
||||||
|
label: Auth
|
||||||
|
sublabel: sessions, OIDC
|
||||||
|
pos: [250, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: orders
|
||||||
|
type: backend
|
||||||
|
label: Orders
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: catalogue
|
||||||
|
type: backend
|
||||||
|
label: Catalogue
|
||||||
|
pos: [250, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: queue
|
||||||
|
pos: [460, 310]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: API
|
||||||
|
wraps: [auth, orders, catalogue]
|
||||||
|
connections:
|
||||||
|
- { from: web, to: auth, label: sign in, variant: security }
|
||||||
|
- { from: web, to: orders, label: place order }
|
||||||
|
- { from: web, to: catalogue, label: browse }
|
||||||
|
- { from: orders, to: db, label: SQL }
|
||||||
|
- { from: catalogue, to: db, label: SQL }
|
||||||
|
- { from: orders, to: queue, label: enqueue }
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
schema_version: 1
|
||||||
|
model:
|
||||||
|
id: e-shop
|
||||||
|
name: E-Shop
|
||||||
|
description: >
|
||||||
|
The web shop, its API and workers, and the stores behind them.
|
||||||
|
Context, containers and components, C4 style.
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: E-Shop — overview
|
||||||
|
output: main.html
|
||||||
|
components:
|
||||||
|
- ref: customer
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: shop
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: psp
|
||||||
|
pos: [480, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
connections:
|
||||||
|
- { from: customer, to: shop, label: buys, variant: emphasis }
|
||||||
|
- { from: shop, to: psp, label: charges }
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
web:
|
||||||
|
type: frontend
|
||||||
|
label: Web App
|
||||||
|
sublabel: React
|
||||||
|
brand: react
|
||||||
|
description: The shop as customers see it; talks only to the API.
|
||||||
|
api:
|
||||||
|
type: backend
|
||||||
|
label: API
|
||||||
|
sublabel: Node.js
|
||||||
|
brand: node-js
|
||||||
|
description: Owns authentication, orders and the catalogue.
|
||||||
|
diagram: 0300-components/api.archify
|
||||||
|
worker:
|
||||||
|
type: backend
|
||||||
|
label: Worker
|
||||||
|
sublabel: async jobs
|
||||||
|
description: Picks jobs from the queue; emails, invoices, exports.
|
||||||
|
queue:
|
||||||
|
type: messagebus
|
||||||
|
label: Queue
|
||||||
|
sublabel: SQS
|
||||||
|
description: Job queue between the API and the workers.
|
||||||
|
db:
|
||||||
|
type: database
|
||||||
|
label: Database
|
||||||
|
sublabel: PostgreSQL
|
||||||
|
brand: postgresql
|
||||||
|
description: Orders, customers and catalogue; the source of truth.
|
||||||
|
cache:
|
||||||
|
type: database
|
||||||
|
label: Cache
|
||||||
|
sublabel: Redis
|
||||||
|
brand: redis
|
||||||
|
description: Sessions and catalogue cache; nothing here is the source of truth.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
customer:
|
||||||
|
type: external
|
||||||
|
label: Customer
|
||||||
|
sublabel: web and mobile
|
||||||
|
description: Buys products; signs in with email or a social account.
|
||||||
|
support:
|
||||||
|
type: external
|
||||||
|
label: Support agent
|
||||||
|
sublabel: back office
|
||||||
|
description: Handles refunds and order problems in the admin console.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
shop:
|
||||||
|
type: cloud
|
||||||
|
label: Online Shop
|
||||||
|
sublabel: the system
|
||||||
|
description: Everything we build and run; web app, API, workers and stores.
|
||||||
|
diagram: 0200-containers/online-shop.archify
|
||||||
|
psp:
|
||||||
|
type: external
|
||||||
|
label: Payment provider
|
||||||
|
sublabel: Stripe
|
||||||
|
brand: stripe
|
||||||
|
description: Charges cards and pays out; we never see card numbers.
|
||||||
|
email:
|
||||||
|
type: external
|
||||||
|
label: Email service
|
||||||
|
sublabel: transactional mail
|
||||||
|
description: Sends order confirmations and password resets.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: System context
|
||||||
|
output: context.html
|
||||||
|
components:
|
||||||
|
- ref: crm
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: erp
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: platform
|
||||||
|
pos: [280, 160]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: warehouse
|
||||||
|
pos: [540, 60]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: ops
|
||||||
|
pos: [540, 260]
|
||||||
|
size: [150, 60]
|
||||||
|
connections:
|
||||||
|
- { from: crm, to: platform, label: customer events, variant: emphasis }
|
||||||
|
- { from: erp, to: platform, label: order events, variant: emphasis }
|
||||||
|
- { from: platform, to: warehouse, label: pick lists }
|
||||||
|
- { from: ops, to: platform, label: replays, variant: dashed }
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Containers
|
||||||
|
output: containers.html
|
||||||
|
components:
|
||||||
|
- ref: crm
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: erp
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: ingest
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [140, 60]
|
||||||
|
- ref: bus
|
||||||
|
pos: [470, 160]
|
||||||
|
size: [140, 60]
|
||||||
|
- ref: router
|
||||||
|
pos: [690, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: dlq
|
||||||
|
pos: [690, 320]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: store
|
||||||
|
pos: [920, 60]
|
||||||
|
size: [140, 60]
|
||||||
|
- ref: notifier
|
||||||
|
pos: [920, 160]
|
||||||
|
size: [140, 60]
|
||||||
|
- ref: analytics
|
||||||
|
pos: [920, 260]
|
||||||
|
size: [140, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Integration platform
|
||||||
|
wraps: [ingest, bus, router, dlq, store, notifier, analytics]
|
||||||
|
connections:
|
||||||
|
- { from: crm, to: ingest, label: HTTPS, variant: emphasis }
|
||||||
|
- { from: erp, to: ingest, label: HTTPS, variant: emphasis }
|
||||||
|
- { from: ingest, to: bus, label: publish }
|
||||||
|
- { from: bus, to: router, label: consume }
|
||||||
|
- { from: router, to: store, label: append }
|
||||||
|
- { from: router, to: notifier, label: notify }
|
||||||
|
- { from: router, to: analytics, label: flatten }
|
||||||
|
- { from: router, to: dlq, label: failed, variant: dashed, labelDy: 24 }
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Event router components
|
||||||
|
output: router.html
|
||||||
|
components:
|
||||||
|
- ref: bus
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [140, 60]
|
||||||
|
- id: validate
|
||||||
|
type: security
|
||||||
|
label: Validate
|
||||||
|
sublabel: schema check
|
||||||
|
pos: [270, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: enrich
|
||||||
|
type: backend
|
||||||
|
label: Enrich
|
||||||
|
sublabel: reference data
|
||||||
|
pos: [480, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: dispatch
|
||||||
|
type: backend
|
||||||
|
label: Dispatch
|
||||||
|
pos: [690, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: store
|
||||||
|
pos: [920, 60]
|
||||||
|
size: [140, 60]
|
||||||
|
- ref: notifier
|
||||||
|
pos: [920, 160]
|
||||||
|
size: [140, 60]
|
||||||
|
- ref: dlq
|
||||||
|
pos: [270, 320]
|
||||||
|
size: [150, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Event router
|
||||||
|
wraps: [validate, enrich, dispatch]
|
||||||
|
connections:
|
||||||
|
- { from: bus, to: validate, label: event }
|
||||||
|
- { from: validate, to: enrich, label: valid }
|
||||||
|
- { from: enrich, to: dispatch, label: enriched }
|
||||||
|
- { from: dispatch, to: store, label: append }
|
||||||
|
- { from: dispatch, to: notifier, label: notify }
|
||||||
|
- { from: validate, to: dlq, label: invalid, variant: dashed, labelDy: 24 }
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
schema_version: 1
|
||||||
|
model:
|
||||||
|
id: event-based-integration
|
||||||
|
name: Event-Based Integration
|
||||||
|
description: >
|
||||||
|
Systems that never call each other directly; events go through a bus,
|
||||||
|
a router validates and enriches them, consumers react. Context,
|
||||||
|
containers, the router's components and one flow, C4 style.
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Event-Based Integration — overview
|
||||||
|
output: main.html
|
||||||
|
components:
|
||||||
|
- ref: crm
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: erp
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: platform
|
||||||
|
pos: [280, 160]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: warehouse
|
||||||
|
pos: [540, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
connections:
|
||||||
|
- { from: crm, to: platform, label: customer events, variant: emphasis }
|
||||||
|
- { from: erp, to: platform, label: order events, variant: emphasis }
|
||||||
|
- { from: platform, to: warehouse, label: pick lists }
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
ingest:
|
||||||
|
type: backend
|
||||||
|
label: Ingest API
|
||||||
|
sublabel: HTTPS in, events out
|
||||||
|
description: Accepts events from systems that cannot publish to the bus themselves.
|
||||||
|
bus:
|
||||||
|
type: messagebus
|
||||||
|
label: Event bus
|
||||||
|
sublabel: Kafka
|
||||||
|
brand: apache-kafka
|
||||||
|
description: One topic per event type; retained for seven days.
|
||||||
|
router:
|
||||||
|
type: backend
|
||||||
|
label: Event router
|
||||||
|
sublabel: validate, enrich, dispatch
|
||||||
|
description: Checks each event against its schema, adds reference data, hands it to the consumers.
|
||||||
|
diagram: 0300-components/router.archify
|
||||||
|
dlq:
|
||||||
|
type: messagebus
|
||||||
|
label: Dead-letter queue
|
||||||
|
sublabel: failed events
|
||||||
|
description: Events the router could not process; replayed by operations.
|
||||||
|
store:
|
||||||
|
type: database
|
||||||
|
label: Event store
|
||||||
|
sublabel: PostgreSQL
|
||||||
|
brand: postgresql
|
||||||
|
description: Every accepted event, for audit and replay.
|
||||||
|
notifier:
|
||||||
|
type: backend
|
||||||
|
label: Notifier
|
||||||
|
sublabel: email, chat
|
||||||
|
description: Turns selected events into messages to people.
|
||||||
|
analytics:
|
||||||
|
type: database
|
||||||
|
label: Analytics
|
||||||
|
sublabel: warehouse tables
|
||||||
|
description: Flattened events for reporting.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
ops:
|
||||||
|
type: external
|
||||||
|
label: Operations
|
||||||
|
sublabel: on-call
|
||||||
|
description: Watches the dead-letter queue and replays failed events.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
platform:
|
||||||
|
type: cloud
|
||||||
|
label: Integration platform
|
||||||
|
sublabel: the system
|
||||||
|
description: Receives events from the business systems, routes them, keeps a record.
|
||||||
|
diagram: 0200-containers/platform.archify
|
||||||
|
crm:
|
||||||
|
type: external
|
||||||
|
label: CRM
|
||||||
|
sublabel: customers
|
||||||
|
description: Source of customer events; created, updated, merged.
|
||||||
|
erp:
|
||||||
|
type: external
|
||||||
|
label: ERP
|
||||||
|
sublabel: orders and stock
|
||||||
|
description: Source of order events and the target of stock updates.
|
||||||
|
warehouse:
|
||||||
|
type: external
|
||||||
|
label: Warehouse system
|
||||||
|
sublabel: fulfilment
|
||||||
|
description: Receives pick lists; reports shipments back as events.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: System context
|
||||||
|
output: context.html
|
||||||
|
components:
|
||||||
|
- ref: user
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: admin
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: webapp
|
||||||
|
pos: [280, 160]
|
||||||
|
size: [160, 60]
|
||||||
|
- ref: idp
|
||||||
|
pos: [530, 60]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: email
|
||||||
|
pos: [530, 260]
|
||||||
|
size: [150, 60]
|
||||||
|
connections:
|
||||||
|
- { from: user, to: webapp, label: uses, variant: emphasis }
|
||||||
|
- { from: admin, to: webapp, label: administers }
|
||||||
|
- { from: webapp, to: idp, label: signs in via, variant: security }
|
||||||
|
- { from: webapp, to: email, label: sends mail }
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Containers
|
||||||
|
output: containers.html
|
||||||
|
components:
|
||||||
|
- ref: user
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: spa
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: api
|
||||||
|
pos: [480, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: idp
|
||||||
|
pos: [480, 20]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: cache
|
||||||
|
pos: [690, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db
|
||||||
|
pos: [690, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: storage
|
||||||
|
pos: [690, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Web Application
|
||||||
|
wraps: [spa, api, cache, db, storage]
|
||||||
|
connections:
|
||||||
|
- { from: user, to: spa, label: HTTPS, variant: emphasis }
|
||||||
|
- { from: spa, to: api, label: JSON }
|
||||||
|
- { from: api, to: idp, label: verify token, variant: security, labelAt: [531, 110] }
|
||||||
|
- { from: api, to: cache, label: read }
|
||||||
|
- { from: api, to: db, label: SQL }
|
||||||
|
- { from: api, to: storage, label: files }
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: API components
|
||||||
|
output: api.html
|
||||||
|
components:
|
||||||
|
- ref: spa
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- id: auth
|
||||||
|
type: security
|
||||||
|
label: Auth
|
||||||
|
sublabel: token check
|
||||||
|
pos: [270, 40]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: accounts
|
||||||
|
type: backend
|
||||||
|
label: Accounts
|
||||||
|
pos: [270, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: content
|
||||||
|
type: backend
|
||||||
|
label: Content
|
||||||
|
pos: [270, 280]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db
|
||||||
|
pos: [500, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: storage
|
||||||
|
pos: [500, 280]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: API
|
||||||
|
wraps: [auth, accounts, content]
|
||||||
|
connections:
|
||||||
|
- { from: spa, to: auth, label: token, variant: security }
|
||||||
|
- { from: spa, to: accounts, label: profile }
|
||||||
|
- { from: spa, to: content, label: pages }
|
||||||
|
- { from: accounts, to: db, label: SQL }
|
||||||
|
- { from: content, to: db, label: SQL }
|
||||||
|
- { from: content, to: storage, label: files }
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
schema_version: 1
|
||||||
|
model:
|
||||||
|
id: standard-web-app
|
||||||
|
name: Standard Web App
|
||||||
|
description: >
|
||||||
|
A classic web application; a single-page app, an API, a database,
|
||||||
|
a cache and file storage, with sign-in through an identity provider.
|
||||||
|
Context, containers and the API's components, C4 style.
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Standard Web App — overview
|
||||||
|
output: main.html
|
||||||
|
components:
|
||||||
|
- ref: user
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: webapp
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [160, 60]
|
||||||
|
- ref: idp
|
||||||
|
pos: [490, 60]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: email
|
||||||
|
pos: [490, 260]
|
||||||
|
size: [150, 60]
|
||||||
|
connections:
|
||||||
|
- { from: user, to: webapp, label: uses, variant: emphasis }
|
||||||
|
- { from: webapp, to: idp, label: signs in via, variant: security }
|
||||||
|
- { from: webapp, to: email, label: sends mail }
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
spa:
|
||||||
|
type: frontend
|
||||||
|
label: Single-page app
|
||||||
|
sublabel: React
|
||||||
|
brand: react
|
||||||
|
description: The user interface; talks only to the API.
|
||||||
|
api:
|
||||||
|
type: backend
|
||||||
|
label: API
|
||||||
|
sublabel: Node.js
|
||||||
|
brand: node-js
|
||||||
|
description: Owns accounts, content and files; checks tokens from the identity provider.
|
||||||
|
diagram: 0300-components/api.archify
|
||||||
|
db:
|
||||||
|
type: database
|
||||||
|
label: Database
|
||||||
|
sublabel: PostgreSQL
|
||||||
|
brand: postgresql
|
||||||
|
description: Accounts and content; the source of truth.
|
||||||
|
cache:
|
||||||
|
type: database
|
||||||
|
label: Cache
|
||||||
|
sublabel: Redis
|
||||||
|
brand: redis
|
||||||
|
description: Sessions and hot content; safe to lose.
|
||||||
|
storage:
|
||||||
|
type: cloud
|
||||||
|
label: Object storage
|
||||||
|
sublabel: uploaded files
|
||||||
|
description: User uploads and generated exports.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
user:
|
||||||
|
type: external
|
||||||
|
label: User
|
||||||
|
sublabel: browser
|
||||||
|
description: Anyone with an account; uses the application from a browser.
|
||||||
|
admin:
|
||||||
|
type: external
|
||||||
|
label: Administrator
|
||||||
|
sublabel: back office
|
||||||
|
description: Manages users and content from the same application with extra rights.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
webapp:
|
||||||
|
type: cloud
|
||||||
|
label: Web Application
|
||||||
|
sublabel: the system
|
||||||
|
description: Everything we build and run; the single-page app, the API and its stores.
|
||||||
|
diagram: 0200-containers/web-app.archify
|
||||||
|
idp:
|
||||||
|
type: external
|
||||||
|
label: Identity provider
|
||||||
|
sublabel: OpenID Connect
|
||||||
|
description: Signs users in and issues tokens; we never store passwords.
|
||||||
|
email:
|
||||||
|
type: external
|
||||||
|
label: Email service
|
||||||
|
sublabel: transactional mail
|
||||||
|
description: Sends sign-up confirmations and notifications.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { parse } from 'yaml';
|
||||||
|
import { validate } from '../src/archify/cli';
|
||||||
|
import { MODEL_EXAMPLES, mainFromExample } from '../src/language/examples';
|
||||||
|
import { MAIN_FILE, REGISTRY_EXT, hasDiagramBody, mergeRegistries, parseRegistry, readModelMeta, resolveRefs } from '../src/model/format';
|
||||||
|
import { readDiagramSource } from '../src/sources/detect';
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..');
|
||||||
|
const examples = path.join(root, 'templates', 'models');
|
||||||
|
const runtime = {
|
||||||
|
cliPath: path.join(root, 'vendor', 'archify', 'bin', 'archify.mjs'),
|
||||||
|
nodePath: process.execPath,
|
||||||
|
runAsNode: false,
|
||||||
|
workDir: fs.mkdtempSync(path.join(os.tmpdir(), 'archify-ext-examples-')),
|
||||||
|
};
|
||||||
|
|
||||||
|
function walk(dir: string, rel = ''): string[] {
|
||||||
|
return fs.readdirSync(path.join(dir, rel), { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const next = rel ? `${rel}/${entry.name}` : entry.name;
|
||||||
|
return entry.isDirectory() ? walk(dir, next) : [next];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('every example folder is listed, every listed example exists, labels are the three C4 examples', () => {
|
||||||
|
const folders = fs.readdirSync(examples, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
||||||
|
assert.deepEqual(MODEL_EXAMPLES.map((e) => e.folder).sort(), folders);
|
||||||
|
assert.deepEqual(
|
||||||
|
MODEL_EXAMPLES.map((e) => e.label),
|
||||||
|
['C4 - Standard Web App', 'C4 - E-Shop', 'C4 - Event-Based Integration'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const example of MODEL_EXAMPLES) {
|
||||||
|
test(`example ${example.folder} is a complete model that validates`, async () => {
|
||||||
|
const dir = path.join(examples, example.folder);
|
||||||
|
const files = walk(dir);
|
||||||
|
const read = (rel: string) => fs.readFileSync(path.join(dir, rel), 'utf8');
|
||||||
|
|
||||||
|
// main.archify: model block with id/name/description and an overview diagram.
|
||||||
|
assert.ok(files.includes(MAIN_FILE));
|
||||||
|
const main = parse(read(MAIN_FILE));
|
||||||
|
const meta = readModelMeta(main);
|
||||||
|
assert.equal(meta?.id, example.folder.replace(/^c4-/, ''));
|
||||||
|
assert.ok(meta?.name && meta.description);
|
||||||
|
assert.ok(hasDiagramBody(main));
|
||||||
|
|
||||||
|
// Registry: at least people, systems and containers; no problems.
|
||||||
|
const registryFiles = files.filter((f) => f.endsWith(REGISTRY_EXT)).sort();
|
||||||
|
assert.ok(registryFiles.length >= 3, `registry files: ${registryFiles.join(', ')}`);
|
||||||
|
const registry = mergeRegistries(registryFiles.map((file) => ({ file, ...parseRegistry(read(file), file) })));
|
||||||
|
assert.deepEqual(registry.problems, []);
|
||||||
|
for (const element of registry.elements.values()) {
|
||||||
|
assert.ok(element.type && element.label && element.description, `${element.id} needs type, label and description`);
|
||||||
|
if (element.diagram) assert.ok(files.includes(element.diagram), `${element.id} links to missing ${element.diagram}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagrams: the three C4 levels, each resolving without problems and valid for Archify.
|
||||||
|
const diagrams = files.filter((f) => f.endsWith('.archify')).sort();
|
||||||
|
for (const level of ['0100-context', '0200-containers', '0300-components']) {
|
||||||
|
assert.ok(diagrams.some((d) => d.startsWith(`${level}/`)), `missing ${level}`);
|
||||||
|
}
|
||||||
|
const used = new Set<string>();
|
||||||
|
for (const file of diagrams) {
|
||||||
|
const source = readDiagramSource(read(file), file);
|
||||||
|
assert.ok(source, `${file} is not a diagram`);
|
||||||
|
const resolved = resolveRefs(source.diagram, { registry, fileName: file });
|
||||||
|
assert.deepEqual(resolved.problems, [], file);
|
||||||
|
for (const link of resolved.links.values()) assert.ok(files.includes(link), `${file} links to missing ${link}`);
|
||||||
|
for (const id of resolved.used) used.add(id);
|
||||||
|
const result = await validate(runtime, source.type, JSON.stringify(resolved.diagram));
|
||||||
|
assert.deepEqual(result.diagnostics.filter((d) => d.severity === 'error').map((d) => `${file}: ${d.code}: ${d.message}`), []);
|
||||||
|
assert.ok(result.ok, file);
|
||||||
|
}
|
||||||
|
// Every registry element is used by some diagram.
|
||||||
|
for (const id of registry.elements.keys()) assert.ok(used.has(id), `element ${id} is never used`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('mainFromExample keeps the example as written, or renames it after the folder', () => {
|
||||||
|
const example = MODEL_EXAMPLES[1];
|
||||||
|
const text = fs.readFileSync(path.join(examples, example.folder, MAIN_FILE), 'utf8');
|
||||||
|
assert.equal(mainFromExample(text, example.folder, example), text);
|
||||||
|
const renamed = parse(mainFromExample(text, 'shop two', example));
|
||||||
|
assert.equal(renamed.model.id, 'shop-two');
|
||||||
|
assert.equal(renamed.model.name, 'Shop two');
|
||||||
|
assert.equal(renamed.meta.title, 'Shop two — overview');
|
||||||
|
assert.equal(renamed.model.description, parse(text).model.description);
|
||||||
|
assert.deepEqual(renamed.components, parse(text).components);
|
||||||
|
});
|
||||||
@@ -340,4 +340,51 @@ export async function run(): Promise<void> {
|
|||||||
assert.match(content, /id: new-shop/);
|
assert.match(content, /id: new-shop/);
|
||||||
assert.match(content, /name: New shop/);
|
assert.match(content, /name: New shop/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await check('Archify | New Model… creates a model folder from an example and opens its page', async () => {
|
||||||
|
const commands = vscode.extensions.getExtension(EXTENSION_ID)?.packageJSON.contributes.commands as { command: string; title: string }[];
|
||||||
|
assert.equal(commands.find((item) => item.command === 'archify.newModel')?.title, 'Archify | New Model…');
|
||||||
|
const parent = workspaceFile('.');
|
||||||
|
const window = vscode.window as unknown as Record<string, unknown>;
|
||||||
|
const originalPick = window.showQuickPick;
|
||||||
|
const originalInput = window.showInputBox;
|
||||||
|
let labels: string[] = [];
|
||||||
|
let existingNameMessage: unknown;
|
||||||
|
window.showQuickPick = async (items: readonly { label: string }[]) => {
|
||||||
|
labels = (await items).map((item) => item.label);
|
||||||
|
return (await items).find((item) => item.label === 'C4 - E-Shop');
|
||||||
|
};
|
||||||
|
window.showInputBox = async (options: vscode.InputBoxOptions) => {
|
||||||
|
existingNameMessage = await options.validateInput?.('new-shop');
|
||||||
|
return 'shop-two';
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await vscode.commands.executeCommand('archify.newModel', parent);
|
||||||
|
} finally {
|
||||||
|
window.showQuickPick = originalPick;
|
||||||
|
window.showInputBox = originalInput;
|
||||||
|
}
|
||||||
|
assert.deepEqual(labels, ['C4 - Standard Web App', 'C4 - E-Shop', 'C4 - Event-Based Integration']);
|
||||||
|
assert.match(String(existingNameMessage), /already exists/);
|
||||||
|
const folder = vscode.Uri.joinPath(parent, 'shop-two');
|
||||||
|
const main = vscode.Uri.joinPath(folder, 'main.archify');
|
||||||
|
const content = new TextDecoder().decode(await vscode.workspace.fs.readFile(main));
|
||||||
|
assert.match(content, /id: shop-two/);
|
||||||
|
assert.match(content, /name: Shop two/);
|
||||||
|
assert.match(content, /title: Shop two — overview/);
|
||||||
|
const names = (await vscode.workspace.fs.readDirectory(folder)).map(([name]) => name).sort();
|
||||||
|
assert.deepEqual(names, ['0100-context', '0200-containers', '0300-components', 'main.archify', 'registry']);
|
||||||
|
const registry = (await vscode.workspace.fs.readDirectory(vscode.Uri.joinPath(folder, 'registry'))).map(([name]) => name).sort();
|
||||||
|
assert.deepEqual(registry, ['containers.archify-registry', 'people.archify-registry', 'systems.archify-registry']);
|
||||||
|
const tab = await waitFor('model page tab', () =>
|
||||||
|
vscode.window.tabGroups.all.flatMap((g) => g.tabs).find((t) => t.isActive && t.input instanceof vscode.TabInputCustom && t.input.uri.toString() === main.toString()),
|
||||||
|
);
|
||||||
|
assert.ok(tab);
|
||||||
|
// The new model is indexed: its diagrams resolve against the copied registry.
|
||||||
|
const containers = await vscode.workspace.openTextDocument(vscode.Uri.joinPath(folder, '0200-containers', 'online-shop.archify'));
|
||||||
|
await vscode.window.showTextDocument(containers);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
const errors = vscode.languages.getDiagnostics(containers.uri).filter((d) => d.severity === vscode.DiagnosticSeverity.Error);
|
||||||
|
assert.deepEqual(errors.map((d) => d.message), []);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user