validator.mjs
83 lines · archify/renderers/shared/validator.mjs at a406594
1import * as validators from './generated-validators.mjs';
2import { throwDiagnosticError } from './diagnostics.mjs';
4// "/nodes/3/label" reads much better as "/nodes/3 (id: "router") /label" for the
5// LLM fixing the JSON; resolve the nearest enclosing element's id or label.
6function annotatedPath(instancePath, data) {
7 if (!instancePath) return { path: '/', identity: null };
8 let node = data;
9 let hint = null;
10 for (const seg of instancePath.split('/').slice(1)) {
11 if (node == null || typeof node !== 'object') break;
12 node = node[/^\d+$/.test(seg) ? Number(seg) : seg];
13 if (node && typeof node === 'object' && !Array.isArray(node)) {
14 const tag = node.id ?? node.label;
15 if (tag != null) hint = String(tag);
16 }
17 }
18 return { path: instancePath, identity: hint };
19}
21function annotatePath(instancePath, data) {
22 const annotated = annotatedPath(instancePath, data);
23 return annotated.identity != null
24 ? `${annotated.path} (id/label: ${JSON.stringify(annotated.identity)})`
25 : annotated.path;
26}
28function formatErrors(errors, data) {
29 return errors.map((e) => {
30 const where = annotatePath(e.instancePath, data);
31 const detail = e.params && Object.keys(e.params).length
32 ? ' ' + JSON.stringify(e.params)
33 : '';
34 return ` ${where} ${e.message}${detail}`;
35 }).join('\n');
36}
38export function validateSchema(diagramType, data) {
39 const validate = validators[diagramType];
40 if (!validate) {
41 throw new Error(`validateSchema: unknown diagram type "${diagramType}"`);
42 }
43 if (!validate(data)) {
44 const diagnostics = validate.errors.map((error) => {
45 const annotated = annotatedPath(error.instancePath, data);
46 const subject = {
47 diagramType,
48 path: annotated.path,
49 ...(annotated.identity != null ? { identity: String(annotated.identity) } : {}),
50 };
51 const evidence = {
52 keyword: error.keyword,
53 expected: error.schema,
54 ...error.params,
55 };
56 const supportedFixes = {
57 additionalProperties: [`remove unsupported property ${JSON.stringify(error.params?.additionalProperty)}`],
58 required: [`add required property ${JSON.stringify(error.params?.missingProperty)}`],
59 type: [`use ${JSON.stringify(error.params?.type)} at ${annotated.path}`],
60 enum: [`choose one of the allowed values`],
61 pattern: [`match the required pattern ${JSON.stringify(error.params?.pattern)}`],
62 minimum: [`use a value ${error.params?.comparison || '>='} ${error.params?.limit}`],
63 maximum: [`use a value ${error.params?.comparison || '<='} ${error.params?.limit}`],
64 minItems: [`provide at least ${error.params?.limit} item(s)`],
65 maxItems: [`provide at most ${error.params?.limit} item(s)`],
66 minLength: [`provide at least ${error.params?.limit} character(s)`],
67 maxLength: [`provide at most ${error.params?.limit} character(s)`],
68 }[error.keyword] || [];
69 return {
70 code: `schema/${error.keyword}`,
71 severity: 'error',
72 message: `${annotatePath(error.instancePath, data)} ${error.message}`,
73 subject,
74 evidence,
75 supportedFixes,
76 };
77 });
78 throwDiagnosticError(
79 `${diagramType} schema validation failed:\n${formatErrors(validate.errors, data)}`,
80 diagnostics,
81 );
82 }
83}