// Minimal JSON-schema subset validator (node built-ins only). // Supports: type, required, enum, const, properties, items, minimum, oneOf, anyOf. A value that // matches no oneOf branch is reported against the branch its const fields pick, if exactly one. const typeOk = (t, v) => t === "array" ? Array.isArray(v) : t === "null" ? v === null : t === "integer" ? Number.isInteger(v) : t === "object" ? v !== null && typeof v === "object" && !Array.isArray(v) : typeof v === t; export function validate(schema, value, path = "$") { const errs = []; if (schema.type) { const types = [].concat(schema.type); if (!types.some((t) => typeOk(t, value))) return [`${path}: expected ${types.join("|")}`]; } if (schema.enum && !schema.enum.includes(value)) errs.push(`${path}: not in enum ${JSON.stringify(schema.enum)}`); if ("const" in schema && value !== schema.const) errs.push(`${path}: expected ${JSON.stringify(schema.const)}`); if (schema.minimum !== undefined && typeof value === "number" && value < schema.minimum) errs.push(`${path}: below minimum ${schema.minimum}`); if (typeOk("object", value)) { for (const k of schema.required || []) if (!(k in value)) errs.push(`${path}: missing "${k}"`); for (const [k, s] of Object.entries(schema.properties || {})) if (k in value) errs.push(...validate(s, value[k], `${path}.${k}`)); } if (Array.isArray(value) && schema.items) value.forEach((v, i) => errs.push(...validate(schema.items, v, `${path}[${i}]`))); if (schema.anyOf && !schema.anyOf.some((s) => validate(s, value, path).length === 0)) errs.push(`${path}: matches no anyOf branch`); if (schema.oneOf) { const n = schema.oneOf.filter((s) => validate(s, value, path).length === 0).length; // No branch matches: if the value's `const` fields pick out one branch (a clause's "kind", // an effect's "type"), say what is wrong with it; that is what the author meant. const meant = n === 0 ? schema.oneOf.filter((s) => discriminates(s, value)) : []; if (meant.length === 1) errs.push(...validate(meant[0], value, path)); else if (n !== 1) errs.push(`${path}: matches ${n} oneOf branches (need 1)`); } return errs; } /** Does `value` have every `const` field of the object schema `s`, and does `s` have one? */ function discriminates(s, value) { const consts = Object.entries(s.properties || {}).filter(([, p]) => "const" in p); return typeOk("object", value) && consts.length > 0 && consts.every(([k, p]) => value[k] === p.const); }