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);
|
||||
});
|
||||
Reference in New Issue
Block a user