diff --git a/tools/conformance/attach-plugin-witness.mjs b/tools/conformance/attach-plugin-witness.mjs index 58faec0..c924a34 100644 --- a/tools/conformance/attach-plugin-witness.mjs +++ b/tools/conformance/attach-plugin-witness.mjs @@ -1,7 +1,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { validateSchemaSubset } from "../../runtimes/incubating/test-support/schema-subset.mjs"; +import { validateSchemaSubset } from "../lib/schema-subset.mjs"; const [witnessArgument, exportArgument] = process.argv.slice(2); if (!witnessArgument || !exportArgument) { @@ -12,8 +12,9 @@ const witnessPath = path.resolve(witnessArgument); const exportDirectory = path.resolve(exportArgument); const indexPath = path.join(exportDirectory, "index.json"); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const kernelRoot = path.resolve(process.env.EVE_KERNEL_ROOT || "E:\\Projects\\Eve"); const witness = parseJson(await readFile(witnessPath, "utf8")); -const schema = parseJson(await readFile(path.join(repoRoot, "schemas/gamecult.eve.plugin_witness.v1.schema.json"), "utf8")); +const schema = parseJson(await readFile(path.join(kernelRoot, "schemas/gamecult.eve.plugin_witness.v1.schema.json"), "utf8")); const schemaErrors = validateSchemaSubset(schema, witness); if (schemaErrors.length) throw new Error(`Plugin witness schema errors: ${schemaErrors.join("; ")}`); diff --git a/tools/conformance/attach-runtime-witness.mjs b/tools/conformance/attach-runtime-witness.mjs index c42adad..e8c0a40 100644 --- a/tools/conformance/attach-runtime-witness.mjs +++ b/tools/conformance/attach-runtime-witness.mjs @@ -1,7 +1,7 @@ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { validateSchemaSubset } from "../../runtimes/incubating/test-support/schema-subset.mjs"; +import { validateSchemaSubset } from "../lib/schema-subset.mjs"; const [witnessArgument, exportArgument] = process.argv.slice(2); if (!witnessArgument || !exportArgument) { @@ -15,7 +15,8 @@ const indexPath = path.join(exportDirectory, "index.json"); const witness = parseJson(await readFile(witnessPath, "utf8")); validateWitness(witness); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); -const witnessSchema = parseJson(await readFile(path.join(repoRoot, "schemas/gamecult.eve.runtime_witness.v1.schema.json"), "utf8")); +const kernelRoot = path.resolve(process.env.EVE_KERNEL_ROOT || "E:\\Projects\\Eve"); +const witnessSchema = parseJson(await readFile(path.join(kernelRoot, "schemas/gamecult.eve.runtime_witness.v1.schema.json"), "utf8")); const schemaErrors = validateSchemaSubset(witnessSchema, witness); if (schemaErrors.length) throw new Error(`Runtime witness schema errors: ${schemaErrors.join("; ")}`); diff --git a/tools/lib/schema-subset.mjs b/tools/lib/schema-subset.mjs new file mode 100644 index 0000000..5437df8 --- /dev/null +++ b/tools/lib/schema-subset.mjs @@ -0,0 +1,71 @@ +export function validateSchemaSubset(schema, value, pointer = "$", rootSchema = schema) { + const errors = []; + if (!schema || typeof schema !== "object") return errors; + + if (schema.$ref) { + const resolved = resolveSchemaReference(rootSchema, schema.$ref); + return resolved + ? validateSchemaSubset(resolved, value, pointer, rootSchema) + : [`${pointer}:unresolved-ref:${schema.$ref}`]; + } + + if (schema.const !== undefined && value !== schema.const) { + errors.push(`${pointer}:const:${schema.const}`); + } + if (schema.enum && !schema.enum.includes(value)) { + errors.push(`${pointer}:enum:${schema.enum.join("|")}`); + } + if (schema.type && !matchesSchemaType(value, schema.type)) { + errors.push(`${pointer}:type:${schema.type}`); + return errors; + } + if (schema.minLength !== undefined && typeof value === "string" && value.length < schema.minLength) { + errors.push(`${pointer}:minLength:${schema.minLength}`); + } + if (schema.minItems !== undefined && Array.isArray(value) && value.length < schema.minItems) { + errors.push(`${pointer}:minItems:${schema.minItems}`); + } + if (schema.required && value && typeof value === "object" && !Array.isArray(value)) { + for (const key of schema.required) { + if (value[key] === undefined) errors.push(`${pointer}.${key}:required`); + } + } + if (schema.properties && value && typeof value === "object" && !Array.isArray(value)) { + for (const [key, childSchema] of Object.entries(schema.properties)) { + if (value[key] !== undefined) { + errors.push(...validateSchemaSubset(childSchema, value[key], `${pointer}.${key}`, rootSchema)); + } + } + } + if (schema.items && Array.isArray(value)) { + value.forEach((item, index) => { + errors.push(...validateSchemaSubset(schema.items, item, `${pointer}[${index}]`, rootSchema)); + }); + } + return errors; +} + +function resolveSchemaReference(rootSchema, reference) { + if (!reference.startsWith("#/")) return null; + return reference.slice(2).split("/").reduce((current, segment) => { + if (!current || typeof current !== "object") return null; + return current[segment.replaceAll("~1", "/").replaceAll("~0", "~")]; + }, rootSchema); +} + +function matchesSchemaType(value, type) { + switch (type) { + case "array": + return Array.isArray(value); + case "object": + return value !== null && typeof value === "object" && !Array.isArray(value); + case "string": + return typeof value === "string"; + case "number": + return typeof value === "number"; + case "boolean": + return typeof value === "boolean"; + default: + return true; + } +}