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:
@@ -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, /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