Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 72 additions & 34 deletions src/output-plugin.js
Original file line number Diff line number Diff line change
@@ -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<unknown>;
* valid: boolean;
* subschemas: SchemaResult[] | undefined;
* }} KeywordResult
*/

/**
* @typedef {ValidationContext & {
* output: NormalizedOutput;
* subSchemaOutput?: NormalizedOutput[];
* schemaResult: SchemaResult;
* subschemaResults?: SchemaResult[];
* }} ErrorsContext
*/

/** @implements EvaluationPlugin<ErrorsContext> */
/**
* 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<ErrorsContext>
*/
export class JsonSchemaErrorsOutputPlugin {
constructor() {
/** @type NormalizedOutput */
this.output = {};
}
/** @type SchemaResult | undefined */
#result;
/** @type NormalizedOutput | undefined */
#output;

/** @type NonNullable<EvaluationPlugin<ErrorsContext>["beforeSchema"]> */
beforeSchema(_url, _instance, context) {
context.output = {};
/** @type NormalizedOutput */
get output() {
this.#output ??= this.#result ? toNormalizedOutput(this.#result) : {};
return this.#output;
}

/** @type NonNullable<EvaluationPlugin<ErrorsContext>["beforeKeyword"]> */
beforeKeyword(_node, _instance, context, schemaContext) {
context.output = schemaContext.output;
/** @type NonNullable<EvaluationPlugin<ErrorsContext>["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<EvaluationPlugin<ErrorsContext>["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<EvaluationPlugin<ErrorsContext>["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;
};
Loading