import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { Ajv2020, type ErrorObject, type ValidateFunction } from "ajv/dist/2020.js"; import { parseStrictJson } from "./canonical.js"; const contractRoot = resolve( dirname(fileURLToPath(import.meta.url)), "../../../src/simplecadapi/scene/contracts/schemas", ); const ajv = new Ajv2020({ strict: true, allErrors: true }); function declareRequiredProperties(value: unknown): void { if (value === null || typeof value !== "object") return; const schema = value as Record; if ( schema.type === undefined && (schema.properties !== undefined || schema.required !== undefined) ) { schema.type = "object"; } if (Array.isArray(schema.required) && schema.required.every((item) => typeof item === "string")) { const properties = schema.properties !== null && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? (schema.properties as Record) : {}; for (const name of schema.required) properties[name] ??= {}; schema.properties = properties; } for (const key of ["properties", "$defs", "patternProperties", "dependentSchemas"]) { const schemas = schema[key]; if (schemas !== null && typeof schemas === "object" && !Array.isArray(schemas)) { Object.values(schemas).forEach(declareRequiredProperties); } } for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) { const schemas = schema[key]; if (Array.isArray(schemas)) schemas.forEach(declareRequiredProperties); } for (const key of [ "not", "if", "then", "else", "items", "contains", "additionalProperties", "unevaluatedProperties", "propertyNames", ]) { declareRequiredProperties(schema[key]); } } function load(name: string): ValidateFunction { const schema = parseStrictJson(readFileSync(resolve(contractRoot, name))); // Ajv strictRequired expects declarations in the same nested subschema. The // contract uses valid required-only guards, so add behavior-neutral schemas. declareRequiredProperties(schema); return ajv.compile(schema as object); } export type SchemaArtifact = "scene" | "entities" | "presentation" | "connector_binding" | "normalized_product"; const schemaFiles: Record = { scene: "scene-1.0.schema.json", entities: "entities-1.0.schema.json", presentation: "presentation-1.0.schema.json", connector_binding: "connector-binding-1.0.schema.json", normalized_product: "normalized-product-1.schema.json", }; const schemaDocuments = Object.fromEntries( Object.entries(schemaFiles).map(([artifact, name]) => [artifact, parseStrictJson(readFileSync(resolve(contractRoot, name)))]), ) as Record>; const pointerValidators = new Map(); export function validateSchemaPointer(artifact: SchemaArtifact, schemaPointer: string, value: unknown): boolean { const key = `${artifact}:${schemaPointer}`; let validate = pointerValidators.get(key); if (validate === undefined) { const root = structuredClone(schemaDocuments[artifact]); delete root.$id; const wrapper = schemaPointer === "#" ? root : { $schema: root.$schema, $ref: schemaPointer, $defs: root.$defs }; declareRequiredProperties(wrapper); validate = ajv.compile(wrapper); pointerValidators.set(key, validate); } return Boolean(validate(value)); } export const validateSceneSchema = load("scene-1.0.schema.json"); export const validateEntitiesSchema = load("entities-1.0.schema.json"); export const validatePresentationSchema = load("presentation-1.0.schema.json"); export const validateConnectorBindingSchema = load("connector-binding-1.0.schema.json"); export const validateNormalizedProductSchema = load("normalized-product-1.schema.json"); export function schemaErrors(validate: ValidateFunction, value: unknown): ErrorObject[] { validate(value); return validate.errors === null || validate.errors === undefined ? [] : [...validate.errors]; }