import { DIAGRAM_SCHEMA, REGISTRY_SCHEMA } from './schemaCheck'; type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; /** * Schemas for completion and hover in the Source view. The editor's schema * library reads one draft-07 document, while ours are 2020-12 files that * refer to each other; so the referenced files are folded into `$defs` and * 2020-12 tuples become draft-07 ones. Problems never come from these * copies (the host checks with the originals), only suggestions. */ export class EditorSchemas { private readonly cache = new Map(); constructor(private readonly schemas: Record) {} /** The schema for a file kind; for diagrams the one of its diagram_type. */ forSource(kind: 'diagram' | 'registry' | 'decision', text: string): { key: string; schema: object } | undefined { if (kind === 'decision') return undefined; if (kind === 'registry') return this.get(REGISTRY_SCHEMA); const type = /^diagram_type:\s*["']?([a-z]+)/m.exec(text)?.[1]; const name = type && this.schemas[`${type}.schema.json`] ? `${type}.schema.json` : DIAGRAM_SCHEMA; return this.get(name); } private get(name: string): { key: string; schema: object } | undefined { if (!this.schemas[name]) return undefined; if (!this.cache.has(name)) this.cache.set(name, bundleSchema(this.schemas, name)); return { key: name, schema: this.cache.get(name)! }; } } const defKey = (file: string) => `file:${file}`; /** One self-contained draft-07 shaped schema for `root` and every file it refers to. */ export function bundleSchema(schemas: Record, root: string): object { const files = new Set([root]); const queue = [root]; while (queue.length) { const file = queue.shift()!; for (const ref of refsOf(schemas[file] as Json)) { const target = ref.split('#')[0]; if (target && !files.has(target) && schemas[target]) { files.add(target); queue.push(target); } } } const base = (file: string) => (file === root ? '#' : `#/$defs/${defKey(file)}`); const rewrite = (node: Json, file: string): Json => { if (Array.isArray(node)) return node.map((item) => rewrite(item, file)); if (!node || typeof node !== 'object') return node; const out: { [key: string]: Json } = {}; for (const [key, value] of Object.entries(node)) { if (key === '$ref' && typeof value === 'string') { const [target, pointer = ''] = value.split('#'); out.$ref = `${base(target || file)}${pointer}`; } else if (key !== '$schema' && key !== '$id') { out[key] = rewrite(value, file); } } if (out.prefixItems) { if (out.items === false) out.additionalItems = false; else if (out.items !== undefined) out.additionalItems = out.items; out.items = out.prefixItems; delete out.prefixItems; } return out; }; const bundled = rewrite(schemas[root] as Json, root) as { [key: string]: Json }; const defs = { ...((bundled.$defs as { [key: string]: Json } | undefined) ?? {}) }; for (const file of files) if (file !== root) defs[defKey(file)] = rewrite(schemas[file] as Json, file); return { ...bundled, $defs: defs }; } function refsOf(node: Json, out: string[] = []): string[] { if (Array.isArray(node)) node.forEach((item) => refsOf(item, out)); else if (node && typeof node === 'object') { for (const [key, value] of Object.entries(node)) { if (key === '$ref' && typeof value === 'string') out.push(value); else refsOf(value, out); } } return out; }