import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { test } from 'node:test'; import { DIAGRAM_SCHEMA, REGISTRY_SCHEMA, SchemaChecker, readSchemas, readablePointer } from '../src/language/schemaCheck'; const root = path.resolve(__dirname, '..', '..'); const checker = new SchemaChecker(readSchemas(path.join(root, 'schemas'))); function walk(dir: string, out: string[] = []): string[] { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) walk(full, out); else if (/\.archify(-registry)?$/.test(entry.name)) out.push(full); } return out; } const schemaFor = (file: string) => (file.endsWith('-registry') ? REGISTRY_SCHEMA : DIAGRAM_SCHEMA); test('every shipped .archify and registry file passes the schema check', () => { const files = ['examples', 'templates', path.join('test', 'fixtures')].flatMap((dir) => walk(path.join(root, dir))); assert.ok(files.length > 30, `found ${files.length} files`); for (const file of files) { const problems = checker.check(fs.readFileSync(file, 'utf8'), schemaFor(file)); assert.deepEqual(problems.map((p) => p.message), [], path.relative(root, file)); } }); function problemsAfter(file: string, from: string, to: string) { const original = fs.readFileSync(path.join(root, file), 'utf8'); assert.ok(original.includes(from), `${file} contains ${from}`); const text = original.replace(from, to); return checker.check(text, schemaFor(file)).map((p) => ({ ...p, text: text.slice(p.from, p.to) })); } const DIAGRAM = 'templates/models/c4-standard-web-app/0200-containers/web-app.archify'; const REGISTRY = 'templates/models/c4-standard-web-app/registry/containers.archify-registry'; test('an unknown key is placed on the key', () => { const [problem, ...rest] = problemsAfter(DIAGRAM, 'label: SQL', 'lable: SQL'); assert.equal(rest.length, 0); assert.equal(problem.text, 'lable'); assert.match(problem.message, /^Unknown key "lable" in connections\[\d+\]\.$/); }); test('a point with one number is placed on the whole flow list', () => { const problems = problemsAfter(DIAGRAM, 'pos: [690, 160]', 'pos: [690]'); assert.equal(problems.length, 1); assert.equal(problems[0].text, '[690]'); assert.match(problems[0].message, /pos needs at least 2 items\.$/); }); test('registry: a misspelt type key, a bad type, a bad id', () => { const typo = problemsAfter(REGISTRY, ' type: database', ' typ: database'); assert.deepEqual(typo.map((p) => p.text).sort(), ['db', 'typ']); assert.ok(typo.some((p) => p.text === 'typ' && /Unknown key "typ"/.test(p.message))); assert.ok(typo.some((p) => p.text === 'db' && /"type" is required in elements\.db/.test(p.message))); const bad = problemsAfter(REGISTRY, ' type: cloud', ' type: bucket'); assert.equal(bad.length, 1); assert.equal(bad[0].text, 'bucket'); assert.match(bad[0].message, /must be one of: frontend, backend, database, cloud, security, messagebus, external\.$/); }); test('a YAML syntax error is reported once, as YAML', () => { const problems = problemsAfter(REGISTRY, 'label: API', 'label: [API'); assert.equal(problems.length, 1); assert.equal(problems[0].kind, 'yaml'); assert.match(problems[0].message, /^YAML: /); }); test('pointers read like paths', () => { assert.equal(readablePointer('/connections/4/label'), 'connections[4].label'); assert.equal(readablePointer('/elements/db'), 'elements.db'); assert.equal(readablePointer(''), 'the file'); });