diff --git a/src/output-plugin.js b/src/output-plugin.js index 7db0c9a..036ac62 100644 --- a/src/output-plugin.js +++ b/src/output-plugin.js @@ -1,59 +1,97 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { EvaluationPlugin, ValidationContext } from "@hyperjump/json-schema/experimental" + * @import { EvaluationPlugin, Node, ValidationContext } from "@hyperjump/json-schema/experimental" + * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" * @import { NormalizedOutput } from "./index.js" */ +/** + * What evaluating a schema did. Each keyword's result includes the results of + * its subschemas so the subschemas can be described. + * + * @typedef {{ + * url: string; + * instance: JsonNode; + * valid: boolean; + * isBoolean: boolean; + * keywords: KeywordResult[]; + * }} SchemaResult + * + * @typedef {{ + * node: Node; + * valid: boolean; + * subschemas: SchemaResult[] | undefined; + * }} KeywordResult + */ + /** * @typedef {ValidationContext & { - * output: NormalizedOutput; - * subSchemaOutput?: NormalizedOutput[]; + * schemaResult: SchemaResult; + * subschemaResults?: SchemaResult[]; * }} ErrorsContext */ -/** @implements EvaluationPlugin */ +/** + * The output is only needed when there are errors, so evaluation records what + * happened and the normalized output is built from it when it's asked for. + * + * @implements EvaluationPlugin + */ export class JsonSchemaErrorsOutputPlugin { - constructor() { - /** @type NormalizedOutput */ - this.output = {}; - } + /** @type SchemaResult | undefined */ + #result; + /** @type NormalizedOutput | undefined */ + #output; - /** @type NonNullable["beforeSchema"]> */ - beforeSchema(_url, _instance, context) { - context.output = {}; + /** @type NormalizedOutput */ + get output() { + this.#output ??= this.#result ? toNormalizedOutput(this.#result) : {}; + return this.#output; } - /** @type NonNullable["beforeKeyword"]> */ - beforeKeyword(_node, _instance, context, schemaContext) { - context.output = schemaContext.output; + /** @type NonNullable["beforeSchema"]> */ + beforeSchema(url, instance, context) { + context.schemaResult = { url, instance, valid: true, isBoolean: typeof context.ast[url] === "boolean", keywords: [] }; } - // Each keyword's result includes the output of its subschemas so the - // subschemas can be described - /** @type NonNullable["afterKeyword"]> */ - afterKeyword(keywordNode, instance, context, valid, schemaContext) { - const [keywordUri, schemaLocation, value] = keywordNode; - - schemaContext.output[Instance.uri(instance)] ??= {}; - schemaContext.output[Instance.uri(instance)][keywordUri] ??= {}; - schemaContext.output[Instance.uri(instance)][keywordUri][schemaLocation] = context.subSchemaOutput - ? { valid, value, outputs: context.subSchemaOutput } - : { valid, value }; + afterKeyword(node, _instance, context, valid, schemaContext) { + schemaContext.schemaResult.keywords.push({ node, valid, subschemas: context.subschemaResults }); } /** @type NonNullable["afterSchema"]> */ - afterSchema(url, instance, context, valid) { - if (typeof context.ast[url] === "boolean" && !valid) { - context.output[Instance.uri(instance)] ??= {}; - context.output[Instance.uri(instance)]["https://json-schema.org/validation"] ??= {}; - context.output[Instance.uri(instance)]["https://json-schema.org/validation"][url] = { valid, value: false }; - } + afterSchema(_url, _instance, context, valid) { + context.schemaResult.valid = valid; - context.subSchemaOutput ??= []; - context.subSchemaOutput.push(context.output); + context.subschemaResults ??= []; + context.subschemaResults.push(context.schemaResult); - this.output = context.output; + this.#result = context.schemaResult; + this.#output = undefined; } } + +/** @type (schemaResult: SchemaResult) => NormalizedOutput */ +const toNormalizedOutput = ({ url, instance, valid, isBoolean, keywords }) => { + /** @type NormalizedOutput */ + const output = Object.create(null); + + if (isBoolean) { + if (!valid) { + output[Instance.uri(instance)] = { + "https://json-schema.org/validation": { [url]: { valid, value: false } } + }; + } + } else if (keywords.length > 0) { + const instanceOutput = output[Instance.uri(instance)] = Object.create(null); + for (const { node: [keywordUri, schemaLocation, value], valid, subschemas } of keywords) { + instanceOutput[keywordUri] ??= Object.create(null); + instanceOutput[keywordUri][schemaLocation] = subschemas + ? { valid, value, outputs: subschemas.map(toNormalizedOutput) } + : { valid, value }; + } + } + + return output; +};