From f7d6a0d65e34f75271d2ec58d2ee3570211721de Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Mon, 5 Oct 2026 12:31:03 -0700 Subject: [PATCH 1/6] Flatten simple applicator results when they're used The results of a simple applicator's subschemas were merged into the results of the parent schema when the output was built. Descriptions leave out the results of conditional keywords, so they were found by matching instance location, keyword, and keyword location and removed. A definition referenced directly and also by a conditional subschema, such as a base definition that an admin definition extends, has the same results through both paths, so its unconditional requirements were dropped from descriptions whenever the condition held. Now the output keeps results with the applicator that produced them and they're flattened when they're used. getErrors flattens every simple applicator. getSuccesses leaves out the results of conditional keywords. flattenOutput is exported for handlers that inspect a subschema's results directly, like the anyOf and oneOf filters. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 +- src/error-handlers/anyOf.js | 15 +- src/error-handlers/dependentSchemas.js | 2 +- src/error-handlers/ifThenElse.js | 2 +- src/error-handlers/oneOf.js | 15 +- src/index.d.ts | 15 +- src/index.js | 1 + src/json-schema-errors.js | 113 +++++-------- src/output-plugin.js | 23 +-- src/output-plugin.test.ts | 225 ------------------------- src/test-suite/tests/not.json | 72 ++++++++ 11 files changed, 156 insertions(+), 330 deletions(-) diff --git a/README.md b/README.md index cc77444..370dc41 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,8 @@ Simple applicator keywords that just evaluate subschemas and don't make any assertions of their own don't need an error handler, only a normalization handler. Whether a keyword is a simple applicator comes from the `simpleApplicator` property of its `@hyperjump/json-schema` keyword definition. -The results of its subschemas are merged into the results of the parent schema. +The results of its subschemas are flattened into the results of the parent +schema before they're passed to error handlers. For example, support for the `allOf` keyword could look like the following. ```TypeScript diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index 9f97a7a..f4c68f1 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -1,6 +1,6 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import * as Pact from "@hyperjump/pact"; -import { allTrue, getErrors, getSuccesses, someTrue } from "../json-schema-errors.js"; +import { allTrue, flattenOutput, getErrors, getSuccesses, someTrue } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject, InstanceOutput } from "../index.d.ts" @@ -25,29 +25,32 @@ const anyOfErrorHandler = { Pact.collectArray ); + const alternativeResults = anyOf.map(flattenOutput); const discriminators = propertyLocations.filter((propertyLocation) => { - return anyOf.some((alternative) => isPassingProperty(alternative[propertyLocation])); + return alternativeResults.some((results) => isPassingProperty(results[propertyLocation])); }); /** @type ErrorObject[][] */ const alternatives = []; const instanceLocation = Instance.uri(instance); - for (const alternative of anyOf) { + for (const [index, alternative] of anyOf.entries()) { + const results = alternativeResults[index]; + // Filter alternatives whose declared type doesn't match the instance type - const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; + const typeResults = results[instanceLocation]?.["https://json-schema.org/keyword/type"]; if (typeResults && !Object.values(typeResults).every(({ valid }) => valid)) { continue; } if (Instance.typeOf(instance) === "object") { // Filter alternative if it has no declared properties in common with the instance - if (!propertyLocations.some((propertyLocation) => propertyLocation in alternative)) { + if (!propertyLocations.some((propertyLocation) => propertyLocation in results)) { continue; } // Filter alternative if it has failing properties that are declared and passing in another alternative - if (discriminators.some((propertyLocation) => !isPassingProperty(alternative[propertyLocation]))) { + if (discriminators.some((propertyLocation) => !isPassingProperty(results[propertyLocation]))) { continue; } } diff --git a/src/error-handlers/dependentSchemas.js b/src/error-handlers/dependentSchemas.js index 3b52721..ec1d0b4 100644 --- a/src/error-handlers/dependentSchemas.js +++ b/src/error-handlers/dependentSchemas.js @@ -13,7 +13,7 @@ import { /** @type ErrorHandler */ const dependentSchemasErrorHandler = { - // Failures in dependent schemas are merged into the parent schema's results, + // Failures in dependent schemas are flattened into the parent schema's results, // so they're reported by the handlers for the keywords that failed success: (normalizedOutput, instance, localization, context) => { diff --git a/src/error-handlers/ifThenElse.js b/src/error-handlers/ifThenElse.js index 22a1cad..8d6a78f 100644 --- a/src/error-handlers/ifThenElse.js +++ b/src/error-handlers/ifThenElse.js @@ -14,7 +14,7 @@ import { /** @type ErrorHandler */ const ifThenElseErrorHandler = { - // Failures in 'then' and 'else' are merged into the parent schema's results, + // Failures in 'then' and 'else' are flattened into the parent schema's results, // so they're reported by the handlers for the keywords that failed success: (normalizedOutput, instance, localization, context) => { diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index 5ce7cf0..6bfcc82 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -1,6 +1,6 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import * as Pact from "@hyperjump/pact"; -import { allTrue, allowsAnyValue, countTrue, getCompiledKeywordValue, getErrors, getSuccesses, isPassing, limitItems, limitOptions, someTrue } from "../json-schema-errors.js"; +import { allowsAnyValue, allTrue, countTrue, flattenOutput, getCompiledKeywordValue, getErrors, getSuccesses, isPassing, limitItems, limitOptions, someTrue } from "../json-schema-errors.js"; /** * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" @@ -35,28 +35,31 @@ const oneOfErrorHandler = { Pact.collectArray ); + const alternativeResults = oneOf.map(flattenOutput); const discriminators = propertyLocations.filter((propertyLocation) => { - return oneOf.some((alternative) => isPassingProperty(alternative[propertyLocation])); + return alternativeResults.some((results) => isPassingProperty(results[propertyLocation])); }); const alternatives = []; const instanceLocation = Instance.uri(instance); - for (const alternative of oneOf) { + for (const [index, alternative] of oneOf.entries()) { + const results = alternativeResults[index]; + // Filter alternatives whose declared type doesn't match the instance type - const typeResults = alternative[instanceLocation]?.["https://json-schema.org/keyword/type"]; + const typeResults = results[instanceLocation]?.["https://json-schema.org/keyword/type"]; if (typeResults && !Object.values(typeResults).every(({ valid }) => valid)) { continue; } if (Instance.typeOf(instance) === "object") { // Filter alternative if it has no declared properties in common with the instance - if (!propertyLocations.some((propertyLocation) => propertyLocation in alternative)) { + if (!propertyLocations.some((propertyLocation) => propertyLocation in results)) { continue; } // Filter alternative if it has failing properties that are declared and passing in another alternative - if (discriminators.some((propertyLocation) => !isPassingProperty(alternative[propertyLocation]))) { + if (discriminators.some((propertyLocation) => !isPassingProperty(results[propertyLocation]))) { continue; } } diff --git a/src/index.d.ts b/src/index.d.ts index 53cbc20..27ff8a2 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -101,7 +101,7 @@ export type NormalizationHandler boolean | undefined; }; +/** + * The results of a simple applicator's subschemas are results of its parent + * schema, but they're kept with the applicator in its `outputs`. `getErrors` + * and `getSuccesses` flatten them for error handlers. Use this to get the + * flattened results of a subschema's output, such as an alternative of an + * applicator, to inspect them directly. + */ +export const flattenOutput: (normalizedOutput: NormalizedOutput) => NormalizedOutput; + /** * Converts the normalized error format to human readable errors. It's used to * build errors in applicator error handlers. diff --git a/src/index.js b/src/index.js index 47e4c6f..e0e0119 100644 --- a/src/index.js +++ b/src/index.js @@ -187,6 +187,7 @@ setErrorHandler("https://hyperjump.io/error-handler/unknown", unknownErrorHandle export { evaluateSchema, + flattenOutput, getErrors, getSuccesses, jsonSchemaErrors, diff --git a/src/json-schema-errors.js b/src/json-schema-errors.js index 90ab9de..b7aab9b 100644 --- a/src/json-schema-errors.js +++ b/src/json-schema-errors.js @@ -170,12 +170,6 @@ export const evaluateSchema = (schemaLocation, instance, context) => { valid = false; } - if (validationKeyword.simpleApplicator) { - for (const suboutput of keywordOutput ?? []) { - mergeOutput(output, suboutput); - } - } - output[instanceLocation] ??= {}; output[instanceLocation][normalizedKeywordUri] ??= {}; output[instanceLocation][normalizedKeywordUri][keywordLocation] = keywordOutput @@ -405,18 +399,6 @@ const relocate = (errorObject, from, to) => { return relocated; }; -/** @type (a: API.NormalizedOutput, b: API.NormalizedOutput) => void */ -const mergeOutput = (a, b) => { - for (const instanceLocation in b) { - a[instanceLocation] ??= {}; - for (const keywordUri in b[instanceLocation]) { - a[instanceLocation][keywordUri] ??= {}; - - Object.assign(a[instanceLocation][keywordUri], b[instanceLocation][keywordUri]); - } - } -}; - /** @type Record */ const errorHandlers = {}; @@ -435,10 +417,11 @@ export const getErrors = (normalizedErrors, rootInstance, localization, context) /** @type API.ErrorObject[] */ const errors = []; - for (const instanceLocation in normalizedErrors) { + const reportedOutput = flattenOutput(normalizedErrors); + for (const instanceLocation in reportedOutput) { const instance = /** @type JsonNode */ (Instance.get(instanceLocation, rootInstance)); for (const errorHandlerUri in errorHandlers) { - const errorObjects = errorHandlers[errorHandlerUri].error?.(normalizedErrors[instanceLocation], instance, localization, context) ?? []; + const errorObjects = errorHandlers[errorHandlerUri].error?.(reportedOutput[instanceLocation], instance, localization, context) ?? []; errors.push(...errorObjects); } } @@ -446,53 +429,32 @@ export const getErrors = (normalizedErrors, rootInstance, localization, context) return errors; }; +/** @type API.flattenOutput */ +export const flattenOutput = (normalizedOutput) => flatten(normalizedOutput, isApplied); + /** - * The results of a conditional keyword's subschemas are merged into the results - * of its parent schema like any simple applicator, but they only apply when the - * condition holds. The conditional keyword's handler describes them along with - * the condition, so they aren't described again as requirements of the parent. + * The results of a simple applicator's subschemas are results of its parent + * schema. The results are kept with the applicator that produced them and + * flattened into the parent's results when they're used. `shouldFlatten` + * decides which simple applicators' results are included. * - * @type (normalizedOutput: API.NormalizedOutput) => API.NormalizedOutput + * @type (normalizedOutput: API.NormalizedOutput, shouldFlatten: (keywordUri: string) => boolean, result?: API.NormalizedOutput) => API.NormalizedOutput */ -const withoutConditionalResults = (normalizedOutput) => { - /** @type Set */ - const conditionalResults = new Set(); +const flatten = (normalizedOutput, shouldFlatten, result = {}) => { for (const instanceLocation in normalizedOutput) { for (const keywordUri in normalizedOutput[instanceLocation]) { - if (!normalizationHandlers[toAbsoluteIri(keywordUri)]?.conditional) { - continue; - } - + const isFlattened = getKeyword(keywordUri)?.simpleApplicator && shouldFlatten(keywordUri); for (const keywordLocation in normalizedOutput[instanceLocation][keywordUri]) { - for (const output of normalizedOutput[instanceLocation][keywordUri][keywordLocation].outputs ?? []) { - for (const subInstanceLocation in output) { - for (const subKeywordUri in output[subInstanceLocation]) { - for (const subKeywordLocation in output[subInstanceLocation][subKeywordUri]) { - conditionalResults.add(resultKey(subInstanceLocation, subKeywordUri, subKeywordLocation)); - } - } + const keywordOutput = normalizedOutput[instanceLocation][keywordUri][keywordLocation]; + if (isFlattened) { + for (const output of keywordOutput.outputs ?? []) { + flatten(output, shouldFlatten, result); } } - } - } - } - - if (conditionalResults.size === 0) { - return normalizedOutput; - } - - /** @type API.NormalizedOutput */ - const result = {}; - for (const instanceLocation in normalizedOutput) { - for (const keywordUri in normalizedOutput[instanceLocation]) { - for (const keywordLocation in normalizedOutput[instanceLocation][keywordUri]) { - if (conditionalResults.has(resultKey(instanceLocation, keywordUri, keywordLocation))) { - continue; - } result[instanceLocation] ??= {}; result[instanceLocation][keywordUri] ??= {}; - result[instanceLocation][keywordUri][keywordLocation] = normalizedOutput[instanceLocation][keywordUri][keywordLocation]; + result[instanceLocation][keywordUri][keywordLocation] = keywordOutput; } } } @@ -500,8 +462,23 @@ const withoutConditionalResults = (normalizedOutput) => { return result; }; -/** @type (instanceLocation: string, keywordUri: string, keywordLocation: string) => string */ -const resultKey = (instanceLocation, keywordUri, keywordLocation) => JSON.stringify([instanceLocation, keywordUri, keywordLocation]); +/** + * The results of every simple applicator's subschemas apply to the parent + * schema when they were evaluated, including conditional ones, because a + * conditional subschema is only evaluated if its condition holds. + * + * @type (keywordUri: string) => boolean + */ +const isApplied = () => true; + +/** + * A conditional keyword's subschemas only apply when the condition holds. The + * conditional keyword's handler describes them along with the condition, so + * they aren't described as requirements of the parent schema. + * + * @type (keywordUri: string) => boolean + */ +const isRequired = (keywordUri) => !normalizationHandlers[toAbsoluteIri(keywordUri)]?.conditional; /** @type API.getSuccesses */ export const getSuccesses = (normalizedOutput, rootInstance, localization, context) => { @@ -521,7 +498,7 @@ export const getSuccesses = (normalizedOutput, rootInstance, localization, conte /** @type API.ErrorObject[] */ const successes = []; - const describedOutput = withoutConditionalResults(normalizedOutput); + const describedOutput = flatten(normalizedOutput, isRequired); descriptionDepth++; try { @@ -630,10 +607,11 @@ const sizeOf = (errorObjects) => errorObjects.reduce((size, errorObject) => { /** @type (normalizedOutput: API.NormalizedOutput) => boolean */ export const isPassing = (normalizedOutput) => { - for (const instanceLocation in normalizedOutput) { - for (const keywordUri in normalizedOutput[instanceLocation]) { - for (const schemaLocation in normalizedOutput[instanceLocation][keywordUri]) { - if (normalizedOutput[instanceLocation][keywordUri][schemaLocation].valid !== true) { + const results = flattenOutput(normalizedOutput); + for (const instanceLocation in results) { + for (const keywordUri in results[instanceLocation]) { + for (const schemaLocation in results[instanceLocation][keywordUri]) { + if (results[instanceLocation][keywordUri][schemaLocation].valid !== true) { return false; } } @@ -645,10 +623,11 @@ export const isPassing = (normalizedOutput) => { /** @type (normalizedOutput: API.NormalizedOutput) => boolean */ export const isFailing = (normalizedOutput) => { - for (const instanceLocation in normalizedOutput) { - for (const keywordUri in normalizedOutput[instanceLocation]) { - for (const schemaLocation in normalizedOutput[instanceLocation][keywordUri]) { - if (normalizedOutput[instanceLocation][keywordUri][schemaLocation].valid === false) { + const results = flattenOutput(normalizedOutput); + for (const instanceLocation in results) { + for (const keywordUri in results[instanceLocation]) { + for (const schemaLocation in results[instanceLocation][keywordUri]) { + if (results[instanceLocation][keywordUri][schemaLocation].valid === false) { return true; } } diff --git a/src/output-plugin.js b/src/output-plugin.js index 85041b5..287f16d 100644 --- a/src/output-plugin.js +++ b/src/output-plugin.js @@ -30,19 +30,12 @@ export class JsonSchemaErrorsOutputPlugin { } // Each keyword's result includes the output of its subschemas so the - // subschemas can be described. The results of a simple applicator's - // subschemas are also merged into the results of its parent schema. + // subschemas can be described /** @type NonNullable["afterKeyword"]> */ - afterKeyword(keywordNode, instance, context, valid, schemaContext, keyword) { + afterKeyword(keywordNode, instance, context, valid, schemaContext) { const [keywordUri, schemaLocation] = keywordNode; - if (keyword.simpleApplicator) { - for (const subSchemaOutput of context.subSchemaOutput ?? []) { - mergeOutput(schemaContext.output, subSchemaOutput); - } - } - schemaContext.output[Instance.uri(instance)] ??= {}; schemaContext.output[Instance.uri(instance)][keywordUri] ??= {}; schemaContext.output[Instance.uri(instance)][keywordUri][schemaLocation] = context.subSchemaOutput @@ -64,15 +57,3 @@ export class JsonSchemaErrorsOutputPlugin { this.output = context.output; } } - -/** @type (a: NormalizedOutput, b: NormalizedOutput) => void */ -const mergeOutput = (a, b) => { - for (const instanceLocation in b) { - a[instanceLocation] ??= {}; - for (const keywordUri in b[instanceLocation]) { - a[instanceLocation][keywordUri] ??= {}; - - Object.assign(a[instanceLocation][keywordUri], b[instanceLocation][keywordUri]); - } - } -}; diff --git a/src/output-plugin.test.ts b/src/output-plugin.test.ts index 7c0f05f..9c73fff 100644 --- a/src/output-plugin.test.ts +++ b/src/output-plugin.test.ts @@ -23,9 +23,6 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/$defs/string/type`]: { valid: false } - }, "https://json-schema.org/keyword/ref": { [`${schemaUri}#/$ref`]: { valid: false, @@ -59,9 +56,6 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/$defs/string/type`]: { valid: true } - }, "https://json-schema.org/keyword/ref": { [`${schemaUri}#/$ref`]: { valid: true, @@ -91,11 +85,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: 42 }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo": { - "https://json-schema.org/validation": { - [`${schemaUri}#/additionalProperties`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/additionalProperties": { [`${schemaUri}#/additionalProperties`]: { @@ -121,16 +110,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: 42, bar: 24 }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo": { - "https://json-schema.org/validation": { - [`${schemaUri}#/additionalProperties`]: { valid: false } - } - }, - "#/bar": { - "https://json-schema.org/validation": { - [`${schemaUri}#/additionalProperties`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/additionalProperties": { [`${schemaUri}#/additionalProperties`]: { @@ -165,11 +144,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: 42 }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/additionalProperties/type`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/additionalProperties": { [`${schemaUri}#/additionalProperties`]: { @@ -217,12 +191,6 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/allOf/0/type`]: { valid: true } - }, - "https://json-schema.org/keyword/maximum": { - [`${schemaUri}#/allOf/1/maximum`]: { valid: false } - }, "https://json-schema.org/keyword/allOf": { [`${schemaUri}#/allOf`]: { valid: false, @@ -264,10 +232,6 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/type": { [`${schemaUri}#/type`]: { valid: true } }, - "https://json-schema.org/keyword/maximum": { - [`${schemaUri}#/allOf/0/maximum`]: { valid: false }, - [`${schemaUri}#/allOf/1/maximum`]: { valid: false } - }, "https://json-schema.org/keyword/allOf": { [`${schemaUri}#/allOf`]: { valid: false, @@ -305,12 +269,6 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/allOf/0/type`]: { valid: true } - }, - "https://json-schema.org/keyword/maximum": { - [`${schemaUri}#/allOf/1/maximum`]: { valid: true } - }, "https://json-schema.org/keyword/allOf": { [`${schemaUri}#/allOf`]: { valid: true, @@ -627,9 +585,6 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { - "https://json-schema.org/keyword/required": { - [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: false } - }, "https://json-schema.org/keyword/dependentSchemas": { [`${schemaUri}#/dependentSchemas`]: { valid: false, @@ -660,10 +615,6 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { - "https://json-schema.org/keyword/required": { - [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: false }, - [`${schemaUri}#/dependentSchemas/bar/required`]: { valid: false } - }, "https://json-schema.org/keyword/dependentSchemas": { [`${schemaUri}#/dependentSchemas`]: { valid: false, @@ -700,9 +651,6 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { - "https://json-schema.org/keyword/required": { - [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: true } - }, "https://json-schema.org/keyword/dependentSchemas": { [`${schemaUri}#/dependentSchemas`]: { valid: true, @@ -747,9 +695,6 @@ describe("JSON Schema Errors Output Format", () => { ] } }, - "https://json-schema.org/keyword/minLength": { - [`${schemaUri}#/then/minLength`]: { valid: false } - }, "https://json-schema.org/keyword/then": { [`${schemaUri}#/then`]: { valid: false, @@ -792,9 +737,6 @@ describe("JSON Schema Errors Output Format", () => { ] } }, - "https://json-schema.org/keyword/minLength": { - [`${schemaUri}#/then/minLength`]: { valid: true } - }, "https://json-schema.org/keyword/then": { [`${schemaUri}#/then`]: { valid: true, @@ -843,9 +785,6 @@ describe("JSON Schema Errors Output Format", () => { ] } }, - "https://json-schema.org/keyword/minimum": { - [`${schemaUri}#/else/minimum`]: { valid: false } - }, "https://json-schema.org/keyword/else": { [`${schemaUri}#/else`]: { valid: false, @@ -892,9 +831,6 @@ describe("JSON Schema Errors Output Format", () => { ] } }, - "https://json-schema.org/keyword/minimum": { - [`${schemaUri}#/else/minimum`]: { valid: true } - }, "https://json-schema.org/keyword/else": { [`${schemaUri}#/else`]: { valid: true, @@ -923,16 +859,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, [42, 24], { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/0": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/items/type`]: { valid: false } - } - }, - "#/1": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/items/type`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/items": { [`${schemaUri}#/items`]: { @@ -967,11 +893,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, ["foo"], { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/0": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/items/type`]: { valid: true } - } - }, "#": { "https://json-schema.org/keyword/items": { [`${schemaUri}#/items`]: { @@ -1004,16 +925,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: 42, bar: true }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/patternProperties/%5Ef/type`]: { valid: false } - } - }, - "#/bar": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/patternProperties/%5Eb/type`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/patternProperties": { [`${schemaUri}#/patternProperties`]: { @@ -1051,16 +962,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: "a", bar: 42 }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/patternProperties/%5Ef/type`]: { valid: true } - } - }, - "#/bar": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/patternProperties/%5Eb/type`]: { valid: true } - } - }, "#": { "https://json-schema.org/keyword/patternProperties": { [`${schemaUri}#/patternProperties`]: { @@ -1100,16 +1001,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, [42, "foo"], { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/0": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/prefixItems/0/type`]: { valid: false } - } - }, - "#/1": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/prefixItems/1/type`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/prefixItems": { [`${schemaUri}#/prefixItems`]: { @@ -1147,16 +1038,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, ["foo", 42], { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/0": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/prefixItems/0/type`]: { valid: true } - } - }, - "#/1": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/prefixItems/1/type`]: { valid: true } - } - }, "#": { "https://json-schema.org/keyword/prefixItems": { [`${schemaUri}#/prefixItems`]: { @@ -1196,16 +1077,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: 42, bar: true }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/foo/type`]: { valid: false } - } - }, - "#/bar": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/bar/type`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties`]: { @@ -1243,16 +1114,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: "a", bar: 42 }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/foo/type`]: { valid: true } - } - }, - "#/bar": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/bar/type`]: { valid: true } - } - }, "#": { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties`]: { @@ -1289,16 +1150,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { banana: true, pear: false }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#*/banana": { - "https://json-schema.org/keyword/pattern": { - [`${schemaUri}#/propertyNames/pattern`]: { valid: false } - } - }, - "#*/pear": { - "https://json-schema.org/keyword/pattern": { - [`${schemaUri}#/propertyNames/pattern`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/propertyNames": { [`${schemaUri}#/propertyNames`]: { @@ -1333,11 +1184,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { apple: true }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#*/apple": { - "https://json-schema.org/keyword/pattern": { - [`${schemaUri}#/propertyNames/pattern`]: { valid: true } - } - }, "#": { "https://json-schema.org/keyword/propertyNames": { [`${schemaUri}#/propertyNames`]: { @@ -1365,11 +1211,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: 42 }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo": { - "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedProperties`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/unevaluatedProperties": { [`${schemaUri}#/unevaluatedProperties`]: { @@ -1401,11 +1242,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: 42, bar: true, baz: null }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/bar": { - "https://json-schema.org/validation": { - [`${schemaUri}#/properties/bar`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties`]: { @@ -1437,11 +1273,6 @@ describe("JSON Schema Errors Output Format", () => { ] } } - }, - "#/baz": { - "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedProperties`]: { valid: false } - } } }); }); @@ -1454,11 +1285,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: 42 }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/unevaluatedProperties/type`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/unevaluatedProperties": { [`${schemaUri}#/unevaluatedProperties`]: { @@ -1500,11 +1326,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, [42], { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/0": { - "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedItems`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/unevaluatedItems": { [`${schemaUri}#/unevaluatedItems`]: { @@ -1532,11 +1353,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, [42], { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/0": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/unevaluatedItems/type`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/unevaluatedItems": { [`${schemaUri}#/unevaluatedItems`]: { @@ -1565,11 +1381,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, [42, true, null], { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/1": { - "https://json-schema.org/validation": { - [`${schemaUri}#/prefixItems/1`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/prefixItems": { [`${schemaUri}#/prefixItems`]: { @@ -1601,11 +1412,6 @@ describe("JSON Schema Errors Output Format", () => { ] } } - }, - "#/2": { - "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedItems`]: { valid: false } - } } }); }); @@ -2232,11 +2038,6 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: 42 }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/foo/type`]: { valid: false } - } - }, "#": { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties`]: { @@ -2273,38 +2074,12 @@ describe("JSON Schema Errors Output Format", () => { await validate(schemaUri, { foo: { bar: 42 } }, { plugins: [outputPlugin] }); expect(outputPlugin.output).to.eql({ - "#/foo/bar": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/foo/properties/bar/type`]: { valid: false } - } - }, - "#/foo": { - "https://json-schema.org/keyword/properties": { - [`${schemaUri}#/properties/foo/properties`]: { - valid: false, - outputs: [ - { - "#/foo/bar": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/foo/properties/bar/type`]: { valid: false } - } - } - } - ] - } - } - }, "#": { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties`]: { valid: false, outputs: [ { - "#/foo/bar": { - "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/foo/properties/bar/type`]: { valid: false } - } - }, "#/foo": { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties/foo/properties`]: { diff --git a/src/test-suite/tests/not.json b/src/test-suite/tests/not.json index aa77e0f..6a818b7 100644 --- a/src/test-suite/tests/not.json +++ b/src/test-suite/tests/not.json @@ -1157,6 +1157,78 @@ } ] }, + { + "description": "not with a definition referenced directly and by a nested then", + "compatibility": "2019", + "schema": { + "not": { "$ref": "#/$defs/user" }, + "$defs": { + "base": { "required": ["id"] }, + "admin": { + "$ref": "#/$defs/base", + "required": ["permissions"] + }, + "user": { + "$ref": "#/$defs/base", + "if": { "required": ["admin"] }, + "then": { "$ref": "#/$defs/admin" } + } + } + }, + "instance": { "id": 1, "admin": true, "permissions": [] }, + "errors": [ + { + "messageId": "not-message", + "messageParams": { + "quantifier": "some" + }, + "alternatives": [ + [ + { + "messageId": "required-negated-message", + "messageParams": { + "count": 1, + "required": { "or": ["'id'"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/$defs/base/required"] + } + ], + [ + { + "messageId": "all-true-message", + "alternatives": [ + [ + { + "messageId": "required-success-message", + "messageParams": { + "count": 1, + "required": { "and": ["'admin'"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/$defs/user/if/required"] + }, + { + "messageId": "required-negated-message", + "messageParams": { + "count": 2, + "required": { "or": ["'id'", "'permissions'"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/$defs/base/required", "#/$defs/admin/required"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/$defs/user/if"] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/not"] + } + ] + }, { "description": "not with maximum", "schema": { From 752d781a83793c8506e844807eaefbf57937e217 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Mon, 5 Oct 2026 12:41:38 -0700 Subject: [PATCH 2/6] Fix the README example for custom keywords The startsWith example used an addErrorHandler function that doesn't exist, passed an async function where error handlers are synchronous, and skipped every result because it tested the keyword's result object instead of whether it failed. It now registers the handler with setErrorHandler, gets the keyword's value with getCompiledKeywordValue, and has a success handler so the keyword can be described for keywords like 'not'. getCompiledKeywordValue is exported because error handlers otherwise have no synchronous way to get a keyword's value. Co-Authored-By: Claude Opus 5.5 --- README.md | 79 +++++++++++++++++++++++++++++++++++++------------- src/index.d.ts | 9 ++++++ src/index.js | 1 + 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 370dc41..9fdd1ad 100644 --- a/README.md +++ b/README.md @@ -146,14 +146,14 @@ custom keyword we'll need to register a handler for each phase of the process. Here's an example of adding support for a simple keyword called `startsWith`. `startsWith` takes a string and asserts that a string JSON instance starts with -the value of `startsWith`. +the value of `startsWith`. The keyword itself needs to be defined with +`@hyperjump/json-schema`'s `addKeyword` and included in a dialect. + +Every keyword needs a normalization handler. Keywords that aren't applicators +don't have anything to evaluate. ```TypeScript -import { setNormalizationHandler, addErrorHandler } from "@hyperjump/json-schema-errors"; -import { getSchema } from "@hyperjump/json-schema/experimental"; -import * as Schema from "@hyperjump/browser"; -import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import type { ErrorObject } from "@hyperjump/json-schema-errors"; +import { setNormalizationHandler } from "@hyperjump/json-schema-errors"; const KEYWORD_URI = "https://example.com/keyword/startsWith"; @@ -162,26 +162,65 @@ setNormalizationHandler(KEYWORD_URI, { // Only applicator keywords need to return a value } }); +``` + +An error handler turns the normalized results into messages. It's registered +with a URI that identifies the handler. A handler can handle any number of +keywords, so it gets the results of every keyword that applies to a location in +the instance and picks out the ones it handles. + +`error` describes keywords that failed. A result's `valid` is `false` if the +keyword failed, `true` if it passed, and `undefined` if the result isn't known. + +`success` describes what keywords require. It's used to explain failures caused +by a subschema passing, such as with `not`. When `localization.isNegated` is +`true`, it describes what would make the keyword fail instead. A keyword without +a `success` handler can't be described, so messages for keywords like `not` and +`oneOf` will be less specific. -addErrorHandler(async (normalizedErrors, instance, localization) => { - const errors: ErrorObject = []; +```TypeScript +import * as Instance from "@hyperjump/json-schema/instance/experimental"; +import { getCompiledKeywordValue, setErrorHandler } from "@hyperjump/json-schema-errors"; +import type { ErrorObject } from "@hyperjump/json-schema-errors"; - for (const schemaLocation in normalizedErrors[KEYWORD_URI]) { - if (normalizedErrors[KEYWORD_URI][schemaLocation]) { - continue; +const KEYWORD_URI = "https://example.com/keyword/startsWith"; + +setErrorHandler("https://example.com/error-handler/startsWith", { + error: (normalizedErrors, instance, localization, context) => { + const errors: ErrorObject[] = []; + + for (const schemaLocation in normalizedErrors[KEYWORD_URI]) { + if (normalizedErrors[KEYWORD_URI][schemaLocation].valid !== false) { + continue; + } + + const startsWith = getCompiledKeywordValue(context.ast, schemaLocation) as string; + errors.push({ + message: `Expected a string that starts with '${startsWith}'`, + instanceLocation: Instance.uri(instance), + schemaLocations: [schemaLocation] + }); } - const keyword = await getSchema(schemaLocation); - const startsWith = Schema.value(keyword) as string; + return errors; + }, + + success: (normalizedOutput, instance, localization, context) => { + const successes: ErrorObject[] = []; + + for (const schemaLocation in normalizedOutput[KEYWORD_URI]) { + const startsWith = getCompiledKeywordValue(context.ast, schemaLocation) as string; + successes.push({ + message: localization.isNegated + ? `The value is a string that doesn't start with '${startsWith}'` + : `The value is either not a string or starts with '${startsWith}'`, + instanceLocation: Instance.uri(instance), + schemaLocations: [schemaLocation] + }); + } - errors.push({ - message: "Expected a string that starts with '${startsWith}'", - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); + return successes; } - - return errors; }); ``` diff --git a/src/index.d.ts b/src/index.d.ts index 27ff8a2..1d1b4e3 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -226,6 +226,15 @@ export type ErrorHandlerContext = { */ export const flattenOutput: (normalizedOutput: NormalizedOutput) => NormalizedOutput; +/** + * Gets the compiled value of the keyword at the given schema location, as + * returned by the keyword's `compile` function in `@hyperjump/json-schema`. + * + * @param ast - The compiled schema, from the error handler's context + * @param schemaLocation - The keyword's schema location + */ +export const getCompiledKeywordValue: (ast: AST, schemaLocation: string) => unknown; + /** * Converts the normalized error format to human readable errors. It's used to * build errors in applicator error handlers. diff --git a/src/index.js b/src/index.js index e0e0119..98f0602 100644 --- a/src/index.js +++ b/src/index.js @@ -188,6 +188,7 @@ setErrorHandler("https://hyperjump.io/error-handler/unknown", unknownErrorHandle export { evaluateSchema, flattenOutput, + getCompiledKeywordValue, getErrors, getSuccesses, jsonSchemaErrors, From 2faafe6641b82cb0495354f20ec3a4078a5d7ab7 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Mon, 5 Oct 2026 13:23:12 -0700 Subject: [PATCH 3/6] Move localization into the error handler context Error handlers, getErrors, and getSuccesses took a localization and a context as separate arguments, and helpers took whichever one they happened to need. Now the localization is part of ErrorHandlerContext, so handlers and helpers take just the context. negate(context) gives a context whose success messages describe what would make keywords fail, replacing localization.negated(). describeConditional's callbacks get a context for the same reason. The message callbacks for describeScope still take a localization because they only build messages. This gives the helpers consistent signatures before they're exported for custom handlers. Co-Authored-By: Claude Opus 5.5 --- README.md | 10 +- src/error-handlers/anyOf.js | 22 ++-- src/error-handlers/boolean-schema.js | 4 +- src/error-handlers/contains.js | 62 +++++----- src/error-handlers/dependentSchemas.js | 18 +-- src/error-handlers/draft-04/dependencies.js | 8 +- src/error-handlers/eachProperty.js | 8 +- src/error-handlers/format.js | 10 +- src/error-handlers/ifThenElse.js | 18 +-- src/error-handlers/items.js | 4 +- src/error-handlers/maxItems.js | 8 +- src/error-handlers/maxLength.js | 8 +- src/error-handlers/maxProperties.js | 8 +- src/error-handlers/maximum.js | 16 +-- src/error-handlers/minItems.js | 8 +- src/error-handlers/minLength.js | 8 +- src/error-handlers/minProperties.js | 8 +- src/error-handlers/minimum.js | 16 +-- src/error-handlers/multipleOf.js | 8 +- src/error-handlers/not.js | 28 ++--- src/error-handlers/oneOf.js | 50 ++++---- src/error-handlers/pattern.js | 8 +- src/error-handlers/prefixItems.js | 10 +- src/error-handlers/properties.js | 10 +- src/error-handlers/required.js | 12 +- src/error-handlers/typeConstEnum.js | 16 +-- src/error-handlers/uniqueItems.js | 8 +- src/error-handlers/unknown.js | 4 +- src/index.d.ts | 26 ++++- src/index.js | 1 + src/json-schema-errors.js | 121 ++++++++++---------- src/output-format.js | 4 +- 32 files changed, 285 insertions(+), 265 deletions(-) diff --git a/README.md b/README.md index 9fdd1ad..b031de3 100644 --- a/README.md +++ b/README.md @@ -173,8 +173,8 @@ the instance and picks out the ones it handles. keyword failed, `true` if it passed, and `undefined` if the result isn't known. `success` describes what keywords require. It's used to explain failures caused -by a subschema passing, such as with `not`. When `localization.isNegated` is -`true`, it describes what would make the keyword fail instead. A keyword without +by a subschema passing, such as with `not`. When `context.localization.isNegated` +is `true`, it describes what would make the keyword fail instead. A keyword without a `success` handler can't be described, so messages for keywords like `not` and `oneOf` will be less specific. @@ -186,7 +186,7 @@ import type { ErrorObject } from "@hyperjump/json-schema-errors"; const KEYWORD_URI = "https://example.com/keyword/startsWith"; setErrorHandler("https://example.com/error-handler/startsWith", { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { const errors: ErrorObject[] = []; for (const schemaLocation in normalizedErrors[KEYWORD_URI]) { @@ -205,13 +205,13 @@ setErrorHandler("https://example.com/error-handler/startsWith", { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { const successes: ErrorObject[] = []; for (const schemaLocation in normalizedOutput[KEYWORD_URI]) { const startsWith = getCompiledKeywordValue(context.ast, schemaLocation) as string; successes.push({ - message: localization.isNegated + message: context.localization.isNegated ? `The value is a string that doesn't start with '${startsWith}'` : `The value is either not a string or starts with '${startsWith}'`, instanceLocation: Instance.uri(instance), diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index f4c68f1..b5c6707 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -8,7 +8,7 @@ import { allTrue, flattenOutput, getErrors, getSuccesses, someTrue } from "../js /** @type ErrorHandler */ const anyOfErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -56,13 +56,13 @@ const anyOfErrorHandler = { } // The alternative passed all the filters - alternatives.push(getErrors(alternative, instance, localization, context)); + alternatives.push(getErrors(alternative, instance, context)); } // If all alternatives were filtered out, default to returning all of them if (alternatives.length === 0) { for (const alternative of anyOf) { - alternatives.push(getErrors(alternative, instance, localization, context)); + alternatives.push(getErrors(alternative, instance, context)); } } @@ -70,7 +70,7 @@ const anyOfErrorHandler = { errors.push(...alternatives[0]); } else { errors.push({ - message: localization.getAnyOfErrorMessage(), + message: context.localization.getAnyOfErrorMessage(), alternatives, instanceLocation, schemaLocations: [schemaLocation] @@ -81,20 +81,20 @@ const anyOfErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/anyOf"]) { const alternatives = normalizedOutput["https://json-schema.org/keyword/anyOf"][schemaLocation].outputs ?? []; - if (localization.isNegated) { + if (context.localization.isNegated) { // 'anyOf' fails if all of its alternatives fail. An alternative fails if // at least one of its keywords fails. Changing the value to make one // alternative fail could make another one pass, so all of them need to // be described even if some of them fail now. const alternativeOptions = alternatives.map((alternative) => { - return getSuccesses(alternative, instance, localization, context); + return getSuccesses(alternative, instance, context); }); // An alternative that can't be described means we can't say how to make it fail @@ -106,23 +106,23 @@ const anyOfErrorHandler = { successes.push(...alternativeOptions[0]); } else { const requirements = alternativeOptions.flatMap((options) => { - return someTrue(options.map((option) => [option]), instance, schemaLocation, localization); + return someTrue(options.map((option) => [option]), instance, schemaLocation, context); }); - successes.push(...allTrue(requirements, instance, schemaLocation, localization)); + successes.push(...allTrue(requirements, instance, schemaLocation, context)); } } else { // Passes if at least one alternative passes. All of them are described, // even if we know which ones match, because these descriptions tell the // user what would need to change to make 'anyOf' fail. const descriptions = alternatives.map((alternative) => { - return getSuccesses(alternative, instance, localization, context); + return getSuccesses(alternative, instance, context); }); if (descriptions.some((description) => description.length === 0)) { continue; } - successes.push(...someTrue(descriptions, instance, schemaLocation, localization)); + successes.push(...someTrue(descriptions, instance, schemaLocation, context)); } } diff --git a/src/error-handlers/boolean-schema.js b/src/error-handlers/boolean-schema.js index 4b350e1..70c02a8 100644 --- a/src/error-handlers/boolean-schema.js +++ b/src/error-handlers/boolean-schema.js @@ -6,7 +6,7 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** @type ErrorHandler */ const booleanSchemaErrorHandler = { - error: (normalizedErrors, instance, localization) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -16,7 +16,7 @@ const booleanSchemaErrorHandler = { } errors.push({ - message: localization.getBooleanSchemaErrorMessage(), + message: context.localization.getBooleanSchemaErrorMessage(), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); diff --git a/src/error-handlers/contains.js b/src/error-handlers/contains.js index f40221e..1775ebb 100644 --- a/src/error-handlers/contains.js +++ b/src/error-handlers/contains.js @@ -1,9 +1,9 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { allTrue, allowsAnyValue, countTrue, describeEach, getCompiledKeywordValue, getPlaceholder, getSiblingKeywordLocation, getSuccesses, isFailing, isPassing, limitItems, someTrue } from "../json-schema-errors.js"; +import { allTrue, allowsAnyValue, countTrue, describeEach, getCompiledKeywordValue, getPlaceholder, getSiblingKeywordLocation, getSuccesses, isFailing, isPassing, limitItems, negate, someTrue } from "../json-schema-errors.js"; /** * @import { ContainsAst } from "../normalization-handlers/contains.js" - * @import { ContainsRange, ErrorHandler, ErrorObject, Localization, NormalizedOutput } from "../index.d.ts" + * @import { ContainsRange, ErrorHandler, ErrorHandlerContext, ErrorObject, NormalizedOutput } from "../index.d.ts" */ const keywordUris = [ @@ -13,7 +13,7 @@ const keywordUris = [ /** @type ErrorHandler */ const containsErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -53,15 +53,15 @@ const containsErrorHandler = { }); if (matches.length > contains.maxContains) { const descriptions = matches.map(({ itemOutput }) => { - return getSuccesses(itemOutput, instance, localization, context); + return getSuccesses(itemOutput, instance, context); }); // If any match can't be described, the errors would be misleading if (descriptions.every((description) => description.length > 0)) { matches.forEach(({ item }, index) => { errors.push({ - message: localization.getContainsTooManyErrorMessage(contains.maxContains), - alternatives: [limitItems(descriptions[index], item, localization)], + message: context.localization.getContainsTooManyErrorMessage(contains.maxContains), + alternatives: [limitItems(descriptions[index], item, context)], instanceLocation: Instance.uri(item), schemaLocations: [schemaLocation, maxContainsLocation] }); @@ -71,7 +71,7 @@ const containsErrorHandler = { // Every item matched and there's nothing to say about why, so the // problem is effectively that there are too many items. errors.push({ - message: localization.getMaxItemsErrorMessage(contains.maxContains), + message: context.localization.getMaxItemsErrorMessage(contains.maxContains), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation, maxContainsLocation] }); @@ -84,25 +84,25 @@ const containsErrorHandler = { // Describe what an item would need to be like using an item that doesn't exist const containsLocation = typeof contains === "string" ? contains : contains.contains; const item = getPlaceholder(instance, String(Instance.length(instance))); - const description = describeEach(containsLocation, item, instance, localization, context); + const description = describeEach(containsLocation, item, instance, context); if (description.length > 0) { errors.push({ - message: localization.getContainsErrorMessage(range, true), - alternatives: [limitItems(description, instance, localization)], + message: context.localization.getContainsErrorMessage(range, true), + alternatives: [limitItems(description, instance, context)], instanceLocation: Instance.uri(instance), schemaLocations: schemaLocations }); } else if (allowsAnyValue(containsLocation, context.ast)) { // Any item matches, so there aren't enough items errors.push({ - message: localization.getMinItemsErrorMessage(range.minContains ?? 1), + message: context.localization.getMinItemsErrorMessage(range.minContains ?? 1), instanceLocation: Instance.uri(instance), schemaLocations: schemaLocations }); } else { errors.push({ - message: localization.getContainsErrorMessage(range, false), + message: context.localization.getContainsErrorMessage(range, false), instanceLocation: Instance.uri(instance), schemaLocations: schemaLocations }); @@ -113,7 +113,7 @@ const containsErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -131,58 +131,58 @@ const containsErrorHandler = { const matching = itemOutputs.filter(isPassing); const notMatching = itemOutputs.filter((itemOutput) => !isPassing(itemOutput)); - /** @type (itemOutput: NormalizedOutput, localization: Localization) => ErrorObject[] */ - const describe = (itemOutput, localization) => getSuccesses(itemOutput, instance, localization, context); + /** @type (itemOutput: NormalizedOutput, context: ErrorHandlerContext) => ErrorObject[] */ + const describe = (itemOutput, context) => getSuccesses(itemOutput, instance, context); - if (localization.isNegated) { + if (context.localization.isNegated) { // 'contains' fails if too few items match or too many items match if (isKnown) { // Make enough of the matching items fail - const itemOptions = matching.map((itemOutput) => describe(itemOutput, localization)); + const itemOptions = matching.map((itemOutput) => describe(itemOutput, context)); if (minContains > 0 && itemOptions.every((options) => options.length > 0)) { const failCount = matching.length - minContains + 1; if (matching.length === 1 && failCount === 1) { successes.push(...itemOptions[0]); } else { const items = itemOptions.map((options) => { - return someTrue(options.map((option) => [option]), instance, schemaLocation, localization); + return someTrue(options.map((option) => [option]), instance, schemaLocation, context); }); - const requirements = countTrue(items, { min: failCount }, instance, schemaLocation, localization); - successes.push(...allTrue(requirements, instance, schemaLocation, localization)); + const requirements = countTrue(items, { min: failCount }, instance, schemaLocation, context); + successes.push(...allTrue(requirements, instance, schemaLocation, context)); } } // Make enough of the other items match const matchCount = maxContains - matching.length + 1; - const descriptions = notMatching.map((itemOutput) => describe(itemOutput, localization.negated())); + const descriptions = notMatching.map((itemOutput) => describe(itemOutput, negate(context))); if (matchCount <= notMatching.length && descriptions.every((description) => description.length > 0)) { - const requirements = countTrue(descriptions, { min: matchCount }, instance, schemaLocation, localization); - successes.push(...allTrue(requirements, instance, schemaLocation, localization)); + const requirements = countTrue(descriptions, { min: matchCount }, instance, schemaLocation, context); + successes.push(...allTrue(requirements, instance, schemaLocation, context)); } } else { - const descriptions = itemOutputs.map((itemOutput) => describe(itemOutput, localization.negated())); + const descriptions = itemOutputs.map((itemOutput) => describe(itemOutput, negate(context))); if (descriptions.length > 0 && descriptions.every((description) => description.length > 0)) { if (minContains > 0) { - const requirements = countTrue(descriptions, { max: minContains - 1 }, instance, schemaLocation, localization); - successes.push(...allTrue(requirements, instance, schemaLocation, localization)); + const requirements = countTrue(descriptions, { max: minContains - 1 }, instance, schemaLocation, context); + successes.push(...allTrue(requirements, instance, schemaLocation, context)); } if (maxContains < descriptions.length) { - const requirements = countTrue(descriptions, { min: maxContains + 1 }, instance, schemaLocation, localization); - successes.push(...allTrue(requirements, instance, schemaLocation, localization)); + const requirements = countTrue(descriptions, { min: maxContains + 1 }, instance, schemaLocation, context); + successes.push(...allTrue(requirements, instance, schemaLocation, context)); } } } } else { if (isKnown) { - const descriptions = matching.map((itemOutput) => describe(itemOutput, localization)); + const descriptions = matching.map((itemOutput) => describe(itemOutput, context)); if (descriptions.every((description) => description.length > 0)) { successes.push(...descriptions.flat()); } } else { - const descriptions = itemOutputs.map((itemOutput) => describe(itemOutput, localization)); + const descriptions = itemOutputs.map((itemOutput) => describe(itemOutput, context)); if (descriptions.every((description) => description.length > 0)) { - successes.push(...countTrue(descriptions, { min: minContains, max: maxContains }, instance, schemaLocation, localization)); + successes.push(...countTrue(descriptions, { min: minContains, max: maxContains }, instance, schemaLocation, context)); } } } diff --git a/src/error-handlers/dependentSchemas.js b/src/error-handlers/dependentSchemas.js index ec1d0b4..4cf296a 100644 --- a/src/error-handlers/dependentSchemas.js +++ b/src/error-handlers/dependentSchemas.js @@ -8,7 +8,7 @@ import { /** * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" - * @import { ErrorHandler, ErrorHandlerContext, ErrorObject, Localization, NormalizedOutput } from "../index.d.ts" + * @import { ErrorHandler, ErrorHandlerContext, ErrorObject, NormalizedOutput } from "../index.d.ts" */ /** @type ErrorHandler */ @@ -16,14 +16,14 @@ const dependentSchemasErrorHandler = { // Failures in dependent schemas are flattened into the parent schema's results, // so they're reported by the handlers for the keywords that failed - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/dependentSchemas"]) { const dependencies = /** @type [string, string][] */ (getCompiledKeywordValue(context.ast, schemaLocation)); const outputs = normalizedOutput["https://json-schema.org/keyword/dependentSchemas"][schemaLocation].outputs ?? []; - successes.push(...describeSchemaDependencies(dependencies, outputs, instance, localization, context)); + successes.push(...describeSchemaDependencies(dependencies, outputs, instance, context)); } return successes; @@ -35,9 +35,9 @@ const dependentSchemasErrorHandler = { * dependency's property. The outputs are for the dependencies whose property is * present, in the order they appear in the schema. * - * @type (dependencies: [string, string][], outputs: NormalizedOutput[], instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[] + * @type (dependencies: [string, string][], outputs: NormalizedOutput[], instance: JsonNode, context: ErrorHandlerContext) => ErrorObject[] */ -export const describeSchemaDependencies = (dependencies, outputs, instance, localization, context) => { +export const describeSchemaDependencies = (dependencies, outputs, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -51,13 +51,13 @@ export const describeSchemaDependencies = (dependencies, outputs, instance, loca // The dependency only applies if the property is present successes.push(...describeConditional({ - condition: (localization) => [{ - message: localization.getHasPropertySuccessMessage([propertyName]), + condition: (context) => [{ + message: context.localization.getHasPropertySuccessMessage([propertyName]), instanceLocation: Instance.uri(instance), schemaLocations: [dependencyLocation] }], - then: (localization) => getSuccesses(output, instance, localization, context) - }, instance, dependencyLocation, localization)); + then: (context) => getSuccesses(output, instance, context) + }, instance, dependencyLocation, context)); } return successes; diff --git a/src/error-handlers/draft-04/dependencies.js b/src/error-handlers/draft-04/dependencies.js index 2abfb50..e37d79d 100644 --- a/src/error-handlers/draft-04/dependencies.js +++ b/src/error-handlers/draft-04/dependencies.js @@ -7,7 +7,7 @@ import { describeSchemaDependencies } from "../dependentSchemas.js"; /** @type ErrorHandler */ const dependenciesErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -18,7 +18,7 @@ const dependenciesErrorHandler = { const dependentSchemaOutputs = normalizedErrors["https://json-schema.org/keyword/draft-04/dependencies"][schemaLocation].outputs ?? []; for (const dependentSchemaOutput of dependentSchemaOutputs) { - const dependentSchemaErrors = getErrors(dependentSchemaOutput, instance, localization, context); + const dependentSchemaErrors = getErrors(dependentSchemaOutput, instance, context); errors.push(...dependentSchemaErrors); } } @@ -26,7 +26,7 @@ const dependenciesErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -35,7 +35,7 @@ const dependenciesErrorHandler = { const dependencies = /** @type [string, string | string[]][] */ (getCompiledKeywordValue(context.ast, schemaLocation)); const schemaDependencies = /** @type [string, string][] */ (dependencies.filter(([, dependency]) => typeof dependency === "string")); const outputs = normalizedOutput["https://json-schema.org/keyword/draft-04/dependencies"][schemaLocation].outputs ?? []; - successes.push(...describeSchemaDependencies(schemaDependencies, outputs, instance, localization, context)); + successes.push(...describeSchemaDependencies(schemaDependencies, outputs, instance, context)); } return successes; diff --git a/src/error-handlers/eachProperty.js b/src/error-handlers/eachProperty.js index 62a5ab3..9053972 100644 --- a/src/error-handlers/eachProperty.js +++ b/src/error-handlers/eachProperty.js @@ -23,7 +23,7 @@ import { * @type ErrorHandler */ const eachPropertyErrorHandler = { - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -44,7 +44,7 @@ const eachPropertyErrorHandler = { placeholder: property, each: (localization, count) => localization.getEachMatchingPropertySuccessMessage(pattern.source, count), none: (localization) => localization.getNoMatchingPropertySuccessMessage(pattern.source) - }, instance, schemaLocation, localization, context)); + }, instance, schemaLocation, context)); } } @@ -61,7 +61,7 @@ const eachPropertyErrorHandler = { placeholder: property, each: (localization, count) => localization.getEachAdditionalPropertySuccessMessage(properties, patterns, count), none: (localization) => localization.getNoAdditionalPropertySuccessMessage(properties, patterns) - }, instance, schemaLocation, localization, context)); + }, instance, schemaLocation, context)); } // Property names are at a different location than property values @@ -73,7 +73,7 @@ const eachPropertyErrorHandler = { placeholder: propertyName, each: (localization, count) => localization.getEachPropertyNameSuccessMessage(count), none: (localization) => localization.getMaxPropertiesSuccessMessage(0) - }, instance, schemaLocation, localization, context)); + }, instance, schemaLocation, context)); } return successes; diff --git a/src/error-handlers/format.js b/src/error-handlers/format.js index 643427b..dc2ad46 100644 --- a/src/error-handlers/format.js +++ b/src/error-handlers/format.js @@ -18,7 +18,7 @@ const keywordUris = [ /** @type ErrorHandler */ const formatErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -31,7 +31,7 @@ const formatErrorHandler = { const format = /** @type string */ (getCompiledKeywordValue(context.ast, schemaLocation)); errors.push({ - message: localization.getFormatErrorMessage(format), + message: context.localization.getFormatErrorMessage(format), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); @@ -41,18 +41,18 @@ const formatErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { // A 'format' that's only an annotation doesn't require anything. If it's // not known, the message says that it only applies if formats are validated. return keywordUris.flatMap((keywordUri) => { return describeKeyword(normalizedOutput, keywordUri, instance, context.ast, (/** @type string */ format) => { switch (context.isFormatAsserted(keywordUri, format)) { case true: - return localization.getFormatSuccessMessage(format); + return context.localization.getFormatSuccessMessage(format); case false: return undefined; default: - return localization.getFormatIfValidatedSuccessMessage(format); + return context.localization.getFormatIfValidatedSuccessMessage(format); } }); }); diff --git a/src/error-handlers/ifThenElse.js b/src/error-handlers/ifThenElse.js index 8d6a78f..eb3ae68 100644 --- a/src/error-handlers/ifThenElse.js +++ b/src/error-handlers/ifThenElse.js @@ -9,7 +9,7 @@ import { /** * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" * @import { AST } from "@hyperjump/json-schema/experimental" - * @import { ErrorHandler, ErrorObject, InstanceOutput, Localization, NormalizedOutput } from "../index.d.ts" + * @import { ErrorHandler, ErrorHandlerContext, ErrorObject, InstanceOutput, NormalizedOutput } from "../index.d.ts" */ /** @type ErrorHandler */ @@ -17,7 +17,7 @@ const ifThenElseErrorHandler = { // Failures in 'then' and 'else' are flattened into the parent schema's results, // so they're reported by the handlers for the keywords that failed - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -29,22 +29,22 @@ const ifThenElseErrorHandler = { continue; } - /** @type (output: NormalizedOutput) => (localization: Localization) => ErrorObject[] */ - const describe = (output) => (localization) => getSuccesses(output, instance, localization, context); + /** @type (output: NormalizedOutput) => (context: ErrorHandlerContext) => ErrorObject[] */ + const describe = (output) => (context) => getSuccesses(output, instance, context); // Both branches are described, even if we know which way 'if' went, // because changing the value could change whether 'if' passes successes.push(...describeConditional({ - condition: (localization) => { - const description = describe(ifOutput)(localization); + condition: (context) => { + const description = describe(ifOutput)(context); // 'if' fails if any of its keywords fail - return localization.isNegated - ? someTrue(description.map((option) => [option]), instance, ifLocation, localization) + return context.localization.isNegated + ? someTrue(description.map((option) => [option]), instance, ifLocation, context) : description; }, then: thenOutput ? describe(thenOutput) : undefined, else: elseOutput ? describe(elseOutput) : undefined - }, instance, ifLocation, localization)); + }, instance, ifLocation, context)); } return successes; diff --git a/src/error-handlers/items.js b/src/error-handlers/items.js index 341440d..6f4f083 100644 --- a/src/error-handlers/items.js +++ b/src/error-handlers/items.js @@ -14,7 +14,7 @@ import { describeScope, getCompiledKeywordValue, getPlaceholder, isPlaceholder } * @type ErrorHandler */ const itemsErrorHandler = { - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -32,7 +32,7 @@ const itemsErrorHandler = { placeholder: getPlaceholder(instance, String(Math.max(startIndex, length))), each: (localization, count) => localization.getEachItemSuccessMessage(startIndex, count), none: (localization) => localization.getMaxItemsSuccessMessage(startIndex) - }, instance, schemaLocation, localization, context)); + }, instance, schemaLocation, context)); } return successes; diff --git a/src/error-handlers/maxItems.js b/src/error-handlers/maxItems.js index 5b530a4..3141550 100644 --- a/src/error-handlers/maxItems.js +++ b/src/error-handlers/maxItems.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const maxItemsErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; let lowestMaxItems = Infinity; @@ -28,7 +28,7 @@ const maxItemsErrorHandler = { if (lowestMaxItems != Infinity) { errors.push({ - message: localization.getMaxItemsErrorMessage(lowestMaxItems), + message: context.localization.getMaxItemsErrorMessage(lowestMaxItems), instanceLocation: Instance.uri(instance), schemaLocations: [effectiveSchemaLocation] }); @@ -37,9 +37,9 @@ const maxItemsErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxItems", instance, context.ast, (/** @type number */ maxItems) => { - return localization.getMaxItemsSuccessMessage(maxItems); + return context.localization.getMaxItemsSuccessMessage(maxItems); }); } }; diff --git a/src/error-handlers/maxLength.js b/src/error-handlers/maxLength.js index 463910e..a3a180e 100644 --- a/src/error-handlers/maxLength.js +++ b/src/error-handlers/maxLength.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const maxLengthErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; let lowestMaxLength = Infinity; @@ -27,7 +27,7 @@ const maxLengthErrorHandler = { } if (mostConstrainingLocation !== null) { errors.push({ - message: localization.getMaxLengthErrorMessage(lowestMaxLength), + message: context.localization.getMaxLengthErrorMessage(lowestMaxLength), instanceLocation: Instance.uri(instance), schemaLocations: [mostConstrainingLocation] }); @@ -36,9 +36,9 @@ const maxLengthErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxLength", instance, context.ast, (/** @type number */ maxLength) => { - return localization.getMaxLengthSuccessMessage(maxLength); + return context.localization.getMaxLengthSuccessMessage(maxLength); }); } }; diff --git a/src/error-handlers/maxProperties.js b/src/error-handlers/maxProperties.js index b9c48c7..bd44f4c 100644 --- a/src/error-handlers/maxProperties.js +++ b/src/error-handlers/maxProperties.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const maxPropertiesErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; let lowestMaxProperties = Infinity; @@ -26,7 +26,7 @@ const maxPropertiesErrorHandler = { } if (mostConstrainingLocation !== null) { errors.push({ - message: localization.getMaxPropertiesErrorMessage(lowestMaxProperties), + message: context.localization.getMaxPropertiesErrorMessage(lowestMaxProperties), instanceLocation: Instance.uri(instance), schemaLocations: [mostConstrainingLocation] }); @@ -35,9 +35,9 @@ const maxPropertiesErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxProperties", instance, context.ast, (/** @type number */ maxProperties) => { - return localization.getMaxPropertiesSuccessMessage(maxProperties); + return context.localization.getMaxPropertiesSuccessMessage(maxProperties); }); } }; diff --git a/src/error-handlers/maximum.js b/src/error-handlers/maximum.js index 4a19ab8..0a38857 100644 --- a/src/error-handlers/maximum.js +++ b/src/error-handlers/maximum.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue, getSiblingKeywordLocation } f /** @type ErrorHandler */ const maximumErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { let lowestMaximum = Infinity; let isExclusive = false; @@ -60,29 +60,29 @@ const maximumErrorHandler = { return []; } else if (isExclusive) { return [{ - message: localization.getExclusiveMaximumErrorMessage(lowestMaximum), + message: context.localization.getExclusiveMaximumErrorMessage(lowestMaximum), instanceLocation: Instance.uri(instance), schemaLocations: schemaLocations }]; } else { return [{ - message: localization.getMaximumErrorMessage(lowestMaximum), + message: context.localization.getMaximumErrorMessage(lowestMaximum), instanceLocation: Instance.uri(instance), schemaLocations: schemaLocations }]; } }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maximum", instance, context.ast, (/** @type number */ maximum) => { - return localization.getMaximumSuccessMessage(maximum); + return context.localization.getMaximumSuccessMessage(maximum); })); successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/exclusiveMaximum", instance, context.ast, (/** @type number */ exclusiveMaximum) => { - return localization.getExclusiveMaximumSuccessMessage(exclusiveMaximum); + return context.localization.getExclusiveMaximumSuccessMessage(exclusiveMaximum); })); // Draft-04 has a boolean 'exclusiveMaximum' keyword that modifies 'maximum' @@ -91,13 +91,13 @@ const maximumErrorHandler = { if (exclusive) { const exclusiveLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMaximum"); successes.push({ - message: localization.getExclusiveMaximumSuccessMessage(maximum), + message: context.localization.getExclusiveMaximumSuccessMessage(maximum), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation, exclusiveLocation] }); } else { successes.push({ - message: localization.getMaximumSuccessMessage(maximum), + message: context.localization.getMaximumSuccessMessage(maximum), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); diff --git a/src/error-handlers/minItems.js b/src/error-handlers/minItems.js index 3ba7193..1342226 100644 --- a/src/error-handlers/minItems.js +++ b/src/error-handlers/minItems.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const minItemsErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; let highestMinItem = 0; @@ -28,7 +28,7 @@ const minItemsErrorHandler = { if (highestMinItem != 0) { errors.push({ - message: localization.getMinItemsErrorMessage(highestMinItem), + message: context.localization.getMinItemsErrorMessage(highestMinItem), instanceLocation: Instance.uri(instance), schemaLocations: [effectiveSchemaLocation] }); @@ -37,9 +37,9 @@ const minItemsErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minItems", instance, context.ast, (/** @type number */ minItems) => { - return localization.getMinItemsSuccessMessage(minItems); + return context.localization.getMinItemsSuccessMessage(minItems); }); } }; diff --git a/src/error-handlers/minLength.js b/src/error-handlers/minLength.js index 2d1c247..b3e5e31 100644 --- a/src/error-handlers/minLength.js +++ b/src/error-handlers/minLength.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const minLengthErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; let highestMinLength = -Infinity; @@ -27,7 +27,7 @@ const minLengthErrorHandler = { } if (mostConstrainingLocation !== null) { errors.push({ - message: localization.getMinLengthErrorMessage(highestMinLength), + message: context.localization.getMinLengthErrorMessage(highestMinLength), instanceLocation: Instance.uri(instance), schemaLocations: [mostConstrainingLocation] }); @@ -36,9 +36,9 @@ const minLengthErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minLength", instance, context.ast, (/** @type number */ minLength) => { - return localization.getMinLengthSuccessMessage(minLength); + return context.localization.getMinLengthSuccessMessage(minLength); }); } }; diff --git a/src/error-handlers/minProperties.js b/src/error-handlers/minProperties.js index 6e1bc23..3c55594 100644 --- a/src/error-handlers/minProperties.js +++ b/src/error-handlers/minProperties.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const minPropertiesErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -29,7 +29,7 @@ const minPropertiesErrorHandler = { if (mostConstrainingLocation !== null) { errors.push({ - message: localization.getMinPropertiesErrorMessage(highestMinProperties), + message: context.localization.getMinPropertiesErrorMessage(highestMinProperties), instanceLocation: Instance.uri(instance), schemaLocations: [mostConstrainingLocation] }); @@ -38,9 +38,9 @@ const minPropertiesErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minProperties", instance, context.ast, (/** @type number */ minProperties) => { - return localization.getMinPropertiesSuccessMessage(minProperties); + return context.localization.getMinPropertiesSuccessMessage(minProperties); }); } }; diff --git a/src/error-handlers/minimum.js b/src/error-handlers/minimum.js index 8ec4cab..7828e37 100644 --- a/src/error-handlers/minimum.js +++ b/src/error-handlers/minimum.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue, getSiblingKeywordLocation } f /** @type ErrorHandler */ const minimumErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { let highestMinimum = -Infinity; let isExclusive = false; /** @type string[] */ @@ -61,29 +61,29 @@ const minimumErrorHandler = { return []; } else if (isExclusive) { return [{ - message: localization.getExclusiveMinimumErrorMessage(highestMinimum), + message: context.localization.getExclusiveMinimumErrorMessage(highestMinimum), instanceLocation: Instance.uri(instance), schemaLocations: schemaLocations }]; } else { return [{ - message: localization.getMinimumErrorMessage(highestMinimum), + message: context.localization.getMinimumErrorMessage(highestMinimum), instanceLocation: Instance.uri(instance), schemaLocations: schemaLocations }]; } }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minimum", instance, context.ast, (/** @type number */ minimum) => { - return localization.getMinimumSuccessMessage(minimum); + return context.localization.getMinimumSuccessMessage(minimum); })); successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/exclusiveMinimum", instance, context.ast, (/** @type number */ exclusiveMinimum) => { - return localization.getExclusiveMinimumSuccessMessage(exclusiveMinimum); + return context.localization.getExclusiveMinimumSuccessMessage(exclusiveMinimum); })); // Draft-04 has a boolean 'exclusiveMinimum' keyword that modifies 'minimum' @@ -92,13 +92,13 @@ const minimumErrorHandler = { if (exclusive) { const exclusiveLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMinimum"); successes.push({ - message: localization.getExclusiveMinimumSuccessMessage(minimum), + message: context.localization.getExclusiveMinimumSuccessMessage(minimum), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation, exclusiveLocation] }); } else { successes.push({ - message: localization.getMinimumSuccessMessage(minimum), + message: context.localization.getMinimumSuccessMessage(minimum), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); diff --git a/src/error-handlers/multipleOf.js b/src/error-handlers/multipleOf.js index 53d4a85..571717e 100644 --- a/src/error-handlers/multipleOf.js +++ b/src/error-handlers/multipleOf.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const multipleOfErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -30,7 +30,7 @@ const multipleOfErrorHandler = { if (combinedMultipleOf !== null && hasError) { errors.push({ - message: localization.getMultipleOfErrorMessage(combinedMultipleOf), + message: context.localization.getMultipleOfErrorMessage(combinedMultipleOf), instanceLocation: Instance.uri(instance), schemaLocations }); @@ -39,9 +39,9 @@ const multipleOfErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/multipleOf", instance, context.ast, (/** @type number */ multipleOf) => { - return localization.getMultipleOfSuccessMessage(multipleOf); + return context.localization.getMultipleOfSuccessMessage(multipleOf); }); } }; diff --git a/src/error-handlers/not.js b/src/error-handlers/not.js index f8da16d..9ac6143 100644 --- a/src/error-handlers/not.js +++ b/src/error-handlers/not.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { allTrue, getSuccesses, isAllTrueGroup, limitOptions, someTrue } from "../json-schema-errors.js"; +import { allTrue, getSuccesses, isAllTrueGroup, limitOptions, negate, someTrue } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -7,7 +7,7 @@ import { allTrue, getSuccesses, isAllTrueGroup, limitOptions, someTrue } from ". /** @type ErrorHandler */ const notErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -19,29 +19,29 @@ const notErrorHandler = { // The 'not' schema passed. Describe what would make it fail so the user // knows what needs to change. At least one of these needs to be true. - const negatedLocalization = localization.negated(); + const negatedContext = negate(context); const options = (not.outputs ?? []).flatMap((notOutput) => { - return getSuccesses(notOutput, instance, negatedLocalization, context); + return getSuccesses(notOutput, instance, negatedContext); }); if (options.length === 0) { // Nothing to describe means the 'not' schema allows any value errors.push({ - message: localization.getBooleanSchemaErrorMessage(), + message: context.localization.getBooleanSchemaErrorMessage(), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); } else if (options.length === 1 && isAllTrueGroup(options[0])) { errors.push({ - message: localization.getNotErrorMessage("all"), + message: context.localization.getNotErrorMessage("all"), alternatives: /** @type ErrorObject[][] */ (options[0].alternatives), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); } else { errors.push({ - message: localization.getNotErrorMessage(options.length === 1 ? "one" : "some"), - alternatives: limitOptions(options.map((option) => [option]), instance, localization), + message: context.localization.getNotErrorMessage(options.length === 1 ? "one" : "some"), + alternatives: limitOptions(options.map((option) => [option]), instance, context), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); @@ -51,7 +51,7 @@ const notErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -59,16 +59,16 @@ const notErrorHandler = { const not = normalizedOutput["https://json-schema.org/keyword/not"][schemaLocation]; for (const notOutput of not.outputs ?? []) { - if (localization.isNegated) { + if (context.localization.isNegated) { // 'not' fails if its schema passes, which requires all of its keywords to pass - const requirements = getSuccesses(notOutput, instance, localization.negated(), context); - successes.push(...allTrue(requirements, instance, schemaLocation, localization)); + const requirements = getSuccesses(notOutput, instance, negate(context)); + successes.push(...allTrue(requirements, instance, schemaLocation, context)); } else { // 'not' passes if at least one of its schema's keywords fails. All of them // are described, even if we know which ones fail, because these // descriptions tell the user what would need to change to make 'not' fail. - const options = getSuccesses(notOutput, instance, localization.negated(), context); - successes.push(...someTrue(options.map((option) => [option]), instance, schemaLocation, localization)); + const options = getSuccesses(notOutput, instance, negate(context)); + successes.push(...someTrue(options.map((option) => [option]), instance, schemaLocation, context)); } } } diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index 6bfcc82..40b504a 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -1,15 +1,15 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import * as Pact from "@hyperjump/pact"; -import { allowsAnyValue, allTrue, countTrue, flattenOutput, getCompiledKeywordValue, getErrors, getSuccesses, isPassing, limitItems, limitOptions, someTrue } from "../json-schema-errors.js"; +import { allowsAnyValue, allTrue, countTrue, flattenOutput, getCompiledKeywordValue, getErrors, getSuccesses, isPassing, limitItems, limitOptions, negate, someTrue } from "../json-schema-errors.js"; /** * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" - * @import { ErrorHandler, ErrorHandlerContext, ErrorObject, InstanceOutput, Localization, NormalizedOutput } from "../index.d.ts" + * @import { ErrorHandler, ErrorHandlerContext, ErrorObject, InstanceOutput, NormalizedOutput } from "../index.d.ts" */ /** @type ErrorHandler */ const oneOfErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -25,7 +25,7 @@ const oneOfErrorHandler = { return isPassing(oneOf[index]) ? [{ alternativeLocation, output: oneOf[index] }] : []; }); if (matches.length > 1) { - errors.push(multipleMatchesError(matches, schemaLocation, instance, localization, context)); + errors.push(multipleMatchesError(matches, schemaLocation, instance, context)); continue; } @@ -65,7 +65,7 @@ const oneOfErrorHandler = { } // The alternative passed all the filters - const alternativeErrors = getErrors(alternative, instance, localization, context); + const alternativeErrors = getErrors(alternative, instance, context); if (alternativeErrors.length) { alternatives.push(alternativeErrors); } @@ -74,7 +74,7 @@ const oneOfErrorHandler = { // If all alternatives were filtered out, default to returning all of them if (alternatives.length === 0) { for (const alternative of oneOf) { - const alternativeErrors = getErrors(alternative, instance, localization, context); + const alternativeErrors = getErrors(alternative, instance, context); if (alternativeErrors.length) { alternatives.push(alternativeErrors); } @@ -86,7 +86,7 @@ const oneOfErrorHandler = { } else { /** @type ErrorObject */ const alternativeErrors = { - message: localization.getOneOfErrorMessage(), + message: context.localization.getOneOfErrorMessage(), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }; @@ -100,40 +100,40 @@ const oneOfErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/oneOf"]) { const alternatives = normalizedOutput["https://json-schema.org/keyword/oneOf"][schemaLocation].outputs ?? []; - if (localization.isNegated) { + if (context.localization.isNegated) { // 'oneOf' fails if no alternatives match or more than one matches. Changing // the value could change which alternatives match, so all of them need to // be described even if we know which one matches now. // Make all alternatives fail - const alternativeOptions = alternatives.map((alternative) => getSuccesses(alternative, instance, localization, context)); + const alternativeOptions = alternatives.map((alternative) => getSuccesses(alternative, instance, context)); if (alternativeOptions.every((alternativeOption) => alternativeOption.length > 0)) { const requirements = alternativeOptions.flatMap((alternativeOption) => { - return someTrue(alternativeOption.map((option) => [option]), instance, schemaLocation, localization); + return someTrue(alternativeOption.map((option) => [option]), instance, schemaLocation, context); }); - successes.push(...allTrue(requirements, instance, schemaLocation, localization)); + successes.push(...allTrue(requirements, instance, schemaLocation, context)); } // Make at least two alternatives match - const descriptions = alternatives.map((alternative) => getSuccesses(alternative, instance, localization.negated(), context)); + const descriptions = alternatives.map((alternative) => getSuccesses(alternative, instance, negate(context))); if (descriptions.every((description) => description.length > 0)) { - const requirements = countTrue(descriptions, { min: 2 }, instance, schemaLocation, localization.negated()); - successes.push(...allTrue(requirements, instance, schemaLocation, localization)); + const requirements = countTrue(descriptions, { min: 2 }, instance, schemaLocation, negate(context)); + successes.push(...allTrue(requirements, instance, schemaLocation, context)); } } else { // Passes if exactly one alternative passes. All of them are described, even // if we know which one matches, because these descriptions tell the user // what would need to change to make 'oneOf' fail. - const descriptions = alternatives.map((alternative) => getSuccesses(alternative, instance, localization, context)); + const descriptions = alternatives.map((alternative) => getSuccesses(alternative, instance, context)); if (descriptions.every((description) => description.length > 0)) { - successes.push(...countTrue(descriptions, { min: 1, max: 1 }, instance, schemaLocation, localization)); + successes.push(...countTrue(descriptions, { min: 1, max: 1 }, instance, schemaLocation, context)); } } } @@ -146,14 +146,14 @@ const oneOfErrorHandler = { * More than one alternative passed. Describe how the instance satisfied each * matching alternative so the user knows what needs to change. * - * @type (matches: { alternativeLocation: string, output: NormalizedOutput }[], schemaLocation: string, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject + * @type (matches: { alternativeLocation: string, output: NormalizedOutput }[], schemaLocation: string, instance: JsonNode, context: ErrorHandlerContext) => ErrorObject */ -const multipleMatchesError = (matches, schemaLocation, instance, localization, context) => { +const multipleMatchesError = (matches, schemaLocation, instance, context) => { const alternatives = matches.map(({ alternativeLocation, output }) => { - const description = getSuccesses(output, instance, localization, context); + const description = getSuccesses(output, instance, context); if (description.length === 0 && allowsAnyValue(alternativeLocation, context.ast)) { return [{ - message: localization.getAnyValueMessage(), + message: context.localization.getAnyValueMessage(), instanceLocation: Instance.uri(instance), schemaLocations: [alternativeLocation] }]; @@ -165,18 +165,18 @@ const multipleMatchesError = (matches, schemaLocation, instance, localization, c // If any match can't be described, the alternatives would be misleading if (alternatives.some((alternative) => alternative.length === 0)) { return { - message: localization.getOneOfTooManyErrorMessage(), + message: context.localization.getOneOfTooManyErrorMessage(), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }; } return { - message: localization.getOneOfMultipleMatchesErrorMessage(), + message: context.localization.getOneOfMultipleMatchesErrorMessage(), // Options are shown in the same order as the alternatives alternatives: limitOptions(removeCommonSuccesses(alternatives).map((alternative) => { - return limitItems(alternative, instance, localization); - }), instance, localization, false), + return limitItems(alternative, instance, context); + }), instance, context, false), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }; diff --git a/src/error-handlers/pattern.js b/src/error-handlers/pattern.js index 3c69e9e..c98b1d9 100644 --- a/src/error-handlers/pattern.js +++ b/src/error-handlers/pattern.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const patternErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -20,7 +20,7 @@ const patternErrorHandler = { const pattern = compiledPattern.source; errors.push({ - message: localization.getPatternErrorMessage(pattern), + message: context.localization.getPatternErrorMessage(pattern), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); @@ -29,9 +29,9 @@ const patternErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/pattern", instance, context.ast, (/** @type RegExp */ pattern) => { - return localization.getPatternSuccessMessage(pattern.source); + return context.localization.getPatternSuccessMessage(pattern.source); }); } }; diff --git a/src/error-handlers/prefixItems.js b/src/error-handlers/prefixItems.js index 59e34e0..07e31aa 100644 --- a/src/error-handlers/prefixItems.js +++ b/src/error-handlers/prefixItems.js @@ -20,7 +20,7 @@ import { * @type ErrorHandler */ const prefixItemsErrorHandler = { - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -47,13 +47,13 @@ const prefixItemsErrorHandler = { const item = getPlaceholder(instance, String(index)); const output = evaluateRequirements(prefixItems[index], item, context.ast); successes.push(...describeConditional({ - condition: (localization) => [{ - message: localization.getHasItemSuccessMessage(index), + condition: (context) => [{ + message: context.localization.getHasItemSuccessMessage(index), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }], - then: (localization) => getSuccesses(output, instance, localization, context) - }, instance, schemaLocation, localization)); + then: (context) => getSuccesses(output, instance, context) + }, instance, schemaLocation, context)); } } diff --git a/src/error-handlers/properties.js b/src/error-handlers/properties.js index 70b7b01..6b6d4ef 100644 --- a/src/error-handlers/properties.js +++ b/src/error-handlers/properties.js @@ -19,7 +19,7 @@ import { * @type ErrorHandler */ const propertiesErrorHandler = { - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -42,13 +42,13 @@ const propertiesErrorHandler = { const property = getPlaceholder(instance, propertyName); const output = evaluateRequirements(properties[propertyName], property, context.ast); successes.push(...describeConditional({ - condition: (localization) => [{ - message: localization.getHasPropertySuccessMessage([propertyName]), + condition: (context) => [{ + message: context.localization.getHasPropertySuccessMessage([propertyName]), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }], - then: (localization) => getSuccesses(output, instance, localization, context) - }, instance, schemaLocation, localization)); + then: (context) => getSuccesses(output, instance, context) + }, instance, schemaLocation, context)); } } diff --git a/src/error-handlers/required.js b/src/error-handlers/required.js index 0f0b020..2b1e403 100644 --- a/src/error-handlers/required.js +++ b/src/error-handlers/required.js @@ -8,7 +8,7 @@ import { getCompiledKeywordValue } from "../json-schema-errors.js"; /** @type ErrorHandler */ const requiredErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type {Set} */ const allMissingRequired = new Set(); const allSchemaLocations = []; @@ -68,13 +68,13 @@ const requiredErrorHandler = { } return [{ - message: localization.getRequiredErrorMessage([...allMissingRequired]), + message: context.localization.getRequiredErrorMessage([...allMissingRequired]), instanceLocation: Instance.uri(instance), schemaLocations: /** @type {string[]} */ ([...allSchemaLocations]) }]; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -91,7 +91,7 @@ const requiredErrorHandler = { if (allRequired.size > 0) { successes.push({ - message: localization.getRequiredSuccessMessage([...allRequired]), + message: context.localization.getRequiredSuccessMessage([...allRequired]), instanceLocation: Instance.uri(instance), schemaLocations: requiredSchemaLocations }); @@ -102,7 +102,7 @@ const requiredErrorHandler = { for (const [propertyName, requiredProperties] of dependencies) { if (requiredProperties.length > 0) { successes.push({ - message: localization.getDependentRequiredSuccessMessage(propertyName, requiredProperties), + message: context.localization.getDependentRequiredSuccessMessage(propertyName, requiredProperties), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); @@ -115,7 +115,7 @@ const requiredErrorHandler = { for (const [propertyName, dependency] of dependencies) { if (Array.isArray(dependency) && dependency.length > 0) { successes.push({ - message: localization.getDependentRequiredSuccessMessage(propertyName, /** @type {string[]} */ (dependency)), + message: context.localization.getDependentRequiredSuccessMessage(propertyName, /** @type {string[]} */ (dependency)), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); diff --git a/src/error-handlers/typeConstEnum.js b/src/error-handlers/typeConstEnum.js index 7ba8c34..8ce6abe 100644 --- a/src/error-handlers/typeConstEnum.js +++ b/src/error-handlers/typeConstEnum.js @@ -9,7 +9,7 @@ const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "obje /** @type {ErrorHandler} */ const typeConstEnumErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { let allowedTypes = new Set(ALL_TYPES); /** @type {string[]} */ const failedTypeLocations = []; @@ -88,7 +88,7 @@ const typeConstEnumErrorHandler = { return []; } else if (allowedTypes.size === 0 || allowedJson?.size === 0) { return [{ - message: localization.getBooleanSchemaErrorMessage(), + message: context.localization.getBooleanSchemaErrorMessage(), instanceLocation: Instance.uri(instance), schemaLocations: [...failedTypeLocations, ...constEnumLocations] }]; @@ -97,7 +97,7 @@ const typeConstEnumErrorHandler = { const allowedValues = [...allowedJson ?? []].map((json) => JSON.parse(json)); return [{ - message: localization.getEnumErrorMessage(allowedValues), + message: context.localization.getEnumErrorMessage(allowedValues), instanceLocation: Instance.uri(instance), schemaLocations: typeFiltered ? [...failedTypeLocations, ...constEnumLocations] @@ -105,23 +105,23 @@ const typeConstEnumErrorHandler = { }]; } else { return [{ - message: localization.getTypeErrorMessage([...allowedTypes]), + message: context.localization.getTypeErrorMessage([...allowedTypes]), instanceLocation: Instance.uri(instance), schemaLocations: failedTypeLocations }]; } }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { return [ ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/type", instance, context.ast, (/** @type string | string[] */ type) => { - return localization.getTypeSuccessMessage(Array.isArray(type) ? type : [type]); + return context.localization.getTypeSuccessMessage(Array.isArray(type) ? type : [type]); }), ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/const", instance, context.ast, (/** @type string */ constJson) => { - return localization.getEnumSuccessMessage([JSON.parse(constJson)]); + return context.localization.getEnumSuccessMessage([JSON.parse(constJson)]); }), ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/enum", instance, context.ast, (/** @type string[] */ enumJson) => { - return localization.getEnumSuccessMessage(enumJson.map((json) => JSON.parse(json))); + return context.localization.getEnumSuccessMessage(enumJson.map((json) => JSON.parse(json))); }) ]; } diff --git a/src/error-handlers/uniqueItems.js b/src/error-handlers/uniqueItems.js index 02c9313..9f46371 100644 --- a/src/error-handlers/uniqueItems.js +++ b/src/error-handlers/uniqueItems.js @@ -8,7 +8,7 @@ import jsonStringify from "json-stringify-deterministic"; /** @type ErrorHandler */ const uniqueItemsErrorHandler = { - error: (normalizedErrors, instance, localization) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -30,7 +30,7 @@ const uniqueItemsErrorHandler = { if (itemCounts[key].count > 1) { for (const instanceLocation of itemCounts[key].instanceLocations) { errors.push({ - message: localization.getUniqueItemsErrorMessage(), + message: context.localization.getUniqueItemsErrorMessage(), instanceLocation: instanceLocation, schemaLocations: [schemaLocation] }); @@ -42,10 +42,10 @@ const uniqueItemsErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { // 'uniqueItems: false' allows anything, so there's nothing to say return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/uniqueItems", instance, context.ast, (/** @type boolean */ uniqueItems) => { - return uniqueItems ? localization.getUniqueItemsSuccessMessage() : undefined; + return uniqueItems ? context.localization.getUniqueItemsSuccessMessage() : undefined; }); } }; diff --git a/src/error-handlers/unknown.js b/src/error-handlers/unknown.js index 48b58af..0187234 100644 --- a/src/error-handlers/unknown.js +++ b/src/error-handlers/unknown.js @@ -14,7 +14,7 @@ import { pointerSegments } from "@hyperjump/json-pointer"; * @type ErrorHandler */ const unknownErrorHandler = { - error: (normalizedErrors, instance, localization) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -26,7 +26,7 @@ const unknownErrorHandler = { const keyword = /** @type string */ ([...pointerSegments(decodeURI(schemaLocation.split("#")[1]))].pop()); errors.push({ - message: localization.getUnknownErrorMessage(keyword), + message: context.localization.getUnknownErrorMessage(keyword), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); diff --git a/src/index.d.ts b/src/index.d.ts index 1d1b4e3..f5db99c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -199,16 +199,24 @@ export const removeErrorHandler: (errorHandlerUri: string) => void; * passing, such as with `not`. A handler can have either or both. */ export type ErrorHandler = { - error?: (normalizedErrors: InstanceOutput, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[]; - success?: (normalizedOutput: InstanceOutput, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[]; + error?: (normalizedErrors: InstanceOutput, instance: JsonNode, context: ErrorHandlerContext) => ErrorObject[]; + success?: (normalizedOutput: InstanceOutput, instance: JsonNode, context: ErrorHandlerContext) => ErrorObject[]; }; /** - * What error handlers need to know about the schema and how it was validated. + * What error handlers need to know about the schema, how it was validated, and + * how to write messages. */ export type ErrorHandlerContext = { ast: AST; + /** + * Builds messages in the requested locale. In a negated context, success + * messages describe what would make keywords fail instead of what they + * require. + */ + localization: Localization; + /** * Whether the validator treated a `format` keyword as an assertion rather * than an annotation. It can depend on the dialect, given by the keyword @@ -239,14 +247,22 @@ export const getCompiledKeywordValue: (ast: AST, schemaLocation: string) => unkn * Converts the normalized error format to human readable errors. It's used to * build errors in applicator error handlers. */ -export const getErrors: (normalizedErrors: NormalizedOutput, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[]; +export const getErrors: (normalizedErrors: NormalizedOutput, instance: JsonNode, context: ErrorHandlerContext) => ErrorObject[]; /** * Converts the normalized output of a passing subschema to human readable * messages describing how the instance satisfied the subschema. It's used to * build errors in applicator error handlers that fail when a subschema passes. */ -export const getSuccesses: (normalizedOutput: NormalizedOutput, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[]; +export const getSuccesses: (normalizedOutput: NormalizedOutput, instance: JsonNode, context: ErrorHandlerContext) => ErrorObject[]; + +/** + * A view of the context where success messages describe what would make + * keywords fail instead of what they require. It's used to explain failures of + * keywords like `not` that fail when a subschema passes. Negating a negated + * context gives back the original meaning. + */ +export const negate: (context: ErrorHandlerContext) => ErrorHandlerContext; export type { Localization }; diff --git a/src/index.js b/src/index.js index 98f0602..5414e06 100644 --- a/src/index.js +++ b/src/index.js @@ -192,6 +192,7 @@ export { getErrors, getSuccesses, jsonSchemaErrors, + negate, removeErrorHandler, setErrorHandler, setNormalizationHandler diff --git a/src/json-schema-errors.js b/src/json-schema-errors.js index b7aab9b..996b023 100644 --- a/src/json-schema-errors.js +++ b/src/json-schema-errors.js @@ -23,9 +23,9 @@ export const jsonSchemaErrors = async (errorOutput, schemaUri, instance, options errorIndex, plugins: [...ast.plugins] }); - const localization = Localization.forLocale(options.locale ?? "en-US"); - return getErrors(normalizedErrors, rootInstance, localization, { + return getErrors(normalizedErrors, rootInstance, { ast, + localization: Localization.forLocale(options.locale ?? "en-US"), isFormatAsserted: () => options.isFormatAsserted }); }; @@ -267,17 +267,20 @@ export const evaluateRequirements = (schemaLocation, instance, ast) => { }); }; +/** @type API.negate */ +export const negate = (context) => ({ ...context, localization: context.localization.negated() }); + /** * Describes a subschema that applies to every location in some scope, such as * every item in an array. The subschema is described at a placeholder that * stands in for any of those locations and then the description is moved to * the parent because it applies to all of them. * - * @type (subschemaLocation: string, placeholder: JsonNode, parent: JsonNode, localization: Localization, context: API.ErrorHandlerContext) => API.ErrorObject[] + * @type (subschemaLocation: string, placeholder: JsonNode, parent: JsonNode, context: API.ErrorHandlerContext) => API.ErrorObject[] */ -export const describeEach = (subschemaLocation, placeholder, parent, localization, context) => { +export const describeEach = (subschemaLocation, placeholder, parent, context) => { const output = evaluateRequirements(subschemaLocation, placeholder, context.ast); - return getSuccesses(output, parent, localization, context) + return getSuccesses(output, parent, context) .map((success) => relocate(success, Instance.uri(placeholder), Instance.uri(parent))); }; @@ -297,28 +300,28 @@ export const describeEach = (subschemaLocation, placeholder, parent, localizatio * the group of what each location requires, given how many things are in it, and `none` describes the scope * being empty, which is what the subschema requires if it's `false`. * - * @type (scope: Scope, instance: JsonNode, schemaLocation: string, localization: Localization, context: API.ErrorHandlerContext) => API.ErrorObject[] + * @type (scope: Scope, instance: JsonNode, schemaLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] */ -export const describeScope = ({ subschemaLocation, placeholder, each, none }, instance, schemaLocation, localization, context) => { +export const describeScope = ({ subschemaLocation, placeholder, each, none }, instance, schemaLocation, context) => { if (context.ast[subschemaLocation] === false) { return [{ - message: none(localization), + message: none(context.localization), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }]; } - const description = describeEach(subschemaLocation, placeholder, instance, localization, context); + const description = describeEach(subschemaLocation, placeholder, instance, context); if (description.length === 0) { return []; } return [{ - message: each(localization, description.length), + message: each(context.localization, description.length), // Every location satisfies all of them or there's one that satisfies at least one - alternatives: localization.isNegated - ? limitOptions(description.map((option) => [option]), instance, localization) - : [limitItems(description, instance, localization)], + alternatives: context.localization.isNegated + ? limitOptions(description.map((option) => [option]), instance, context) + : [limitItems(description, instance, context)], instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }]; @@ -326,59 +329,59 @@ export const describeScope = ({ subschemaLocation, placeholder, each, none }, in /** * @typedef {{ - * condition: (localization: Localization) => API.ErrorObject[]; - * then?: (localization: Localization) => API.ErrorObject[]; - * else?: (localization: Localization) => API.ErrorObject[]; + * condition: (context: API.ErrorHandlerContext) => API.ErrorObject[]; + * then?: (context: API.ErrorHandlerContext) => API.ErrorObject[]; + * else?: (context: API.ErrorHandlerContext) => API.ErrorObject[]; * }} Conditional */ /** * Describes subschemas that only apply under some condition, such as a * property's subschema only applying if the property is present. Each function - * describes its part using the given localization, so `condition` describes the - * condition holding, or with a negated localization, not holding. + * describes its part using the given context, so `condition` describes the + * condition holding, or with a negated context, not holding. * - * @type (conditional: Conditional, instance: JsonNode, schemaLocation: string, localization: Localization) => API.ErrorObject[] + * @type (conditional: Conditional, instance: JsonNode, schemaLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] */ -export const describeConditional = (conditional, instance, schemaLocation, localization) => { - const positive = localization.isNegated ? localization.negated() : localization; - const negated = positive.negated(); +export const describeConditional = (conditional, instance, schemaLocation, context) => { + const positive = context.localization.isNegated ? negate(context) : context; + const negated = negate(positive); /** @type (descriptions: API.ErrorObject[]) => API.ErrorObject[] */ - const asOptions = (descriptions) => someTrue(descriptions.map((option) => [option]), instance, schemaLocation, localization); + const asOptions = (descriptions) => someTrue(descriptions.map((option) => [option]), instance, schemaLocation, context); - if (localization.isNegated) { + if (context.localization.isNegated) { // Fails if the condition holds and 'then' fails or if the condition doesn't // hold and 'else' fails /** @type API.ErrorObject[] */ const options = []; - const thenOptions = conditional.then?.(localization) ?? []; + const thenOptions = conditional.then?.(context) ?? []; if (thenOptions.length > 0) { const requirements = [...conditional.condition(positive), ...asOptions(thenOptions)]; - options.push(...allTrue(requirements, instance, schemaLocation, localization)); + options.push(...allTrue(requirements, instance, schemaLocation, context)); } - const elseOptions = conditional.else?.(localization) ?? []; + const elseOptions = conditional.else?.(context) ?? []; if (elseOptions.length > 0) { const requirements = [...conditional.condition(negated), ...asOptions(elseOptions)]; - options.push(...allTrue(requirements, instance, schemaLocation, localization)); + options.push(...allTrue(requirements, instance, schemaLocation, context)); } return options; } else if (conditional.else) { // Passes if the condition holds and 'then' passes or if it doesn't and 'else' passes - const thenOption = [...conditional.condition(positive), ...conditional.then?.(localization) ?? []]; - const elseOption = [...conditional.condition(negated), ...conditional.else(localization)]; - return someTrue([thenOption, elseOption], instance, schemaLocation, localization); + const thenOption = [...conditional.condition(positive), ...conditional.then?.(context) ?? []]; + const elseOption = [...conditional.condition(negated), ...conditional.else(context)]; + return someTrue([thenOption, elseOption], instance, schemaLocation, context); } else { // Passes if the condition doesn't hold or 'then' passes - const description = conditional.then?.(localization) ?? []; + const description = conditional.then?.(context) ?? []; if (description.length === 0) { return []; } - return someTrue([conditional.condition(negated), description], instance, schemaLocation, localization); + return someTrue([conditional.condition(negated), description], instance, schemaLocation, context); } }; @@ -413,7 +416,7 @@ export const removeErrorHandler = (errorHandlerUri) => { }; /** @type API.getErrors */ -export const getErrors = (normalizedErrors, rootInstance, localization, context) => { +export const getErrors = (normalizedErrors, rootInstance, context) => { /** @type API.ErrorObject[] */ const errors = []; @@ -421,7 +424,7 @@ export const getErrors = (normalizedErrors, rootInstance, localization, context) for (const instanceLocation in reportedOutput) { const instance = /** @type JsonNode */ (Instance.get(instanceLocation, rootInstance)); for (const errorHandlerUri in errorHandlers) { - const errorObjects = errorHandlers[errorHandlerUri].error?.(reportedOutput[instanceLocation], instance, localization, context) ?? []; + const errorObjects = errorHandlers[errorHandlerUri].error?.(reportedOutput[instanceLocation], instance, context) ?? []; errors.push(...errorObjects); } } @@ -481,13 +484,13 @@ const isApplied = () => true; const isRequired = (keywordUri) => !normalizationHandlers[toAbsoluteIri(keywordUri)]?.conditional; /** @type API.getSuccesses */ -export const getSuccesses = (normalizedOutput, rootInstance, localization, context) => { +export const getSuccesses = (normalizedOutput, rootInstance, context) => { // Descriptions of nested subschemas can get very large, so stop describing // past some depth and say that there's more if (descriptionDepth >= MAX_DESCRIPTION_DEPTH) { /** @type API.ErrorObject */ const detailsNotShown = { - message: localization.getDetailsNotShownMessage(), + message: context.localization.getDetailsNotShownMessage(), instanceLocation: Instance.uri(rootInstance), schemaLocations: [] }; @@ -507,7 +510,7 @@ export const getSuccesses = (normalizedOutput, rootInstance, localization, conte const instance = getInstance(instanceLocation, rootInstance) ?? toPlaceholder(instanceLocation, rootInstance); for (const errorHandlerUri in errorHandlers) { - const successObjects = errorHandlers[errorHandlerUri].success?.(describedOutput[instanceLocation], instance, localization, context) ?? []; + const successObjects = errorHandlers[errorHandlerUri].success?.(describedOutput[instanceLocation], instance, context) ?? []; successes.push(...successObjects); } } @@ -554,15 +557,15 @@ const MAX_ENTRIES = 5; * A group of things that all need to be true shows only the first few. The * rest are summarized so it's clear that the list isn't complete. * - * @type (allItems: API.ErrorObject[], instance: JsonNode, localization: Localization) => API.ErrorObject[] + * @type (allItems: API.ErrorObject[], instance: JsonNode, context: API.ErrorHandlerContext) => API.ErrorObject[] */ -export const limitItems = (allItems, instance, localization) => { +export const limitItems = (allItems, instance, context) => { const items = collapseDetailsNotShown(allItems); if (items.length <= MAX_ENTRIES) { return items; } - return [...items.slice(0, MAX_ENTRIES), notShown(items.length - MAX_ENTRIES, instance, localization)]; + return [...items.slice(0, MAX_ENTRIES), notShown(items.length - MAX_ENTRIES, instance, context)]; }; /** @@ -570,9 +573,9 @@ export const limitItems = (allItems, instance, localization) => { * are shown first so the simplest options are the ones that are shown. The * order of options doesn't change what they mean. * - * @type (allOptions: API.ErrorObject[][], instance: JsonNode, localization: Localization, isSorted?: boolean) => API.ErrorObject[][] + * @type (allOptions: API.ErrorObject[][], instance: JsonNode, context: API.ErrorHandlerContext, isSorted?: boolean) => API.ErrorObject[][] */ -export const limitOptions = (allOptions, instance, localization, isSorted = true) => { +export const limitOptions = (allOptions, instance, context, isSorted = true) => { // Only one option that's just "details aren't shown" is needed let hasDetailsNotShown = false; const options = allOptions.map(collapseDetailsNotShown).filter((option) => { @@ -588,12 +591,12 @@ export const limitOptions = (allOptions, instance, localization, isSorted = true return sorted; } - return [...sorted.slice(0, MAX_ENTRIES), [notShown(sorted.length - MAX_ENTRIES, instance, localization)]]; + return [...sorted.slice(0, MAX_ENTRIES), [notShown(sorted.length - MAX_ENTRIES, instance, context)]]; }; -/** @type (count: number, instance: JsonNode, localization: Localization) => API.ErrorObject */ -const notShown = (count, instance, localization) => ({ - message: localization.getNotShownMessage(count), +/** @type (count: number, instance: JsonNode, context: API.ErrorHandlerContext) => API.ErrorObject */ +const notShown = (count, instance, context) => ({ + message: context.localization.getNotShownMessage(count), instanceLocation: Instance.uri(instance), schemaLocations: [] }); @@ -645,9 +648,9 @@ const allTrueGroups = new WeakSet(); * are described by a choice of options. Present the options as a group that * says how many of the options are true. * - * @type (options: API.ErrorObject[][], range: { min?: number, max?: number }, instance: JsonNode, schemaLocation: string, localization: Localization) => API.ErrorObject[] + * @type (options: API.ErrorObject[][], range: { min?: number, max?: number }, instance: JsonNode, schemaLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] */ -export const countTrue = (options, { min = 0, max = Infinity }, instance, schemaLocation, localization) => { +export const countTrue = (options, { min = 0, max = Infinity }, instance, schemaLocation, context) => { max = Math.min(max, options.length); if (options.length === 0 || (min <= 0 && max === options.length)) { @@ -655,37 +658,37 @@ export const countTrue = (options, { min = 0, max = Infinity }, instance, schema return []; } else if (min === options.length) { // All of the options are true - return limitItems(options.flat(), instance, localization); + return limitItems(options.flat(), instance, context); } return [{ - message: localization.getCountTrueMessage(min, max === options.length ? Infinity : max), - alternatives: limitOptions(options.map((option) => limitItems(option, instance, localization)), instance, localization), + message: context.localization.getCountTrueMessage(min, max === options.length ? Infinity : max), + alternatives: limitOptions(options.map((option) => limitItems(option, instance, context)), instance, context), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }]; }; -/** @type (options: API.ErrorObject[][], instance: JsonNode, schemaLocation: string, localization: Localization) => API.ErrorObject[] */ -export const someTrue = (options, instance, schemaLocation, localization) => { - return countTrue(options, { min: 1 }, instance, schemaLocation, localization); +/** @type (options: API.ErrorObject[][], instance: JsonNode, schemaLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] */ +export const someTrue = (options, instance, schemaLocation, context) => { + return countTrue(options, { min: 1 }, instance, schemaLocation, context); }; /** * Negated success messages are a list of things where at least one is true, but * some keywords need several things to be true. Present those as a group. * - * @type (items: API.ErrorObject[], instance: JsonNode, schemaLocation: string, localization: Localization) => API.ErrorObject[] + * @type (items: API.ErrorObject[], instance: JsonNode, schemaLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] */ -export const allTrue = (items, instance, schemaLocation, localization) => { +export const allTrue = (items, instance, schemaLocation, context) => { if (items.length <= 1) { return items; } /** @type API.ErrorObject */ const group = { - message: localization.getAllTrueMessage(), - alternatives: [limitItems(items, instance, localization)], + message: context.localization.getAllTrueMessage(), + alternatives: [limitItems(items, instance, context)], instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }; diff --git a/src/output-format.js b/src/output-format.js index e643363..92336a4 100644 --- a/src/output-format.js +++ b/src/output-format.js @@ -16,9 +16,9 @@ const jseOutputFormatHandler = (options) => { return { plugin, getErrors: (instance, context) => { - const localization = Localization.forLocale(options.locale ?? "en-US"); - return getErrors(plugin.output, instance, localization, { + return getErrors(plugin.output, instance, { ast: context.ast, + localization: Localization.forLocale(options.locale ?? "en-US"), isFormatAsserted }); } From c9d4a695b22b52990e82d9d7c1c91733850236b6 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Mon, 5 Oct 2026 13:31:32 -0700 Subject: [PATCH 4/6] Add translations for custom keywords and locales Messages could only come from the translations included in the package, so custom keywords had to hard-code English messages and there was no way to change a message or add a locale. addTranslation adds Fluent messages to a locale, creating the locale if it doesn't exist. A message with the same id as an existing message replaces it. Localization has public format and formatRequirement methods for any message, and or and and for formatting lists the same way the built-in messages do. Messages that aren't translated for a locale fall back to en-US instead of throwing. Co-Authored-By: Claude Opus 5.5 --- README.md | 62 +++++++-- src/index.d.ts | 16 +++ src/index.js | 1 + src/localization.js | 265 ++++++++++++++++++++++++--------------- src/localization.test.js | 63 +++++++++- 5 files changed, 296 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index b031de3..0c49f3a 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ validation. Importing this package adds a `JSE` output format to `@hyperjump/json-schema`. It uses the full results of evaluation rather than only what the standard output formats report. Use the `locale` option to choose the language of the messages. The default is `en-US`, which is currently the -only locale available. +only locale included. See [Messages and Translations](#messages-and-translations) +to add a locale. ```TypeScript import { registerSchema, validate } from "@hyperjump/json-schema/draft-2020-12"; @@ -129,6 +130,32 @@ applies if formats are validated. The `JSE` output format works this out from const errors = await jsonSchemaErrors(output, schemaUri, instance, { isFormatAsserted: true }); ``` +## Messages and Translations + +Messages are written in [Fluent](https://projectfluent.org/). Use +`addTranslation` to add a locale or to change messages. A message with the +same id as an existing message replaces it, and messages that aren't translated +for a locale fall back to `en-US`. See +[`src/translations/en-US.js`](src/translations/en-US.js) for the message ids +and the variables each message gets. + +```TypeScript +import { addTranslation } from "@hyperjump/json-schema-errors"; + +// Change a message +addTranslation("en-US", ` +required-message = Missing {$count -> + [one] the required property {$required} + *[other] the required properties {$required} +} +`); + +// Add a locale. Use the direction option for right-to-left languages. +addTranslation("fr-FR", ` +type-message = Une valeur de type {$expectedTypes} est attendue +`); +``` + ## API https://json-schema-errors.hyperjump.io @@ -149,6 +176,20 @@ Here's an example of adding support for a simple keyword called `startsWith`. the value of `startsWith`. The keyword itself needs to be defined with `@hyperjump/json-schema`'s `addKeyword` and included in a dialect. +Messages for a keyword use the ids `{keyword}-message` for errors and +`{keyword}-success-message` and `{keyword}-negated-message` for describing what +the keyword requires. + +```TypeScript +import { addTranslation } from "@hyperjump/json-schema-errors"; + +addTranslation("en-US", ` +startsWith-message = Expected a string that starts with '{$prefix}' +startsWith-success-message = The value is either not a string or starts with '{$prefix}' +startsWith-negated-message = The value is a string that doesn't start with '{$prefix}' +`); +``` + Every keyword needs a normalization handler. Keywords that aren't applicators don't have anything to evaluate. @@ -173,10 +214,11 @@ the instance and picks out the ones it handles. keyword failed, `true` if it passed, and `undefined` if the result isn't known. `success` describes what keywords require. It's used to explain failures caused -by a subschema passing, such as with `not`. When `context.localization.isNegated` -is `true`, it describes what would make the keyword fail instead. A keyword without -a `success` handler can't be described, so messages for keywords like `not` and -`oneOf` will be less specific. +by a subschema passing, such as with `not`. In a negated context, it describes +what would make the keyword fail instead. `context.localization.formatRequirement` +picks the success or negated message. A keyword without a `success` handler +can't be described, so messages for keywords like `not` and `oneOf` will be less +specific. ```TypeScript import * as Instance from "@hyperjump/json-schema/instance/experimental"; @@ -194,9 +236,9 @@ setErrorHandler("https://example.com/error-handler/startsWith", { continue; } - const startsWith = getCompiledKeywordValue(context.ast, schemaLocation) as string; + const prefix = getCompiledKeywordValue(context.ast, schemaLocation) as string; errors.push({ - message: `Expected a string that starts with '${startsWith}'`, + message: context.localization.format("startsWith-message", { prefix }), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); @@ -209,11 +251,9 @@ setErrorHandler("https://example.com/error-handler/startsWith", { const successes: ErrorObject[] = []; for (const schemaLocation in normalizedOutput[KEYWORD_URI]) { - const startsWith = getCompiledKeywordValue(context.ast, schemaLocation) as string; + const prefix = getCompiledKeywordValue(context.ast, schemaLocation) as string; successes.push({ - message: context.localization.isNegated - ? `The value is a string that doesn't start with '${startsWith}'` - : `The value is either not a string or starts with '${startsWith}'`, + message: context.localization.formatRequirement("startsWith", { prefix }), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); diff --git a/src/index.d.ts b/src/index.d.ts index f5db99c..e3ce784 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -266,6 +266,22 @@ export const negate: (context: ErrorHandlerContext) => ErrorHandlerContext; export type { Localization }; +/** + * Adds Fluent messages for a locale, such as messages for a custom keyword or + * a translation for a locale that isn't included. A message with the same id + * as an existing message replaces it. Messages that aren't translated for a + * locale fall back to en-US. + * + * Messages for keywords use the ids `{keyword}-message` for errors and + * `{keyword}-success-message` and `{keyword}-negated-message` for describing + * what a keyword requires. + * + * @param locale - A locale identifier in the form of "{language}-{region}" + * @param ftl - Messages in the Fluent syntax + * @param options.direction - The text direction of a new locale. It's ignored if the locale already exists. + */ +export const addTranslation: (locale: string, ftl: string, options?: { direction?: "ltr" | "rtl" }) => void; + export type ContainsRange = { minContains?: number; maxContains?: number; diff --git a/src/index.js b/src/index.js index 5414e06..82967fe 100644 --- a/src/index.js +++ b/src/index.js @@ -197,4 +197,5 @@ export { setErrorHandler, setNormalizationHandler } from "./json-schema-errors.js"; +export { addTranslation } from "./localization.js"; export { JSE } from "./output-format.js"; diff --git a/src/localization.js b/src/localization.js index de28e86..213fb64 100644 --- a/src/localization.js +++ b/src/localization.js @@ -6,13 +6,58 @@ import { FluentBundle, FluentResource } from "@fluent/bundle"; * @import { ContainsRange, Json } from "./index.d.ts" */ +const DEFAULT_LOCALE = "en-US"; + +/** @type Map */ +const bundles = new Map(); + /** @type Map */ const localizationCache = new Map(); +/** @type (locale: string, direction: "ltr" | "rtl") => FluentBundle */ +const createBundle = (locale, direction) => { + return new FluentBundle(locale, { useIsolating: direction === "rtl" }); +}; + +/** @type (locale: string) => FluentBundle | undefined */ +const getBundle = (locale) => { + if (!bundles.has(locale)) { + const translation = translations[locale]; + if (!translation) { + return undefined; + } + + const bundle = createBundle(locale, translation.direction); + bundle.addResource(new FluentResource(translation.ftl)); + bundles.set(locale, bundle); + } + + return bundles.get(locale); +}; + +/** + * @type (locale: string, ftl: string, options?: { direction?: "ltr" | "rtl" }) => void + */ +export const addTranslation = (locale, ftl, options = {}) => { + let bundle = getBundle(locale); + if (!bundle) { + bundle = createBundle(locale, options.direction ?? "ltr"); + bundles.set(locale, bundle); + } + + bundle.addResource(new FluentResource(ftl), { allowOverrides: true }); +}; + export class Localization { /** @type Localization | undefined */ #negated; + /** @type Intl.ListFormat */ + #disjunction; + + /** @type Intl.ListFormat */ + #conjunction; + /** * @param {string} locale * @param {FluentBundle} bundle @@ -24,8 +69,8 @@ export class Localization { this.bundle = bundle; this.isNegated = isNegated; this.#negated = negated; - this.disjunction = new Intl.ListFormat(this.locale, { type: "disjunction" }); - this.conjunction = new Intl.ListFormat(this.locale, { type: "conjunction" }); + this.#disjunction = new Intl.ListFormat(this.locale, { type: "disjunction" }); + this.#conjunction = new Intl.ListFormat(this.locale, { type: "conjunction" }); } /** @@ -44,56 +89,80 @@ export class Localization { /** @type (locale: string) => Localization */ static forLocale(locale) { if (!localizationCache.has(locale)) { - const translation = translations[locale]; - if (!translation) { + const bundle = getBundle(locale); + if (!bundle) { throw Error(`The ${locale} locale is not supported.`); } - const resource = new FluentResource(translation.ftl); - const bundle = new FluentBundle(locale, { useIsolating: translation.direction === "rtl" }); - bundle.addResource(resource); localizationCache.set(locale, new Localization(locale, bundle)); } return /** @type Localization */ (localizationCache.get(locale)); } - /** @type (messageId: string, args: Record) => string */ - #formatMessage(messageId, args) { - const message = this.bundle.getMessage(messageId); - if (!message?.value) { - throw Error(`Message '${messageId}' not found.`); + /** + * Formats a message. Messages that haven't been translated for this locale + * fall back to en-US. + * + * @type (messageId: string, args?: Record) => string + */ + format(messageId, args = {}) { + for (const bundle of [this.bundle, getBundle(DEFAULT_LOCALE)]) { + const message = bundle?.getMessage(messageId); + if (bundle && message?.value) { + return bundle.formatPattern(message.value, args); + } } - return this.bundle.formatPattern(message.value, args); + + throw Error(`Message '${messageId}' not found.`); } /** * Success messages describe what a keyword requires. In a negated view, they - * describe what would make the keyword fail. + * describe what would make the keyword fail. The message ids are + * `{keyword}-success-message` and `{keyword}-negated-message`. + * + * @type (keyword: string, args?: Record) => string + */ + formatRequirement(keyword, args = {}) { + return this.format(`${keyword}-${this.isNegated ? "negated" : "success"}-message`, args); + } + + /** + * A list where one of the items applies, such as "a, b, or c". + * + * @type (items: string[]) => string + */ + or(items) { + return this.#disjunction.format(items); + } + + /** + * A list where all of the items apply, such as "a, b, and c". * - * @type (keyword: string, args: Record) => string + * @type (items: string[]) => string */ - #formatSuccessMessage(keyword, args) { - return this.#formatMessage(`${keyword}-${this.isNegated ? "negated" : "success"}-message`, args); + and(items) { + return this.#conjunction.format(items); } getBooleanSchemaErrorMessage() { - return this.#formatMessage("boolean-schema-message", {}); + return this.format("boolean-schema-message", {}); } /** @type (expectedTypes: string[]) => string */ getTypeErrorMessage(expectedTypes) { - return this.#formatMessage("type-message", { + return this.format("type-message", { type: expectedTypes[0], - expectedTypes: this.disjunction.format(expectedTypes), + expectedTypes: this.or(expectedTypes), count: expectedTypes.length }); } /** @type (types: string[]) => string */ getTypeSuccessMessage(types) { - return this.#formatSuccessMessage("type", { + return this.formatRequirement("type", { type: types[0], - types: this.disjunction.format(types), + types: this.or(types), count: types.length }); } @@ -101,80 +170,80 @@ export class Localization { /** @type (expected: Json[]) => string */ getEnumErrorMessage(expected) { if (expected.length === 1) { - return this.#formatMessage("const-message", { + return this.format("const-message", { expected: JSON.stringify(expected[0], null, " ") }); } else { const expectedJson = expected.map((value) => JSON.stringify(value)); - return this.#formatMessage("enum-message", { - expected: this.disjunction.format(expectedJson) + return this.format("enum-message", { + expected: this.or(expectedJson) }); } } /** @type (names: string[]) => string[] */ #propertyNames(names) { - return names.map((name) => this.#formatMessage("property-name", { name })); + return names.map((name) => this.format("property-name", { name })); } /** @type (format: string) => string */ getFormatErrorMessage(format) { - return this.#formatMessage("format-message", { format }); + return this.format("format-message", { format }); } /** @type (exclusiveMaximum: number) => string */ getExclusiveMaximumErrorMessage(exclusiveMaximum) { - return this.#formatMessage("exclusiveMaximum-message", { exclusiveMaximum }); + return this.format("exclusiveMaximum-message", { exclusiveMaximum }); } /** @type (maximum: number) => string */ getMaximumErrorMessage(maximum) { - return this.#formatMessage("maximum-message", { maximum }); + return this.format("maximum-message", { maximum }); } /** @type (exclusiveMinimum: number) => string */ getExclusiveMinimumErrorMessage(exclusiveMinimum) { - return this.#formatMessage("exclusiveMinimum-message", { exclusiveMinimum }); + return this.format("exclusiveMinimum-message", { exclusiveMinimum }); } /** @type (minimum: number) => string */ getMinimumErrorMessage(minimum) { - return this.#formatMessage("minimum-message", { minimum }); + return this.format("minimum-message", { minimum }); } /** @type (multipleOf: number) => string */ getMultipleOfErrorMessage(multipleOf) { - return this.#formatMessage("multipleOf-message", { multipleOf }); + return this.format("multipleOf-message", { multipleOf }); } /** @type (maxLength: number) => string */ getMaxLengthErrorMessage(maxLength) { - return this.#formatMessage("maxLength-message", { maxLength }); + return this.format("maxLength-message", { maxLength }); } /** @type (minLength: number) => string */ getMinLengthErrorMessage(minLength) { - return this.#formatMessage("minLength-message", { minLength }); + return this.format("minLength-message", { minLength }); } /** @type (pattern: string) => string */ getPatternErrorMessage(pattern) { - return this.#formatMessage("pattern-message", { pattern }); + return this.format("pattern-message", { pattern }); } /** @type (pattern: string) => string */ getPatternSuccessMessage(pattern) { - return this.#formatSuccessMessage("pattern", { pattern }); + return this.formatRequirement("pattern", { pattern }); } /** @type (maxItems: number) => string */ getMaxItemsErrorMessage(maxItems) { - return this.#formatMessage("maxItems-message", { maxItems }); + return this.format("maxItems-message", { maxItems }); } /** @type (minItems: number) => string */ getMinItemsErrorMessage(minItems) { - return this.#formatMessage("minItems-message", { minItems }); + return this.format("minItems-message", { minItems }); } /** @@ -188,38 +257,38 @@ export class Localization { const prefix = isDescribed ? "contains" : "contains-schema"; if (range.minContains === range.maxContains) { - return this.#formatMessage(`${prefix}-exact-message`, range); + return this.format(`${prefix}-exact-message`, range); } else if (range.maxContains) { - return this.#formatMessage(`${prefix}-range-message`, range); + return this.format(`${prefix}-range-message`, range); } else { - return this.#formatMessage(`${prefix}-message`, range); + return this.format(`${prefix}-message`, range); } } /** @type (maxContains: number) => string */ getContainsTooManyErrorMessage(maxContains) { - return this.#formatMessage("contains-too-many-message", { maxContains }); + return this.format("contains-too-many-message", { maxContains }); } /** @type () => string */ getUniqueItemsErrorMessage() { - return this.#formatMessage("uniqueItems-message", {}); + return this.format("uniqueItems-message", {}); } /** @type (maxProperties: number) => string */ getMaxPropertiesErrorMessage(maxProperties) { - return this.#formatMessage("maxProperties-message", { maxProperties }); + return this.format("maxProperties-message", { maxProperties }); } /** @type (minProperties: number) => string */ getMinPropertiesErrorMessage(minProperties) { - return this.#formatMessage("minProperties-message", { minProperties }); + return this.format("minProperties-message", { minProperties }); } /** @type (required: string[]) => string */ getRequiredErrorMessage(required) { - return this.#formatMessage("required-message", { - required: this.conjunction.format(required), + return this.format("required-message", { + required: this.and(required), count: required.length }); } @@ -227,13 +296,13 @@ export class Localization { /** @type (required: string[]) => string */ getRequiredSuccessMessage(required) { if (this.isNegated) { - return this.#formatMessage("required-negated-message", { - required: this.disjunction.format(this.#propertyNames(required)), + return this.format("required-negated-message", { + required: this.or(this.#propertyNames(required)), count: required.length }); } else { - return this.#formatMessage("required-success-message", { - required: this.conjunction.format(this.#propertyNames(required)), + return this.format("required-success-message", { + required: this.and(this.#propertyNames(required)), count: required.length }); } @@ -241,22 +310,22 @@ export class Localization { /** @type (pattern: string, count: number) => string */ getEachMatchingPropertySuccessMessage(pattern, count) { - return this.#formatSuccessMessage("eachMatchingProperty", { pattern, count }); + return this.formatRequirement("eachMatchingProperty", { pattern, count }); } /** @type (pattern: string) => string */ getNoMatchingPropertySuccessMessage(pattern) { - return this.#formatSuccessMessage("noMatchingProperty", { pattern }); + return this.formatRequirement("noMatchingProperty", { pattern }); } /** @type (properties: string[], patterns: string[], count: number) => string */ getEachAdditionalPropertySuccessMessage(properties, patterns, count) { - return this.#formatSuccessMessage("eachAdditionalProperty", { ...this.#additionalPropertiesScope(properties, patterns), count }); + return this.formatRequirement("eachAdditionalProperty", { ...this.#additionalPropertiesScope(properties, patterns), count }); } /** @type (properties: string[], patterns: string[]) => string */ getNoAdditionalPropertySuccessMessage(properties, patterns) { - return this.#formatSuccessMessage("noAdditionalProperty", this.#additionalPropertiesScope(properties, patterns)); + return this.formatRequirement("noAdditionalProperty", this.#additionalPropertiesScope(properties, patterns)); } /** @type (properties: string[], patterns: string[]) => Record */ @@ -264,32 +333,32 @@ export class Localization { const scope = properties.length && patterns.length ? "both" : properties.length ? "names" : patterns.length ? "patterns" : "all"; return { scope, - properties: this.conjunction.format(this.#propertyNames(properties)), - patterns: this.disjunction.format(patterns.map((pattern) => `/${pattern}/`)) + properties: this.and(this.#propertyNames(properties)), + patterns: this.or(patterns.map((pattern) => `/${pattern}/`)) }; } /** @type (count: number) => string */ getEachPropertyNameSuccessMessage(count) { - return this.#formatSuccessMessage("eachPropertyName", { count }); + return this.formatRequirement("eachPropertyName", { count }); } /** @type (index: number, count: number) => string */ getEachItemSuccessMessage(index, count) { - return this.#formatSuccessMessage("eachItem", { index, count }); + return this.formatRequirement("eachItem", { index, count }); } /** @type (index: number) => string */ getHasItemSuccessMessage(index) { - return this.#formatSuccessMessage("hasItem", { index }); + return this.formatRequirement("hasItem", { index }); } /** @type (properties: string[]) => string */ getHasPropertySuccessMessage(properties) { - return this.#formatSuccessMessage("hasProperty", { + return this.formatRequirement("hasProperty", { properties: this.isNegated - ? this.conjunction.format(this.#propertyNames(properties)) - : this.disjunction.format(this.#propertyNames(properties)), + ? this.and(this.#propertyNames(properties)) + : this.or(this.#propertyNames(properties)), count: properties.length }); } @@ -297,15 +366,15 @@ export class Localization { /** @type (property: string, required: string[]) => string */ getDependentRequiredSuccessMessage(property, required) { if (this.isNegated) { - return this.#formatMessage("dependentRequired-negated-message", { + return this.format("dependentRequired-negated-message", { property: this.#propertyNames([property])[0], - required: this.disjunction.format(this.#propertyNames(required)), + required: this.or(this.#propertyNames(required)), count: required.length }); } else { - return this.#formatMessage("dependentRequired-success-message", { + return this.format("dependentRequired-success-message", { property: this.#propertyNames([property])[0], - required: this.conjunction.format(this.#propertyNames(required)), + required: this.and(this.#propertyNames(required)), count: required.length }); } @@ -313,142 +382,142 @@ export class Localization { /** @type (maximum: number) => string */ getMaximumSuccessMessage(maximum) { - return this.#formatSuccessMessage("maximum", { maximum }); + return this.formatRequirement("maximum", { maximum }); } /** @type (exclusiveMaximum: number) => string */ getExclusiveMaximumSuccessMessage(exclusiveMaximum) { - return this.#formatSuccessMessage("exclusiveMaximum", { exclusiveMaximum }); + return this.formatRequirement("exclusiveMaximum", { exclusiveMaximum }); } /** @type (minimum: number) => string */ getMinimumSuccessMessage(minimum) { - return this.#formatSuccessMessage("minimum", { minimum }); + return this.formatRequirement("minimum", { minimum }); } /** @type (exclusiveMinimum: number) => string */ getExclusiveMinimumSuccessMessage(exclusiveMinimum) { - return this.#formatSuccessMessage("exclusiveMinimum", { exclusiveMinimum }); + return this.formatRequirement("exclusiveMinimum", { exclusiveMinimum }); } /** @type (multipleOf: number) => string */ getMultipleOfSuccessMessage(multipleOf) { - return this.#formatSuccessMessage("multipleOf", { multipleOf }); + return this.formatRequirement("multipleOf", { multipleOf }); } /** @type (maxLength: number) => string */ getMaxLengthSuccessMessage(maxLength) { - return this.#formatSuccessMessage("maxLength", { maxLength }); + return this.formatRequirement("maxLength", { maxLength }); } /** @type (minLength: number) => string */ getMinLengthSuccessMessage(minLength) { - return this.#formatSuccessMessage("minLength", { minLength }); + return this.formatRequirement("minLength", { minLength }); } /** @type (format: string) => string */ getFormatSuccessMessage(format) { - return this.#formatSuccessMessage("format", { format }); + return this.formatRequirement("format", { format }); } /** @type (format: string) => string */ getFormatIfValidatedSuccessMessage(format) { - return this.#formatSuccessMessage("formatIfValidated", { format }); + return this.formatRequirement("formatIfValidated", { format }); } /** @type (maxItems: number) => string */ getMaxItemsSuccessMessage(maxItems) { - return this.#formatSuccessMessage("maxItems", { maxItems }); + return this.formatRequirement("maxItems", { maxItems }); } /** @type (minItems: number) => string */ getMinItemsSuccessMessage(minItems) { - return this.#formatSuccessMessage("minItems", { minItems }); + return this.formatRequirement("minItems", { minItems }); } /** @type (maxProperties: number) => string */ getMaxPropertiesSuccessMessage(maxProperties) { - return this.#formatSuccessMessage("maxProperties", { maxProperties }); + return this.formatRequirement("maxProperties", { maxProperties }); } /** @type (minProperties: number) => string */ getMinPropertiesSuccessMessage(minProperties) { - return this.#formatSuccessMessage("minProperties", { minProperties }); + return this.formatRequirement("minProperties", { minProperties }); } getUniqueItemsSuccessMessage() { - return this.#formatSuccessMessage("uniqueItems", {}); + return this.formatRequirement("uniqueItems", {}); } /** @type (expected: Json[]) => string */ getEnumSuccessMessage(expected) { if (expected.length === 1) { - return this.#formatSuccessMessage("const", { + return this.formatRequirement("const", { expected: JSON.stringify(expected[0], null, " ") }); } else { const expectedJson = expected.map((value) => JSON.stringify(value)); - return this.#formatSuccessMessage("enum", { - expected: this.disjunction.format(expectedJson) + return this.formatRequirement("enum", { + expected: this.or(expectedJson) }); } } /** @type () => string */ getAnyOfErrorMessage() { - return this.#formatMessage("anyOf-message", {}); + return this.format("anyOf-message", {}); } getOneOfErrorMessage() { - return this.#formatMessage("oneOf-message", {}); + return this.format("oneOf-message", {}); } getOneOfTooManyErrorMessage() { - return this.#formatMessage("oneOf-too-many-message", {}); + return this.format("oneOf-too-many-message", {}); } /** @type () => string */ getOneOfMultipleMatchesErrorMessage() { - return this.#formatMessage("oneOf-multiple-matches-message", {}); + return this.format("oneOf-multiple-matches-message", {}); } /** @type (quantifier: "one" | "all" | "some") => string */ getNotErrorMessage(quantifier) { - return this.#formatMessage("not-message", { quantifier }); + return this.format("not-message", { quantifier }); } getAnyValueMessage() { - return this.#formatMessage("any-value-message", {}); + return this.format("any-value-message", {}); } /** @type (min: number, max: number) => string */ getCountTrueMessage(min, max) { if (min <= 0) { - return this.#formatMessage("count-true-message", { kind: "atMost", max }); + return this.format("count-true-message", { kind: "atMost", max }); } else if (min === max) { - return this.#formatMessage("count-true-message", { kind: "exactly", min }); + return this.format("count-true-message", { kind: "exactly", min }); } else if (max === Infinity) { - return this.#formatMessage("count-true-message", { kind: "atLeast", min }); + return this.format("count-true-message", { kind: "atLeast", min }); } else { - return this.#formatMessage("count-true-message", { kind: "between", min, max }); + return this.format("count-true-message", { kind: "between", min, max }); } } /** @type (count: number) => string */ getNotShownMessage(count) { - return this.#formatMessage("not-shown-message", { count }); + return this.format("not-shown-message", { count }); } getDetailsNotShownMessage() { - return this.#formatMessage("details-not-shown-message", {}); + return this.format("details-not-shown-message", {}); } getAllTrueMessage() { - return this.#formatMessage("all-true-message", {}); + return this.format("all-true-message", {}); } /** @type (keyword: string) => string */ getUnknownErrorMessage(keyword) { - return this.#formatMessage("unknown-message", { keyword }); + return this.format("unknown-message", { keyword }); } } diff --git a/src/localization.test.js b/src/localization.test.js index 82900cc..a922120 100644 --- a/src/localization.test.js +++ b/src/localization.test.js @@ -1,6 +1,6 @@ import { beforeAll, afterAll, describe, expect, test } from "vitest"; import { translations } from "./translations/index.js"; -import { Localization } from "./localization.js"; +import { addTranslation, Localization } from "./localization.js"; describe("Localization", () => { const fixtureLocale = "fx-TR"; @@ -23,9 +23,14 @@ describe("Localization", () => { expect(() => Localization.forLocale("xx-XX")).to.throw(Error); }); + test("untranslated message falls back to en-US", () => { + const localization = Localization.forLocale(fixtureLocale); + expect(localization.getBooleanSchemaErrorMessage()).to.equal(Localization.forLocale("en-US").getBooleanSchemaErrorMessage()); + }); + test("unsupported message", () => { const localization = Localization.forLocale(fixtureLocale); - expect(() => localization.getBooleanSchemaErrorMessage()).to.throw(Error); + expect(() => localization.format("fx-unsupported-message")).to.throw(Error); }); test("ltr locale doesn't isolate placeables", () => { @@ -37,4 +42,58 @@ describe("Localization", () => { const localization = Localization.forLocale(rtlFixtureLocale); expect(localization.getTypeErrorMessage(["string"])).to.equal("Expected a ⁨string⁩"); }); + + describe("addTranslation", () => { + test("adds messages to an existing locale", () => { + addTranslation("en-US", `fx-greeting-message = Hello {$name}`); + expect(Localization.forLocale("en-US").format("fx-greeting-message", { name: "World" })).to.equal("Hello World"); + }); + + test("replaces messages with the same id", () => { + addTranslation("fx-RP", `fx-replaced-message = Original`); + addTranslation("fx-RP", `fx-replaced-message = Replaced`); + expect(Localization.forLocale("fx-RP").format("fx-replaced-message")).to.equal("Replaced"); + }); + + test("adds messages to a locale after it's used", () => { + const localization = Localization.forLocale(ltrFixtureLocale); + addTranslation(ltrFixtureLocale, `fx-later-message = Added later`); + expect(localization.format("fx-later-message")).to.equal("Added later"); + }); + + test("adds a new locale", () => { + addTranslation("fx-NW", `type-message = New {$expectedTypes}`); + expect(Localization.forLocale("fx-NW").getTypeErrorMessage(["string"])).to.equal("New string"); + }); + + test("adds a new rtl locale", () => { + addTranslation("fx-NR", `type-message = New {$expectedTypes}`, { direction: "rtl" }); + expect(Localization.forLocale("fx-NR").getTypeErrorMessage(["string"])).to.equal("New ⁨string⁩"); + }); + }); + + describe("formatRequirement", () => { + beforeAll(() => { + addTranslation("en-US", ` +fx-keyword-success-message = Passes {$value} +fx-keyword-negated-message = Fails {$value} +`); + }); + + test("describes what the keyword requires", () => { + const localization = Localization.forLocale("en-US"); + expect(localization.formatRequirement("fx-keyword", { value: 1 })).to.equal("Passes 1"); + }); + + test("describes what makes the keyword fail when negated", () => { + const localization = Localization.forLocale("en-US").negated(); + expect(localization.formatRequirement("fx-keyword", { value: 1 })).to.equal("Fails 1"); + }); + }); + + test("lists", () => { + const localization = Localization.forLocale("en-US"); + expect(localization.or(["a", "b", "c"])).to.equal("a, b, or c"); + expect(localization.and(["a", "b", "c"])).to.equal("a, b, and c"); + }); }); From 564322eae13d955096fc4e258990eff49dc4c229 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Mon, 5 Oct 2026 13:52:14 -0700 Subject: [PATCH 5/6] Reduce the helpers custom handlers need Custom handlers needed many of the helpers the built-in handlers use, most of which weren't exported. This removes the need for most of them and exports the rest. Keyword results now include the keyword's compiled value, so handlers don't need to look it up in the AST. getSuccesses can describe a subschema from its schema location, which replaces evaluateRequirements. isPassing and isFailing are replaced by getValidity, which says whether a subschema passed, failed, or isn't known. Long lists of messages are now shortened in one pass over the final messages instead of by each handler, so handlers don't need limitItems or limitOptions. This also shortens anyOf and oneOf error alternatives, which weren't shortened before. Options keep their order unless a group is marked to show the smallest options first. The exported helpers are getErrors, getSuccesses, negate, getValidity, someTrue, allTrue, countTrue, getPlaceholder, isPlaceholder, describeConditional, and describeScope. flattenOutput and getCompiledKeywordValue are no longer exported. Co-Authored-By: Claude Opus 5.5 --- README.md | 31 +- src/error-handlers/contains.js | 18 +- src/error-handlers/dependentSchemas.js | 7 +- src/error-handlers/draft-04/dependencies.js | 4 +- src/error-handlers/eachProperty.js | 6 +- src/error-handlers/format.js | 6 +- src/error-handlers/ifThenElse.js | 17 +- src/error-handlers/items.js | 13 +- src/error-handlers/maxItems.js | 6 +- src/error-handlers/maxLength.js | 6 +- src/error-handlers/maxProperties.js | 6 +- src/error-handlers/maximum.js | 14 +- src/error-handlers/minItems.js | 6 +- src/error-handlers/minLength.js | 6 +- src/error-handlers/minProperties.js | 6 +- src/error-handlers/minimum.js | 14 +- src/error-handlers/multipleOf.js | 6 +- src/error-handlers/not.js | 8 +- src/error-handlers/oneOf.js | 10 +- src/error-handlers/pattern.js | 6 +- src/error-handlers/prefixItems.js | 15 +- src/error-handlers/properties.js | 7 +- src/error-handlers/required.js | 13 +- src/error-handlers/typeConstEnum.js | 14 +- src/error-handlers/uniqueItems.js | 2 +- src/index.d.ts | 116 +++++-- src/index.js | 12 +- src/json-schema-errors.js | 206 +++++++------ src/output-format.js | 9 +- src/output-plugin.js | 8 +- src/output-plugin.test.ts | 317 +++++++++++++------- src/test-suite/tests/anyOf.json | 93 ++++++ 32 files changed, 649 insertions(+), 359 deletions(-) diff --git a/README.md b/README.md index 0c49f3a..26d88e3 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,8 @@ the instance and picks out the ones it handles. `error` describes keywords that failed. A result's `valid` is `false` if the keyword failed, `true` if it passed, and `undefined` if the result isn't known. +Its `value` is the keyword's value as compiled by the keyword's +`@hyperjump/json-schema` definition. `success` describes what keywords require. It's used to explain failures caused by a subschema passing, such as with `not`. In a negated context, it describes @@ -222,7 +224,7 @@ specific. ```TypeScript import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { getCompiledKeywordValue, setErrorHandler } from "@hyperjump/json-schema-errors"; +import { setErrorHandler } from "@hyperjump/json-schema-errors"; import type { ErrorObject } from "@hyperjump/json-schema-errors"; const KEYWORD_URI = "https://example.com/keyword/startsWith"; @@ -232,11 +234,12 @@ setErrorHandler("https://example.com/error-handler/startsWith", { const errors: ErrorObject[] = []; for (const schemaLocation in normalizedErrors[KEYWORD_URI]) { - if (normalizedErrors[KEYWORD_URI][schemaLocation].valid !== false) { + const { valid, value } = normalizedErrors[KEYWORD_URI][schemaLocation]; + if (valid !== false) { continue; } - const prefix = getCompiledKeywordValue(context.ast, schemaLocation) as string; + const prefix = value as string; errors.push({ message: context.localization.format("startsWith-message", { prefix }), instanceLocation: Instance.uri(instance), @@ -251,7 +254,7 @@ setErrorHandler("https://example.com/error-handler/startsWith", { const successes: ErrorObject[] = []; for (const schemaLocation in normalizedOutput[KEYWORD_URI]) { - const prefix = getCompiledKeywordValue(context.ast, schemaLocation) as string; + const prefix = normalizedOutput[KEYWORD_URI][schemaLocation].value as string; successes.push({ message: context.localization.formatRequirement("startsWith", { prefix }), instanceLocation: Instance.uri(instance), @@ -284,8 +287,24 @@ setNormalizationHandler(KEYWORD_URI, { }); ``` -See the `anyOf` or `oneOf` normalization and error handlers for an example of -implementing an applicator that also asserts. +Applicators that make assertions of their own, like `not` or `anyOf`, need an +error handler that describes their subschemas. A keyword result's `outputs` has +the normalized output of each subschema. These functions help describe them. + +- `getErrors` and `getSuccesses` describe a subschema's errors or what it + requires. `getSuccesses` can describe a subschema the validator didn't + evaluate from its schema location. +- `negate` gives a context that describes what would make a subschema fail + instead of what it requires. +- `getValidity` says whether a subschema passed, failed, or if it isn't known. +- `someTrue`, `allTrue`, and `countTrue` group descriptions of subschemas, such + as when at least one of them needs to be true. +- `getPlaceholder`, `isPlaceholder`, `describeConditional`, and `describeScope` + describe values that aren't present, such as a property that would only need + to match a subschema if it were present. + +Long lists of messages are shortened automatically. See the `not`, `anyOf`, and +`properties` error handlers for examples. ## Examples diff --git a/src/error-handlers/contains.js b/src/error-handlers/contains.js index 1775ebb..8831527 100644 --- a/src/error-handlers/contains.js +++ b/src/error-handlers/contains.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { allTrue, allowsAnyValue, countTrue, describeEach, getCompiledKeywordValue, getPlaceholder, getSiblingKeywordLocation, getSuccesses, isFailing, isPassing, limitItems, negate, someTrue } from "../json-schema-errors.js"; +import { allTrue, allowsAnyValue, countTrue, describeEach, getPlaceholder, getSiblingKeywordLocation, getSuccesses, getValidity, negate, someTrue } from "../json-schema-errors.js"; /** * @import { ContainsAst } from "../normalization-handlers/contains.js" @@ -27,7 +27,7 @@ const containsErrorHandler = { /** @type string[] */ const schemaLocations = [schemaLocation]; - const contains = /** @type ContainsAst */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const contains = /** @type ContainsAst */ (normalizedErrors[keywordUri][schemaLocation].value); /** @type ContainsRange */ const range = {}; @@ -49,7 +49,7 @@ const containsErrorHandler = { const itemOutputs = containsOutput.outputs ?? []; const matches = items.flatMap((item, index) => { const itemOutput = itemOutputs[index]; - return itemOutput && isPassing(itemOutput) ? [{ item, itemOutput }] : []; + return itemOutput && getValidity(itemOutput) === true ? [{ item, itemOutput }] : []; }); if (matches.length > contains.maxContains) { const descriptions = matches.map(({ itemOutput }) => { @@ -61,7 +61,7 @@ const containsErrorHandler = { matches.forEach(({ item }, index) => { errors.push({ message: context.localization.getContainsTooManyErrorMessage(contains.maxContains), - alternatives: [limitItems(descriptions[index], item, context)], + alternatives: [descriptions[index]], instanceLocation: Instance.uri(item), schemaLocations: [schemaLocation, maxContainsLocation] }); @@ -89,7 +89,7 @@ const containsErrorHandler = { if (description.length > 0) { errors.push({ message: context.localization.getContainsErrorMessage(range, true), - alternatives: [limitItems(description, instance, context)], + alternatives: [description], instanceLocation: Instance.uri(instance), schemaLocations: schemaLocations }); @@ -121,15 +121,15 @@ const containsErrorHandler = { for (const schemaLocation in normalizedOutput[keywordUri]) { const itemOutputs = normalizedOutput[keywordUri][schemaLocation].outputs ?? []; - const contains = /** @type ContainsAst | string */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const contains = /** @type ContainsAst | string */ (normalizedOutput[keywordUri][schemaLocation].value); const minContains = typeof contains === "string" ? 1 : contains.minContains; const maxContains = typeof contains === "string" || contains.maxContains === Number.MAX_SAFE_INTEGER ? Infinity : contains.maxContains; - const isKnown = itemOutputs.every((itemOutput) => isPassing(itemOutput) || isFailing(itemOutput)); - const matching = itemOutputs.filter(isPassing); - const notMatching = itemOutputs.filter((itemOutput) => !isPassing(itemOutput)); + const isKnown = itemOutputs.every((itemOutput) => getValidity(itemOutput) !== undefined); + const matching = itemOutputs.filter((itemOutput) => getValidity(itemOutput) === true); + const notMatching = itemOutputs.filter((itemOutput) => getValidity(itemOutput) !== true); /** @type (itemOutput: NormalizedOutput, context: ErrorHandlerContext) => ErrorObject[] */ const describe = (itemOutput, context) => getSuccesses(itemOutput, instance, context); diff --git a/src/error-handlers/dependentSchemas.js b/src/error-handlers/dependentSchemas.js index 4cf296a..7ec9a9d 100644 --- a/src/error-handlers/dependentSchemas.js +++ b/src/error-handlers/dependentSchemas.js @@ -1,8 +1,6 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import { describeConditional, - evaluateRequirements, - getCompiledKeywordValue, getSuccesses } from "../json-schema-errors.js"; @@ -21,7 +19,7 @@ const dependentSchemasErrorHandler = { const successes = []; for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/dependentSchemas"]) { - const dependencies = /** @type [string, string][] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const dependencies = /** @type [string, string][] */ (normalizedOutput["https://json-schema.org/keyword/dependentSchemas"][schemaLocation].value); const outputs = normalizedOutput["https://json-schema.org/keyword/dependentSchemas"][schemaLocation].outputs ?? []; successes.push(...describeSchemaDependencies(dependencies, outputs, instance, context)); } @@ -46,8 +44,7 @@ export const describeSchemaDependencies = (dependencies, outputs, instance, cont for (const [propertyName, dependencyLocation] of dependencies) { const isPresent = isObject && Instance.has(propertyName, instance); - const output = (isPresent ? outputs[outputIndex++] : undefined) - ?? evaluateRequirements(dependencyLocation, instance, context.ast); + const output = (isPresent ? outputs[outputIndex++] : undefined) ?? dependencyLocation; // The dependency only applies if the property is present successes.push(...describeConditional({ diff --git a/src/error-handlers/draft-04/dependencies.js b/src/error-handlers/draft-04/dependencies.js index e37d79d..ce63473 100644 --- a/src/error-handlers/draft-04/dependencies.js +++ b/src/error-handlers/draft-04/dependencies.js @@ -1,4 +1,4 @@ -import { getCompiledKeywordValue, getErrors } from "../../json-schema-errors.js"; +import { getErrors } from "../../json-schema-errors.js"; import { describeSchemaDependencies } from "../dependentSchemas.js"; /** @@ -32,7 +32,7 @@ const dependenciesErrorHandler = { for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/draft-04/dependencies"]) { // Array-form dependencies are handled with 'required' - const dependencies = /** @type [string, string | string[]][] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const dependencies = /** @type [string, string | string[]][] */ (normalizedOutput["https://json-schema.org/keyword/draft-04/dependencies"][schemaLocation].value); const schemaDependencies = /** @type [string, string][] */ (dependencies.filter(([, dependency]) => typeof dependency === "string")); const outputs = normalizedOutput["https://json-schema.org/keyword/draft-04/dependencies"][schemaLocation].outputs ?? []; successes.push(...describeSchemaDependencies(schemaDependencies, outputs, instance, context)); diff --git a/src/error-handlers/eachProperty.js b/src/error-handlers/eachProperty.js index 9053972..520ca32 100644 --- a/src/error-handlers/eachProperty.js +++ b/src/error-handlers/eachProperty.js @@ -37,7 +37,7 @@ const eachPropertyErrorHandler = { const property = getPlaceholder(instance, unusedPropertyName(instance)); for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/patternProperties"]) { - const patternProperties = /** @type [RegExp, string][] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const patternProperties = /** @type [RegExp, string][] */ (normalizedOutput["https://json-schema.org/keyword/patternProperties"][schemaLocation].value); for (const [pattern, subschemaLocation] of patternProperties) { successes.push(...describeScope({ subschemaLocation, @@ -49,7 +49,7 @@ const eachPropertyErrorHandler = { } for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/additionalProperties"]) { - const [, subschemaLocation] = /** @type [RegExp, string] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const [, subschemaLocation] = /** @type [RegExp, string] */ (normalizedOutput["https://json-schema.org/keyword/additionalProperties"][schemaLocation].value); const properties = Object.keys(/** @type Record */ ( getSiblingValue(context.ast, schemaLocation, "https://json-schema.org/keyword/properties") ?? {} )); @@ -67,7 +67,7 @@ const eachPropertyErrorHandler = { // Property names are at a different location than property values const propertyName = getPropertyNamePlaceholder(instance, unusedPropertyName(instance)); for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/propertyNames"]) { - const subschemaLocation = /** @type string */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const subschemaLocation = /** @type string */ (normalizedOutput["https://json-schema.org/keyword/propertyNames"][schemaLocation].value); successes.push(...describeScope({ subschemaLocation, placeholder: propertyName, diff --git a/src/error-handlers/format.js b/src/error-handlers/format.js index dc2ad46..7dd6532 100644 --- a/src/error-handlers/format.js +++ b/src/error-handlers/format.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; +import { describeKeyword } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -28,7 +28,7 @@ const formatErrorHandler = { continue; } - const format = /** @type string */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const format = /** @type string */ (normalizedErrors[keywordUri][schemaLocation].value); errors.push({ message: context.localization.getFormatErrorMessage(format), @@ -45,7 +45,7 @@ const formatErrorHandler = { // A 'format' that's only an annotation doesn't require anything. If it's // not known, the message says that it only applies if formats are validated. return keywordUris.flatMap((keywordUri) => { - return describeKeyword(normalizedOutput, keywordUri, instance, context.ast, (/** @type string */ format) => { + return describeKeyword(normalizedOutput, keywordUri, instance, (/** @type string */ format) => { switch (context.isFormatAsserted(keywordUri, format)) { case true: return context.localization.getFormatSuccessMessage(format); diff --git a/src/error-handlers/ifThenElse.js b/src/error-handlers/ifThenElse.js index eb3ae68..c591b23 100644 --- a/src/error-handlers/ifThenElse.js +++ b/src/error-handlers/ifThenElse.js @@ -1,7 +1,5 @@ import { describeConditional, - evaluateRequirements, - getCompiledKeywordValue, getSuccesses, someTrue } from "../json-schema-errors.js"; @@ -23,13 +21,13 @@ const ifThenElseErrorHandler = { for (const ifLocation in normalizedOutput["https://json-schema.org/keyword/if"]) { const ifOutput = normalizedOutput["https://json-schema.org/keyword/if"][ifLocation].outputs?.[0]; - const thenOutput = getSiblingOutput(normalizedOutput, "https://json-schema.org/keyword/then", ifLocation, instance, context.ast); - const elseOutput = getSiblingOutput(normalizedOutput, "https://json-schema.org/keyword/else", ifLocation, instance, context.ast); + const thenOutput = getSiblingOutput(normalizedOutput, "https://json-schema.org/keyword/then", ifLocation); + const elseOutput = getSiblingOutput(normalizedOutput, "https://json-schema.org/keyword/else", ifLocation); if (!ifOutput || (!thenOutput && !elseOutput)) { continue; } - /** @type (output: NormalizedOutput) => (context: ErrorHandlerContext) => ErrorObject[] */ + /** @type (output: NormalizedOutput | string) => (context: ErrorHandlerContext) => ErrorObject[] */ const describe = (output) => (context) => getSuccesses(output, instance, context); // Both branches are described, even if we know which way 'if' went, @@ -56,15 +54,14 @@ const ifThenElseErrorHandler = { * still be described from the schema because descriptions don't depend on * results. * - * @type (normalizedOutput: InstanceOutput, keywordUri: string, ifLocation: string, instance: JsonNode, ast: AST) => NormalizedOutput | undefined + * @type (normalizedOutput: InstanceOutput, keywordUri: string, ifLocation: string) => NormalizedOutput | string | undefined */ -const getSiblingOutput = (normalizedOutput, keywordUri, ifLocation, instance, ast) => { +const getSiblingOutput = (normalizedOutput, keywordUri, ifLocation) => { const parentLocation = ifLocation.replace(/\/[^/]+$/, ""); for (const schemaLocation in normalizedOutput[keywordUri]) { if (schemaLocation.replace(/\/[^/]+$/, "") === parentLocation) { - const [, subschemaLocation] = /** @type [string, string] */ (getCompiledKeywordValue(ast, schemaLocation)); - return normalizedOutput[keywordUri][schemaLocation].outputs?.[0] - ?? evaluateRequirements(subschemaLocation, instance, ast); + const [, subschemaLocation] = /** @type [string, string] */ (normalizedOutput[keywordUri][schemaLocation].value); + return normalizedOutput[keywordUri][schemaLocation].outputs?.[0] ?? subschemaLocation; } } }; diff --git a/src/error-handlers/items.js b/src/error-handlers/items.js index 6f4f083..3438b22 100644 --- a/src/error-handlers/items.js +++ b/src/error-handlers/items.js @@ -1,8 +1,7 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeScope, getCompiledKeywordValue, getPlaceholder, isPlaceholder } from "../json-schema-errors.js"; +import { describeScope, getPlaceholder, isPlaceholder } from "../json-schema-errors.js"; /** - * @import { AST } from "@hyperjump/json-schema/experimental" * @import { ErrorHandler, ErrorObject, InstanceOutput } from "../index.d.ts" */ @@ -24,7 +23,7 @@ const itemsErrorHandler = { return successes; } - for (const [schemaLocation, startIndex, itemsLocation] of getItemsKeywords(normalizedOutput, context.ast)) { + for (const [schemaLocation, startIndex, itemsLocation] of getItemsKeywords(normalizedOutput)) { // An item that doesn't exist stands in for any item const length = Instance.typeOf(instance) === "array" ? Instance.length(instance) : 0; successes.push(...describeScope({ @@ -42,15 +41,15 @@ const itemsErrorHandler = { /** * Finds the keywords that apply a schema to every item starting at some index. * - * @type (normalizedOutput: InstanceOutput, ast: AST) => [string, number, string][] + * @type (normalizedOutput: InstanceOutput) => [string, number, string][] */ -const getItemsKeywords = (normalizedOutput, ast) => { +const getItemsKeywords = (normalizedOutput) => { /** @type [string, number, string][] */ const keywords = []; for (const keywordUri of ["https://json-schema.org/keyword/items", "https://json-schema.org/keyword/draft-04/additionalItems"]) { for (const schemaLocation in normalizedOutput[keywordUri]) { - const [startIndex, itemsLocation] = /** @type [number, string] */ (getCompiledKeywordValue(ast, schemaLocation)); + const [startIndex, itemsLocation] = /** @type [number, string] */ (normalizedOutput[keywordUri][schemaLocation].value); // 'additionalItems' doesn't apply unless 'items' is an array if (startIndex !== Number.MAX_SAFE_INTEGER) { keywords.push([schemaLocation, startIndex, itemsLocation]); @@ -60,7 +59,7 @@ const getItemsKeywords = (normalizedOutput, ast) => { // Draft-04 style 'items' with a single schema applies to every item for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/draft-04/items"]) { - const items = /** @type string | string[] */ (getCompiledKeywordValue(ast, schemaLocation)); + const items = /** @type string | string[] */ (normalizedOutput["https://json-schema.org/keyword/draft-04/items"][schemaLocation].value); if (typeof items === "string") { keywords.push([schemaLocation, 0, items]); } diff --git a/src/error-handlers/maxItems.js b/src/error-handlers/maxItems.js index 3141550..1785a2d 100644 --- a/src/error-handlers/maxItems.js +++ b/src/error-handlers/maxItems.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; +import { describeKeyword } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -18,7 +18,7 @@ const maxItemsErrorHandler = { continue; } - const maxItems = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const maxItems = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/maxItems"][schemaLocation].value); if (maxItems < lowestMaxItems) { lowestMaxItems = maxItems; @@ -38,7 +38,7 @@ const maxItemsErrorHandler = { }, success: (normalizedOutput, instance, context) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxItems", instance, context.ast, (/** @type number */ maxItems) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxItems", instance, (/** @type number */ maxItems) => { return context.localization.getMaxItemsSuccessMessage(maxItems); }); } diff --git a/src/error-handlers/maxLength.js b/src/error-handlers/maxLength.js index a3a180e..32f84e5 100644 --- a/src/error-handlers/maxLength.js +++ b/src/error-handlers/maxLength.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; +import { describeKeyword } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -18,7 +18,7 @@ const maxLengthErrorHandler = { continue; } - const maxLength = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const maxLength = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/maxLength"][schemaLocation].value); if (maxLength < lowestMaxLength) { lowestMaxLength = maxLength; @@ -37,7 +37,7 @@ const maxLengthErrorHandler = { }, success: (normalizedOutput, instance, context) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxLength", instance, context.ast, (/** @type number */ maxLength) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxLength", instance, (/** @type number */ maxLength) => { return context.localization.getMaxLengthSuccessMessage(maxLength); }); } diff --git a/src/error-handlers/maxProperties.js b/src/error-handlers/maxProperties.js index bd44f4c..d6915ed 100644 --- a/src/error-handlers/maxProperties.js +++ b/src/error-handlers/maxProperties.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; +import { describeKeyword } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -17,7 +17,7 @@ const maxPropertiesErrorHandler = { continue; } - const maxProperties = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const maxProperties = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/maxProperties"][schemaLocation].value); if (maxProperties < lowestMaxProperties) { lowestMaxProperties = maxProperties; @@ -36,7 +36,7 @@ const maxPropertiesErrorHandler = { }, success: (normalizedOutput, instance, context) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxProperties", instance, context.ast, (/** @type number */ maxProperties) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxProperties", instance, (/** @type number */ maxProperties) => { return context.localization.getMaxPropertiesSuccessMessage(maxProperties); }); } diff --git a/src/error-handlers/maximum.js b/src/error-handlers/maximum.js index 0a38857..345550e 100644 --- a/src/error-handlers/maximum.js +++ b/src/error-handlers/maximum.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue, getSiblingKeywordLocation } from "../json-schema-errors.js"; +import { describeKeyword, getSiblingKeywordLocation } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -19,7 +19,7 @@ const maximumErrorHandler = { continue; } - const maximum = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const maximum = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/maximum"][schemaLocation].value); if (maximum < lowestMaximum) { lowestMaximum = maximum; schemaLocations = [schemaLocation]; @@ -31,7 +31,7 @@ const maximumErrorHandler = { continue; } - const exclusiveMaximum = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const exclusiveMaximum = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/exclusiveMaximum"][schemaLocation].value); if (exclusiveMaximum < lowestMaximum) { lowestMaximum = exclusiveMaximum; isExclusive = true; @@ -44,7 +44,7 @@ const maximumErrorHandler = { continue; } - const [maximum, exclusive] = /** @type [number, boolean] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const [maximum, exclusive] = /** @type [number, boolean] */ (normalizedErrors["https://json-schema.org/keyword/draft-04/maximum"][schemaLocation].value); if (maximum < lowestMaximum) { lowestMaximum = maximum; isExclusive = exclusive; @@ -77,17 +77,17 @@ const maximumErrorHandler = { /** @type ErrorObject[] */ const successes = []; - successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maximum", instance, context.ast, (/** @type number */ maximum) => { + successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maximum", instance, (/** @type number */ maximum) => { return context.localization.getMaximumSuccessMessage(maximum); })); - successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/exclusiveMaximum", instance, context.ast, (/** @type number */ exclusiveMaximum) => { + successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/exclusiveMaximum", instance, (/** @type number */ exclusiveMaximum) => { return context.localization.getExclusiveMaximumSuccessMessage(exclusiveMaximum); })); // Draft-04 has a boolean 'exclusiveMaximum' keyword that modifies 'maximum' for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/draft-04/maximum"]) { - const [maximum, exclusive] = /** @type [number, boolean] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const [maximum, exclusive] = /** @type [number, boolean] */ (normalizedOutput["https://json-schema.org/keyword/draft-04/maximum"][schemaLocation].value); if (exclusive) { const exclusiveLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMaximum"); successes.push({ diff --git a/src/error-handlers/minItems.js b/src/error-handlers/minItems.js index 1342226..502dd7c 100644 --- a/src/error-handlers/minItems.js +++ b/src/error-handlers/minItems.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; +import { describeKeyword } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -18,7 +18,7 @@ const minItemsErrorHandler = { continue; } - const minItems = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const minItems = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/minItems"][schemaLocation].value); if (minItems > highestMinItem) { highestMinItem = minItems; @@ -38,7 +38,7 @@ const minItemsErrorHandler = { }, success: (normalizedOutput, instance, context) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minItems", instance, context.ast, (/** @type number */ minItems) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minItems", instance, (/** @type number */ minItems) => { return context.localization.getMinItemsSuccessMessage(minItems); }); } diff --git a/src/error-handlers/minLength.js b/src/error-handlers/minLength.js index b3e5e31..b744b6c 100644 --- a/src/error-handlers/minLength.js +++ b/src/error-handlers/minLength.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; +import { describeKeyword } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -18,7 +18,7 @@ const minLengthErrorHandler = { continue; } - const minLength = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const minLength = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/minLength"][schemaLocation].value); if (minLength > highestMinLength) { highestMinLength = minLength; @@ -37,7 +37,7 @@ const minLengthErrorHandler = { }, success: (normalizedOutput, instance, context) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minLength", instance, context.ast, (/** @type number */ minLength) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minLength", instance, (/** @type number */ minLength) => { return context.localization.getMinLengthSuccessMessage(minLength); }); } diff --git a/src/error-handlers/minProperties.js b/src/error-handlers/minProperties.js index 3c55594..8fb567d 100644 --- a/src/error-handlers/minProperties.js +++ b/src/error-handlers/minProperties.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; +import { describeKeyword } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -19,7 +19,7 @@ const minPropertiesErrorHandler = { continue; } - const minProperties = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const minProperties = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/minProperties"][schemaLocation].value); if (minProperties > highestMinProperties) { highestMinProperties = minProperties; @@ -39,7 +39,7 @@ const minPropertiesErrorHandler = { }, success: (normalizedOutput, instance, context) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minProperties", instance, context.ast, (/** @type number */ minProperties) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minProperties", instance, (/** @type number */ minProperties) => { return context.localization.getMinPropertiesSuccessMessage(minProperties); }); } diff --git a/src/error-handlers/minimum.js b/src/error-handlers/minimum.js index 7828e37..edc0e17 100644 --- a/src/error-handlers/minimum.js +++ b/src/error-handlers/minimum.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue, getSiblingKeywordLocation } from "../json-schema-errors.js"; +import { describeKeyword, getSiblingKeywordLocation } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -18,7 +18,7 @@ const minimumErrorHandler = { continue; } - const minimum = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const minimum = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/minimum"][schemaLocation].value); if (minimum > highestMinimum) { highestMinimum = minimum; @@ -31,7 +31,7 @@ const minimumErrorHandler = { continue; } - const exclusiveMinimum = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const exclusiveMinimum = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/exclusiveMinimum"][schemaLocation].value); if (exclusiveMinimum > highestMinimum) { highestMinimum = exclusiveMinimum; @@ -45,7 +45,7 @@ const minimumErrorHandler = { continue; } - const [minimum, exclusive] = /** @type [number, boolean] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const [minimum, exclusive] = /** @type [number, boolean] */ (normalizedErrors["https://json-schema.org/keyword/draft-04/minimum"][schemaLocation].value); if (minimum > highestMinimum) { highestMinimum = minimum; isExclusive = exclusive; @@ -78,17 +78,17 @@ const minimumErrorHandler = { /** @type ErrorObject[] */ const successes = []; - successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minimum", instance, context.ast, (/** @type number */ minimum) => { + successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minimum", instance, (/** @type number */ minimum) => { return context.localization.getMinimumSuccessMessage(minimum); })); - successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/exclusiveMinimum", instance, context.ast, (/** @type number */ exclusiveMinimum) => { + successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/exclusiveMinimum", instance, (/** @type number */ exclusiveMinimum) => { return context.localization.getExclusiveMinimumSuccessMessage(exclusiveMinimum); })); // Draft-04 has a boolean 'exclusiveMinimum' keyword that modifies 'minimum' for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/draft-04/minimum"]) { - const [minimum, exclusive] = /** @type [number, boolean] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const [minimum, exclusive] = /** @type [number, boolean] */ (normalizedOutput["https://json-schema.org/keyword/draft-04/minimum"][schemaLocation].value); if (exclusive) { const exclusiveLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMinimum"); successes.push({ diff --git a/src/error-handlers/multipleOf.js b/src/error-handlers/multipleOf.js index 571717e..9e18f5b 100644 --- a/src/error-handlers/multipleOf.js +++ b/src/error-handlers/multipleOf.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; +import { describeKeyword } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -22,7 +22,7 @@ const multipleOfErrorHandler = { hasError = true; } - const multipleOf = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const multipleOf = /** @type number */ (normalizedErrors["https://json-schema.org/keyword/multipleOf"][schemaLocation].value); combinedMultipleOf = combinedMultipleOf === null ? multipleOf : lcm(combinedMultipleOf, multipleOf); schemaLocations.push(schemaLocation); @@ -40,7 +40,7 @@ const multipleOfErrorHandler = { }, success: (normalizedOutput, instance, context) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/multipleOf", instance, context.ast, (/** @type number */ multipleOf) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/multipleOf", instance, (/** @type number */ multipleOf) => { return context.localization.getMultipleOfSuccessMessage(multipleOf); }); } diff --git a/src/error-handlers/not.js b/src/error-handlers/not.js index 9ac6143..7320759 100644 --- a/src/error-handlers/not.js +++ b/src/error-handlers/not.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { allTrue, getSuccesses, isAllTrueGroup, limitOptions, negate, someTrue } from "../json-schema-errors.js"; +import { allTrue, getSuccesses, isAllTrueGroup, negate, showSmallestFirst, someTrue } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -39,12 +39,12 @@ const notErrorHandler = { schemaLocations: [schemaLocation] }); } else { - errors.push({ + errors.push(showSmallestFirst({ message: context.localization.getNotErrorMessage(options.length === 1 ? "one" : "some"), - alternatives: limitOptions(options.map((option) => [option]), instance, context), + alternatives: options.map((option) => [option]), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] - }); + })); } } diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index 40b504a..4e1c52f 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -1,6 +1,6 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import * as Pact from "@hyperjump/pact"; -import { allowsAnyValue, allTrue, countTrue, flattenOutput, getCompiledKeywordValue, getErrors, getSuccesses, isPassing, limitItems, limitOptions, negate, someTrue } from "../json-schema-errors.js"; +import { allowsAnyValue, allTrue, countTrue, flattenOutput, getErrors, getSuccesses, getValidity, negate, someTrue } from "../json-schema-errors.js"; /** * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" @@ -20,9 +20,9 @@ const oneOfErrorHandler = { } const oneOf = oneOfOutput.outputs ?? []; - const alternativeLocations = /** @type string[] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const alternativeLocations = /** @type string[] */ (normalizedErrors["https://json-schema.org/keyword/oneOf"][schemaLocation].value); const matches = alternativeLocations.flatMap((alternativeLocation, index) => { - return isPassing(oneOf[index]) ? [{ alternativeLocation, output: oneOf[index] }] : []; + return getValidity(oneOf[index]) === true ? [{ alternativeLocation, output: oneOf[index] }] : []; }); if (matches.length > 1) { errors.push(multipleMatchesError(matches, schemaLocation, instance, context)); @@ -174,9 +174,7 @@ const multipleMatchesError = (matches, schemaLocation, instance, context) => { return { message: context.localization.getOneOfMultipleMatchesErrorMessage(), // Options are shown in the same order as the alternatives - alternatives: limitOptions(removeCommonSuccesses(alternatives).map((alternative) => { - return limitItems(alternative, instance, context); - }), instance, context, false), + alternatives: removeCommonSuccesses(alternatives), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }; diff --git a/src/error-handlers/pattern.js b/src/error-handlers/pattern.js index c98b1d9..10cc126 100644 --- a/src/error-handlers/pattern.js +++ b/src/error-handlers/pattern.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; +import { describeKeyword } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -16,7 +16,7 @@ const patternErrorHandler = { continue; } - const compiledPattern = /** @type RegExp */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const compiledPattern = /** @type RegExp */ (normalizedErrors["https://json-schema.org/keyword/pattern"][schemaLocation].value); const pattern = compiledPattern.source; errors.push({ @@ -30,7 +30,7 @@ const patternErrorHandler = { }, success: (normalizedOutput, instance, context) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/pattern", instance, context.ast, (/** @type RegExp */ pattern) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/pattern", instance, (/** @type RegExp */ pattern) => { return context.localization.getPatternSuccessMessage(pattern.source); }); } diff --git a/src/error-handlers/prefixItems.js b/src/error-handlers/prefixItems.js index 07e31aa..009f000 100644 --- a/src/error-handlers/prefixItems.js +++ b/src/error-handlers/prefixItems.js @@ -1,8 +1,6 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import { describeConditional, - evaluateRequirements, - getCompiledKeywordValue, getPlaceholder, getSuccesses, isPlaceholder @@ -30,12 +28,12 @@ const prefixItemsErrorHandler = { return successes; } - const schemaLocations = [ - ...Object.keys(normalizedOutput["https://json-schema.org/keyword/prefixItems"] ?? {}), - ...Object.keys(normalizedOutput["https://json-schema.org/keyword/draft-04/items"] ?? {}) + const keywordOutputs = [ + ...Object.entries(normalizedOutput["https://json-schema.org/keyword/prefixItems"] ?? {}), + ...Object.entries(normalizedOutput["https://json-schema.org/keyword/draft-04/items"] ?? {}) ]; - for (const schemaLocation of schemaLocations) { - const prefixItems = /** @type string | string[] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + for (const [schemaLocation, keywordOutput] of keywordOutputs) { + const prefixItems = /** @type string | string[] */ (keywordOutput.value); if (typeof prefixItems === "string") { // A single schema for all items applies to items that can't be named continue; @@ -45,14 +43,13 @@ const prefixItemsErrorHandler = { for (let index = length; index < prefixItems.length; index++) { // The item's subschema only applies if there's an item at that index const item = getPlaceholder(instance, String(index)); - const output = evaluateRequirements(prefixItems[index], item, context.ast); successes.push(...describeConditional({ condition: (context) => [{ message: context.localization.getHasItemSuccessMessage(index), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }], - then: (context) => getSuccesses(output, instance, context) + then: (context) => getSuccesses(prefixItems[index], item, context) }, instance, schemaLocation, context)); } } diff --git a/src/error-handlers/properties.js b/src/error-handlers/properties.js index 6b6d4ef..06fc819 100644 --- a/src/error-handlers/properties.js +++ b/src/error-handlers/properties.js @@ -1,8 +1,6 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import { describeConditional, - evaluateRequirements, - getCompiledKeywordValue, getPlaceholder, getSuccesses, isPlaceholder @@ -30,7 +28,7 @@ const propertiesErrorHandler = { } for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/properties"]) { - const properties = /** @type Record */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const properties = /** @type Record */ (normalizedOutput["https://json-schema.org/keyword/properties"][schemaLocation].value); const isObject = Instance.typeOf(instance) === "object"; for (const propertyName in properties) { @@ -40,14 +38,13 @@ const propertiesErrorHandler = { // The property's subschema only applies if the property is present const property = getPlaceholder(instance, propertyName); - const output = evaluateRequirements(properties[propertyName], property, context.ast); successes.push(...describeConditional({ condition: (context) => [{ message: context.localization.getHasPropertySuccessMessage([propertyName]), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }], - then: (context) => getSuccesses(output, instance, context) + then: (context) => getSuccesses(properties[propertyName], property, context) }, instance, schemaLocation, context)); } } diff --git a/src/error-handlers/required.js b/src/error-handlers/required.js index 2b1e403..d19938c 100644 --- a/src/error-handlers/required.js +++ b/src/error-handlers/required.js @@ -1,5 +1,4 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { getCompiledKeywordValue } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject } from "../index.d.ts" @@ -19,7 +18,7 @@ const requiredErrorHandler = { } allSchemaLocations.push(schemaLocation); - const required = /** @type string[] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const required = /** @type string[] */ (normalizedErrors["https://json-schema.org/keyword/required"][schemaLocation].value); addMissingProperties(required, instance, allMissingRequired); } @@ -30,7 +29,7 @@ const requiredErrorHandler = { } allSchemaLocations.push(schemaLocation); - const dependencies = /** @type {[string, string[]][]} */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const dependencies = /** @type {[string, string[]][]} */ (normalizedErrors["https://json-schema.org/keyword/dependentRequired"][schemaLocation].value); for (const [propertyName, requiredProperties] of dependencies) { if (!Instance.has(propertyName, instance)) { @@ -45,7 +44,7 @@ const requiredErrorHandler = { continue; } - const dependencies = /** @type {[string, unknown][]} */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const dependencies = /** @type {[string, unknown][]} */ (normalizedErrors["https://json-schema.org/keyword/draft-04/dependencies"][schemaLocation].value); let hasArrayFormDependencies = false; for (const [propertyName, dependency] of dependencies) { @@ -82,7 +81,7 @@ const requiredErrorHandler = { const allRequired = new Set(); const requiredSchemaLocations = []; for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/required"]) { - const required = /** @type string[] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const required = /** @type string[] */ (normalizedOutput["https://json-schema.org/keyword/required"][schemaLocation].value); if (required.length) { requiredSchemaLocations.push(schemaLocation); addAll(required, allRequired); @@ -98,7 +97,7 @@ const requiredErrorHandler = { } for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/dependentRequired"]) { - const dependencies = /** @type {[string, string[]][]} */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const dependencies = /** @type {[string, string[]][]} */ (normalizedOutput["https://json-schema.org/keyword/dependentRequired"][schemaLocation].value); for (const [propertyName, requiredProperties] of dependencies) { if (requiredProperties.length > 0) { successes.push({ @@ -111,7 +110,7 @@ const requiredErrorHandler = { } for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/draft-04/dependencies"]) { - const dependencies = /** @type {[string, unknown][]} */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const dependencies = /** @type {[string, unknown][]} */ (normalizedOutput["https://json-schema.org/keyword/draft-04/dependencies"][schemaLocation].value); for (const [propertyName, dependency] of dependencies) { if (Array.isArray(dependency) && dependency.length > 0) { successes.push({ diff --git a/src/error-handlers/typeConstEnum.js b/src/error-handlers/typeConstEnum.js index 8ce6abe..9e9a9f2 100644 --- a/src/error-handlers/typeConstEnum.js +++ b/src/error-handlers/typeConstEnum.js @@ -1,5 +1,5 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; +import { describeKeyword } from "../json-schema-errors.js"; /** * @import { ErrorHandler, ErrorObject, Json } from "../index.d.ts" @@ -19,7 +19,7 @@ const typeConstEnumErrorHandler = { failedTypeLocations.push(schemaLocation); /** @type {string | string[]} */ - const value = /** @type {string | string[]} */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const value = /** @type {string | string[]} */ (normalizedErrors["https://json-schema.org/keyword/type"][schemaLocation].value); const types = Array.isArray(value) ? value : [value]; /** @type {Set} */ const keywordTypes = new Set(types); @@ -51,7 +51,7 @@ const typeConstEnumErrorHandler = { } const keywordJson = new Set(); - const constValueJson = /** @type string */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const constValueJson = /** @type string */ (normalizedErrors["https://json-schema.org/keyword/const"][schemaLocation].value); if (allowedTypes.has(jsonTypeOf(constValueJson))) { keywordJson.add(constValueJson); } else { @@ -68,7 +68,7 @@ const typeConstEnumErrorHandler = { } const keywordJson = new Set(); - const enumValuesJson = /** @type string[] */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const enumValuesJson = /** @type string[] */ (normalizedErrors["https://json-schema.org/keyword/enum"][schemaLocation].value); for (const enumValueJson of enumValuesJson) { if (allowedTypes.has(jsonTypeOf(enumValueJson))) { keywordJson.add(enumValueJson); @@ -114,13 +114,13 @@ const typeConstEnumErrorHandler = { success: (normalizedOutput, instance, context) => { return [ - ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/type", instance, context.ast, (/** @type string | string[] */ type) => { + ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/type", instance, (/** @type string | string[] */ type) => { return context.localization.getTypeSuccessMessage(Array.isArray(type) ? type : [type]); }), - ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/const", instance, context.ast, (/** @type string */ constJson) => { + ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/const", instance, (/** @type string */ constJson) => { return context.localization.getEnumSuccessMessage([JSON.parse(constJson)]); }), - ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/enum", instance, context.ast, (/** @type string[] */ enumJson) => { + ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/enum", instance, (/** @type string[] */ enumJson) => { return context.localization.getEnumSuccessMessage(enumJson.map((json) => JSON.parse(json))); }) ]; diff --git a/src/error-handlers/uniqueItems.js b/src/error-handlers/uniqueItems.js index 9f46371..b84b85d 100644 --- a/src/error-handlers/uniqueItems.js +++ b/src/error-handlers/uniqueItems.js @@ -44,7 +44,7 @@ const uniqueItemsErrorHandler = { success: (normalizedOutput, instance, context) => { // 'uniqueItems: false' allows anything, so there's nothing to say - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/uniqueItems", instance, context.ast, (/** @type boolean */ uniqueItems) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/uniqueItems", instance, (/** @type boolean */ uniqueItems) => { return uniqueItems ? context.localization.getUniqueItemsSuccessMessage() : undefined; }); } diff --git a/src/index.d.ts b/src/index.d.ts index e3ce784..366914f 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -139,13 +139,14 @@ export type ErrorIndex = { }; /** - * The result of a keyword. `valid` is `undefined` if the result isn't known. For - * applicators, `outputs` has the normalized output of each subschema. The - * results of a simple applicator's subschemas are only in its `outputs`. Use - * `flattenOutput` to include them in the results of the parent schema. + * The result of a keyword. `valid` is `undefined` if the result isn't known. + * `value` is the keyword's value as compiled by its `@hyperjump/json-schema` + * keyword definition. For applicators, `outputs` has the normalized output of + * each subschema. */ export type KeywordOutput = { valid?: boolean; + value: unknown; outputs?: NormalizedOutput[]; }; @@ -226,22 +227,12 @@ export type ErrorHandlerContext = { }; /** - * The results of a simple applicator's subschemas are results of its parent - * schema, but they're kept with the applicator in its `outputs`. `getErrors` - * and `getSuccesses` flatten them for error handlers. Use this to get the - * flattened results of a subschema's output, such as an alternative of an - * applicator, to inspect them directly. + * Whether a subschema passed, based on the results of its keywords. It's + * `false` if any keyword failed, `true` if every keyword passed, and + * `undefined` if it isn't known. Validators don't usually report what happened + * inside an applicator that passed, so results there are often unknown. */ -export const flattenOutput: (normalizedOutput: NormalizedOutput) => NormalizedOutput; - -/** - * Gets the compiled value of the keyword at the given schema location, as - * returned by the keyword's `compile` function in `@hyperjump/json-schema`. - * - * @param ast - The compiled schema, from the error handler's context - * @param schemaLocation - The keyword's schema location - */ -export const getCompiledKeywordValue: (ast: AST, schemaLocation: string) => unknown; +export const getValidity: (normalizedOutput: NormalizedOutput) => boolean | undefined; /** * Converts the normalized error format to human readable errors. It's used to @@ -250,11 +241,19 @@ export const getCompiledKeywordValue: (ast: AST, schemaLocation: string) => unkn export const getErrors: (normalizedErrors: NormalizedOutput, instance: JsonNode, context: ErrorHandlerContext) => ErrorObject[]; /** - * Converts the normalized output of a passing subschema to human readable - * messages describing how the instance satisfied the subschema. It's used to - * build errors in applicator error handlers that fail when a subschema passes. + * Describes what a subschema requires, or in a negated context, what would make + * it fail. It's used to build errors in applicator error handlers that fail + * when a subschema passes, such as `not`. Descriptions don't depend on results, + * so a subschema the validator didn't evaluate can be described by its schema + * location instead of its normalized output, such as the subschema of a + * property that isn't present. + * + * @param subschema - The subschema's normalized output or its schema location + * @param instance - The value the subschema applies to. Use `getPlaceholder` + * for a value that isn't present. + * @param context */ -export const getSuccesses: (normalizedOutput: NormalizedOutput, instance: JsonNode, context: ErrorHandlerContext) => ErrorObject[]; +export const getSuccesses: (subschema: NormalizedOutput | string, instance: JsonNode, context: ErrorHandlerContext) => ErrorObject[]; /** * A view of the context where success messages describe what would make @@ -264,6 +263,77 @@ export const getSuccesses: (normalizedOutput: NormalizedOutput, instance: JsonNo */ export const negate: (context: ErrorHandlerContext) => ErrorHandlerContext; +/** + * A placeholder stands in for a value that isn't present, such as a property + * that isn't present or an item that could be added, so what that value would + * need to be can be described. + * + * @param parent - The value that would contain it + * @param segment - The property name or item index + */ +export const getPlaceholder: (parent: JsonNode, segment: string) => JsonNode; + +/** + * Whether a value is a placeholder for a value that isn't present. Handlers + * that describe values that aren't present, such as properties, shouldn't + * describe them for a value that isn't present either. + */ +export const isPlaceholder: (instance: JsonNode) => boolean; + +/** + * Describes subschemas that only apply under some condition, such as a + * property's subschema only applying if the property is present. Each function + * describes its part using the given context, so `condition` describes the + * condition holding, or with a negated context, not holding. + */ +export const describeConditional: (conditional: Conditional, instance: JsonNode, schemaLocation: string, context: ErrorHandlerContext) => ErrorObject[]; + +export type Conditional = { + condition: (context: ErrorHandlerContext) => ErrorObject[]; + then?: (context: ErrorHandlerContext) => ErrorObject[]; + else?: (context: ErrorHandlerContext) => ErrorObject[]; +}; + +/** + * Describes a subschema that applies to every location in some scope, such as + * every item in an array, including locations that could be added. + */ +export const describeScope: (scope: Scope, instance: JsonNode, schemaLocation: string, context: ErrorHandlerContext) => ErrorObject[]; + +export type Scope = { + /** The subschema that applies to every location in the scope */ + subschemaLocation: string; + + /** A placeholder that stands in for any location in the scope */ + placeholder: JsonNode; + + /** The message for what each location requires, given how many things that is */ + each: (localization: Localization, count: number) => string; + + /** The message for the scope being empty, which is what a `false` subschema requires */ + none: (localization: Localization) => string; +}; + +/** + * Success messages are a list of things that are all true. When something is + * a choice of options instead, this groups them with a message that says how + * many of the options are true. Each option is a list of things that are all + * true. + */ +export const countTrue: (options: ErrorObject[][], range: { min?: number; max?: number }, instance: JsonNode, schemaLocation: string, context: ErrorHandlerContext) => ErrorObject[]; + +/** + * Groups options where at least one of them is true. The same as `countTrue` + * with a `min` of 1. + */ +export const someTrue: (options: ErrorObject[][], instance: JsonNode, schemaLocation: string, context: ErrorHandlerContext) => ErrorObject[]; + +/** + * Negated success messages are a list of things where at least one is true. + * When several things need to be true instead, this groups them. + */ +export const allTrue: (items: ErrorObject[], instance: JsonNode, schemaLocation: string, context: ErrorHandlerContext) => ErrorObject[]; + export type { Localization }; /** diff --git a/src/index.js b/src/index.js index 82967fe..6366b80 100644 --- a/src/index.js +++ b/src/index.js @@ -186,16 +186,22 @@ setErrorHandler("https://hyperjump.io/error-handler/uniqueItems", uniqueItemsErr setErrorHandler("https://hyperjump.io/error-handler/unknown", unknownErrorHandler); export { + allTrue, + countTrue, + describeConditional, + describeScope, evaluateSchema, - flattenOutput, - getCompiledKeywordValue, getErrors, + getPlaceholder, getSuccesses, + getValidity, + isPlaceholder, jsonSchemaErrors, negate, removeErrorHandler, setErrorHandler, - setNormalizationHandler + setNormalizationHandler, + someTrue } from "./json-schema-errors.js"; export { addTranslation } from "./localization.js"; export { JSE } from "./output-format.js"; diff --git a/src/json-schema-errors.js b/src/json-schema-errors.js index 996b023..1e1736e 100644 --- a/src/json-schema-errors.js +++ b/src/json-schema-errors.js @@ -23,11 +23,13 @@ export const jsonSchemaErrors = async (errorOutput, schemaUri, instance, options errorIndex, plugins: [...ast.plugins] }); - return getErrors(normalizedErrors, rootInstance, { + /** @type API.ErrorHandlerContext */ + const context = { ast, localization: Localization.forLocale(options.locale ?? "en-US"), isFormatAsserted: () => options.isFormatAsserted - }); + }; + return limitMessages(getErrors(normalizedErrors, rootInstance, context), context); }; /** @type Record */ @@ -121,12 +123,12 @@ export const evaluateSchema = (schemaLocation, instance, context) => { const schemaNode = context.ast[schemaLocation]; if (typeof schemaNode === "boolean") { - const isSchemaValid = getValidity(schemaLocation, instanceLocation, context); + const isSchemaValid = getReportedValidity(schemaLocation, instanceLocation, context); if (schemaNode === false && isSchemaValid !== true) { valid = false; output[instanceLocation] = { "https://json-schema.org/validation": { - [schemaLocation]: { valid: isSchemaValid } + [schemaLocation]: { valid: isSchemaValid, value: false } } }; } @@ -143,7 +145,7 @@ export const evaluateSchema = (schemaLocation, instance, context) => { const validationKeyword = getKeyword(keywordUri); const keywordInstanceLocation = instanceLocation.replace(/^#\*/, "#"); - let isKeywordValid = getValidity(keywordLocation, keywordInstanceLocation, context); + let isKeywordValid = getReportedValidity(keywordLocation, keywordInstanceLocation, context); /** @type API.EvaluationContext */ const keywordContext = { @@ -162,7 +164,7 @@ export const evaluateSchema = (schemaLocation, instance, context) => { const keywordOutput = keyword.evaluate(keywordValue, instance, keywordContext); const isReported = context.errorIndex[keywordLocation]?.[keywordInstanceLocation] !== undefined; - if (validationKeyword.simpleApplicator && !isReported && keywordOutput?.some(isFailing)) { + if (validationKeyword.simpleApplicator && !isReported && keywordOutput?.some((suboutput) => getValidity(suboutput) === false)) { isKeywordValid = false; } @@ -173,8 +175,8 @@ export const evaluateSchema = (schemaLocation, instance, context) => { output[instanceLocation] ??= {}; output[instanceLocation][normalizedKeywordUri] ??= {}; output[instanceLocation][normalizedKeywordUri][keywordLocation] = keywordOutput - ? { valid: isKeywordValid, outputs: keywordOutput } - : { valid: isKeywordValid }; + ? { valid: isKeywordValid, value: keywordValue, outputs: keywordOutput } + : { valid: isKeywordValid, value: keywordValue }; for (const plugin of context.plugins) { plugin.afterKeyword?.(node, instance, keywordContext, isKeywordValid !== false, context, validationKeyword); @@ -195,7 +197,7 @@ export const evaluateSchema = (schemaLocation, instance, context) => { * * @type (schemaLocation: string, instanceLocation: string, context: API.EvaluationContext) => boolean | undefined */ -const getValidity = (schemaLocation, instanceLocation, context) => { +const getReportedValidity = (schemaLocation, instanceLocation, context) => { const isError = context.errorIndex[schemaLocation]?.[instanceLocation]; if (isError === undefined) { return context.isValidityUnknown ? undefined : true; @@ -208,7 +210,7 @@ const getValidity = (schemaLocation, instanceLocation, context) => { * A placeholder stands in for a value that doesn't exist, such as a property * that isn't present, so it can be described what that value would need to be. * - * @type (parent: JsonNode, segment: string) => JsonNode + * @type API.getPlaceholder */ export const getPlaceholder = (parent, segment) => { return createPlaceholder(parent.baseUri, JsonPointer.append(segment, parent.pointer), parent); @@ -228,7 +230,7 @@ const createPlaceholder = (baseUri, pointer, parent) => { return Instance.cons(baseUri, pointer, undefined, /** @type JsonNode["type"] */ ("undefined"), [], parent); }; -/** @type (node: JsonNode) => boolean */ +/** @type API.isPlaceholder */ export const isPlaceholder = (node) => /** @type string */ (Instance.typeOf(node)) === "undefined"; /** @@ -284,15 +286,6 @@ export const describeEach = (subschemaLocation, placeholder, parent, context) => .map((success) => relocate(success, Instance.uri(placeholder), Instance.uri(parent))); }; -/** - * @typedef {{ - * subschemaLocation: string; - * placeholder: JsonNode; - * each: (localization: Localization, count: number) => string; - * none: (localization: Localization) => string; - * }} Scope - */ - /** * Describes a subschema that applies to every location in some scope, such as * every item in an array, including locations that could be added. The @@ -300,7 +293,7 @@ export const describeEach = (subschemaLocation, placeholder, parent, context) => * the group of what each location requires, given how many things are in it, and `none` describes the scope * being empty, which is what the subschema requires if it's `false`. * - * @type (scope: Scope, instance: JsonNode, schemaLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] + * @type API.describeScope */ export const describeScope = ({ subschemaLocation, placeholder, each, none }, instance, schemaLocation, context) => { if (context.ast[subschemaLocation] === false) { @@ -316,32 +309,32 @@ export const describeScope = ({ subschemaLocation, placeholder, each, none }, in return []; } - return [{ - message: each(context.localization, description.length), - // Every location satisfies all of them or there's one that satisfies at least one - alternatives: context.localization.isNegated - ? limitOptions(description.map((option) => [option]), instance, context) - : [limitItems(description, instance, context)], - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }]; + if (context.localization.isNegated) { + // There's a location that satisfies at least one of them + return [showSmallestFirst({ + message: each(context.localization, description.length), + alternatives: description.map((option) => [option]), + instanceLocation: Instance.uri(instance), + schemaLocations: [schemaLocation] + })]; + } else { + // Every location satisfies all of them + return [{ + message: each(context.localization, description.length), + alternatives: [description], + instanceLocation: Instance.uri(instance), + schemaLocations: [schemaLocation] + }]; + } }; -/** - * @typedef {{ - * condition: (context: API.ErrorHandlerContext) => API.ErrorObject[]; - * then?: (context: API.ErrorHandlerContext) => API.ErrorObject[]; - * else?: (context: API.ErrorHandlerContext) => API.ErrorObject[]; - * }} Conditional - */ - /** * Describes subschemas that only apply under some condition, such as a * property's subschema only applying if the property is present. Each function * describes its part using the given context, so `condition` describes the * condition holding, or with a negated context, not holding. * - * @type (conditional: Conditional, instance: JsonNode, schemaLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] + * @type API.describeConditional */ export const describeConditional = (conditional, instance, schemaLocation, context) => { const positive = context.localization.isNegated ? negate(context) : context; @@ -432,7 +425,7 @@ export const getErrors = (normalizedErrors, rootInstance, context) => { return errors; }; -/** @type API.flattenOutput */ +/** @type (normalizedOutput: API.NormalizedOutput) => API.NormalizedOutput */ export const flattenOutput = (normalizedOutput) => flatten(normalizedOutput, isApplied); /** @@ -484,7 +477,7 @@ const isApplied = () => true; const isRequired = (keywordUri) => !normalizationHandlers[toAbsoluteIri(keywordUri)]?.conditional; /** @type API.getSuccesses */ -export const getSuccesses = (normalizedOutput, rootInstance, context) => { +export const getSuccesses = (subschema, rootInstance, context) => { // Descriptions of nested subschemas can get very large, so stop describing // past some depth and say that there's more if (descriptionDepth >= MAX_DESCRIPTION_DEPTH) { @@ -498,6 +491,12 @@ export const getSuccesses = (normalizedOutput, rootInstance, context) => { return [detailsNotShown]; } + // Subschemas the validator didn't evaluate can still be described because + // descriptions don't depend on results + const normalizedOutput = typeof subschema === "string" + ? evaluateRequirements(subschema, rootInstance, context.ast) + : subschema; + /** @type API.ErrorObject[] */ const successes = []; @@ -553,32 +552,63 @@ const collapseDetailsNotShown = (items) => { const MAX_ENTRIES = 5; +/** @type WeakSet */ +const smallestFirstGroups = new WeakSet(); + +/** + * Options in a group are shown in the order they're given unless the group is + * marked to show the smallest options first so the simplest options are the + * ones that are shown. Only use this if the order doesn't change what the + * options mean. + * + * @type (errorObject: API.ErrorObject) => API.ErrorObject + */ +export const showSmallestFirst = (errorObject) => { + smallestFirstGroups.add(errorObject); + return errorObject; +}; + /** - * A group of things that all need to be true shows only the first few. The - * rest are summarized so it's clear that the list isn't complete. + * Lists of messages can get very long, so only the first few items in a group + * and the first few options in a choice are shown. The rest are summarized so + * it's clear that the list isn't complete. This is done once for all the + * messages so error handlers don't need to. * - * @type (allItems: API.ErrorObject[], instance: JsonNode, context: API.ErrorHandlerContext) => API.ErrorObject[] + * @type (errorObjects: API.ErrorObject[], context: API.ErrorHandlerContext) => API.ErrorObject[] */ -export const limitItems = (allItems, instance, context) => { +export const limitMessages = (errorObjects, context) => errorObjects.map((errorObject) => { + if (!errorObject.alternatives) { + return errorObject; + } + + const alternatives = errorObject.alternatives.map((alternative) => { + return limitItems(limitMessages(alternative, context), errorObject.instanceLocation, context); + }); + + return { + ...errorObject, + // A single alternative is a group of things that all need to be true + alternatives: alternatives.length === 1 + ? alternatives + : limitOptions(alternatives, errorObject.instanceLocation, context, smallestFirstGroups.has(errorObject)) + }; +}); + +/** @type (allItems: API.ErrorObject[], instanceLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] */ +const limitItems = (allItems, instanceLocation, context) => { const items = collapseDetailsNotShown(allItems); if (items.length <= MAX_ENTRIES) { return items; } - return [...items.slice(0, MAX_ENTRIES), notShown(items.length - MAX_ENTRIES, instance, context)]; + return [...items.slice(0, MAX_ENTRIES), notShown(items.length - MAX_ENTRIES, instanceLocation, context)]; }; -/** - * A choice of options shows only the first few. By default, the smallest ones - * are shown first so the simplest options are the ones that are shown. The - * order of options doesn't change what they mean. - * - * @type (allOptions: API.ErrorObject[][], instance: JsonNode, context: API.ErrorHandlerContext, isSorted?: boolean) => API.ErrorObject[][] - */ -export const limitOptions = (allOptions, instance, context, isSorted = true) => { +/** @type (allOptions: API.ErrorObject[][], instanceLocation: string, context: API.ErrorHandlerContext, isSorted: boolean) => API.ErrorObject[][] */ +const limitOptions = (allOptions, instanceLocation, context, isSorted) => { // Only one option that's just "details aren't shown" is needed let hasDetailsNotShown = false; - const options = allOptions.map(collapseDetailsNotShown).filter((option) => { + const options = allOptions.filter((option) => { const isDetailsNotShown = option.length === 1 && detailsNotShownMarkers.has(option[0]); if (isDetailsNotShown && hasDetailsNotShown) { return false; @@ -591,13 +621,13 @@ export const limitOptions = (allOptions, instance, context, isSorted = true) => return sorted; } - return [...sorted.slice(0, MAX_ENTRIES), [notShown(sorted.length - MAX_ENTRIES, instance, context)]]; + return [...sorted.slice(0, MAX_ENTRIES), [notShown(sorted.length - MAX_ENTRIES, instanceLocation, context)]]; }; -/** @type (count: number, instance: JsonNode, context: API.ErrorHandlerContext) => API.ErrorObject */ -const notShown = (count, instance, context) => ({ +/** @type (count: number, instanceLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject */ +const notShown = (count, instanceLocation, context) => ({ message: context.localization.getNotShownMessage(count), - instanceLocation: Instance.uri(instance), + instanceLocation, schemaLocations: [] }); @@ -608,36 +638,32 @@ const sizeOf = (errorObjects) => errorObjects.reduce((size, errorObject) => { }, 0); }, 0); -/** @type (normalizedOutput: API.NormalizedOutput) => boolean */ -export const isPassing = (normalizedOutput) => { - const results = flattenOutput(normalizedOutput); - for (const instanceLocation in results) { - for (const keywordUri in results[instanceLocation]) { - for (const schemaLocation in results[instanceLocation][keywordUri]) { - if (results[instanceLocation][keywordUri][schemaLocation].valid !== true) { - return false; - } - } - } - } - - return true; -}; +/** + * Whether a subschema passed, based on the results of its keywords. It's + * `false` if any keyword failed, `true` if every keyword passed, and + * `undefined` if it isn't known. + * + * @type API.getValidity + */ +export const getValidity = (normalizedOutput) => { + /** @type boolean | undefined */ + let validity = true; -/** @type (normalizedOutput: API.NormalizedOutput) => boolean */ -export const isFailing = (normalizedOutput) => { const results = flattenOutput(normalizedOutput); for (const instanceLocation in results) { for (const keywordUri in results[instanceLocation]) { for (const schemaLocation in results[instanceLocation][keywordUri]) { - if (results[instanceLocation][keywordUri][schemaLocation].valid === false) { - return true; + const { valid } = results[instanceLocation][keywordUri][schemaLocation]; + if (valid === false) { + return false; + } else if (valid === undefined) { + validity = undefined; } } } } - return false; + return validity; }; /** @type WeakSet */ @@ -648,7 +674,7 @@ const allTrueGroups = new WeakSet(); * are described by a choice of options. Present the options as a group that * says how many of the options are true. * - * @type (options: API.ErrorObject[][], range: { min?: number, max?: number }, instance: JsonNode, schemaLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] + * @type API.countTrue */ export const countTrue = (options, { min = 0, max = Infinity }, instance, schemaLocation, context) => { max = Math.min(max, options.length); @@ -658,18 +684,18 @@ export const countTrue = (options, { min = 0, max = Infinity }, instance, schema return []; } else if (min === options.length) { // All of the options are true - return limitItems(options.flat(), instance, context); + return options.flat(); } - return [{ + return [showSmallestFirst({ message: context.localization.getCountTrueMessage(min, max === options.length ? Infinity : max), - alternatives: limitOptions(options.map((option) => limitItems(option, instance, context)), instance, context), + alternatives: options, instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] - }]; + })]; }; -/** @type (options: API.ErrorObject[][], instance: JsonNode, schemaLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] */ +/** @type API.someTrue */ export const someTrue = (options, instance, schemaLocation, context) => { return countTrue(options, { min: 1 }, instance, schemaLocation, context); }; @@ -678,7 +704,7 @@ export const someTrue = (options, instance, schemaLocation, context) => { * Negated success messages are a list of things where at least one is true, but * some keywords need several things to be true. Present those as a group. * - * @type (items: API.ErrorObject[], instance: JsonNode, schemaLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject[] + * @type API.allTrue */ export const allTrue = (items, instance, schemaLocation, context) => { if (items.length <= 1) { @@ -688,7 +714,7 @@ export const allTrue = (items, instance, schemaLocation, context) => { /** @type API.ErrorObject */ const group = { message: context.localization.getAllTrueMessage(), - alternatives: [limitItems(items, instance, context)], + alternatives: [items], instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }; @@ -704,14 +730,14 @@ export const isAllTrueGroup = (errorObject) => allTrueGroups.has(errorObject); * based on the keyword's value. Return `undefined` if the keyword doesn't * require anything. * - * @type (normalizedOutput: API.InstanceOutput, keywordUri: string, instance: JsonNode, ast: AST, toMessage: (value: Value) => string | undefined) => API.ErrorObject[] + * @type (normalizedOutput: API.InstanceOutput, keywordUri: string, instance: JsonNode, toMessage: (value: Value) => string | undefined) => API.ErrorObject[] */ -export const describeKeyword = (normalizedOutput, keywordUri, instance, ast, toMessage) => { +export const describeKeyword = (normalizedOutput, keywordUri, instance, toMessage) => { /** @type API.ErrorObject[] */ const successes = []; for (const schemaLocation in normalizedOutput[keywordUri]) { - const message = toMessage(/** @type any */ (getCompiledKeywordValue(ast, schemaLocation))); + const message = toMessage(/** @type any */ (normalizedOutput[keywordUri][schemaLocation].value)); if (message !== undefined) { successes.push({ message, diff --git a/src/output-format.js b/src/output-format.js index 92336a4..a8c468a 100644 --- a/src/output-format.js +++ b/src/output-format.js @@ -1,11 +1,12 @@ import { getKeyword, setOutputFormat } from "@hyperjump/json-schema/experimental"; import { getShouldValidateFormat } from "@hyperjump/json-schema/draft-2020-12"; -import { getErrors } from "./json-schema-errors.js"; +import { getErrors, limitMessages } from "./json-schema-errors.js"; import { Localization } from "./localization.js"; import { JsonSchemaErrorsOutputPlugin } from "./output-plugin.js"; /** * @import { OutputFormatHandler } from "@hyperjump/json-schema/experimental" + * @import { ErrorHandlerContext } from "./index.d.ts" */ export const JSE = "JSE"; @@ -16,11 +17,13 @@ const jseOutputFormatHandler = (options) => { return { plugin, getErrors: (instance, context) => { - return getErrors(plugin.output, instance, { + /** @type ErrorHandlerContext */ + const errorHandlerContext = { ast: context.ast, localization: Localization.forLocale(options.locale ?? "en-US"), isFormatAsserted - }); + }; + return limitMessages(getErrors(plugin.output, instance, errorHandlerContext), errorHandlerContext); } }; }; diff --git a/src/output-plugin.js b/src/output-plugin.js index 287f16d..7db0c9a 100644 --- a/src/output-plugin.js +++ b/src/output-plugin.js @@ -34,13 +34,13 @@ export class JsonSchemaErrorsOutputPlugin { /** @type NonNullable["afterKeyword"]> */ afterKeyword(keywordNode, instance, context, valid, schemaContext) { - const [keywordUri, schemaLocation] = keywordNode; + 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, outputs: context.subSchemaOutput } - : { valid }; + ? { valid, value, outputs: context.subSchemaOutput } + : { valid, value }; } /** @type NonNullable["afterSchema"]> */ @@ -48,7 +48,7 @@ export class JsonSchemaErrorsOutputPlugin { 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 }; + context.output[Instance.uri(instance)]["https://json-schema.org/validation"][url] = { valid, value: false }; } context.subSchemaOutput ??= []; diff --git a/src/output-plugin.test.ts b/src/output-plugin.test.ts index 9c73fff..50477e3 100644 --- a/src/output-plugin.test.ts +++ b/src/output-plugin.test.ts @@ -26,11 +26,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/ref": { [`${schemaUri}#/$ref`]: { valid: false, + value: `${schemaUri}#/$defs/string`, outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/$defs/string/type`]: { valid: false } + [`${schemaUri}#/$defs/string/type`]: { valid: false, value: "string" } } } } @@ -38,7 +39,7 @@ describe("JSON Schema Errors Output Format", () => { } }, "https://json-schema.org/keyword/definitions": { - [`${schemaUri}#/$defs`]: { valid: true } + [`${schemaUri}#/$defs`]: { valid: true, value: [`${schemaUri}#/$defs/string`] } } } }); @@ -59,11 +60,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/ref": { [`${schemaUri}#/$ref`]: { valid: true, + value: `${schemaUri}#/$defs/string`, outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/$defs/string/type`]: { valid: true } + [`${schemaUri}#/$defs/string/type`]: { valid: true, value: "string" } } } } @@ -71,7 +73,7 @@ describe("JSON Schema Errors Output Format", () => { } }, "https://json-schema.org/keyword/definitions": { - [`${schemaUri}#/$defs`]: { valid: true } + [`${schemaUri}#/$defs`]: { valid: true, value: [`${schemaUri}#/$defs/string`] } } } }); @@ -89,11 +91,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/additionalProperties": { [`${schemaUri}#/additionalProperties`]: { valid: false, + value: [/(?!)/u, `${schemaUri}#/additionalProperties`], outputs: [ { "#/foo": { "https://json-schema.org/validation": { - [`${schemaUri}#/additionalProperties`]: { valid: false } + [`${schemaUri}#/additionalProperties`]: { valid: false, value: false } } } } @@ -114,18 +117,19 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/additionalProperties": { [`${schemaUri}#/additionalProperties`]: { valid: false, + value: [/(?!)/u, `${schemaUri}#/additionalProperties`], outputs: [ { "#/foo": { "https://json-schema.org/validation": { - [`${schemaUri}#/additionalProperties`]: { valid: false } + [`${schemaUri}#/additionalProperties`]: { valid: false, value: false } } } }, { "#/bar": { "https://json-schema.org/validation": { - [`${schemaUri}#/additionalProperties`]: { valid: false } + [`${schemaUri}#/additionalProperties`]: { valid: false, value: false } } } } @@ -148,11 +152,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/additionalProperties": { [`${schemaUri}#/additionalProperties`]: { valid: false, + value: [/(?!)/u, `${schemaUri}#/additionalProperties`], outputs: [ { "#/foo": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/additionalProperties/type`]: { valid: false } + [`${schemaUri}#/additionalProperties/type`]: { valid: false, value: "string" } } } } @@ -171,7 +176,10 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/additionalProperties": { - [`${schemaUri}#/additionalProperties`]: { valid: true } + [`${schemaUri}#/additionalProperties`]: { + valid: true, + value: [/(?!)/u, `${schemaUri}#/additionalProperties`] + } } } }); @@ -194,18 +202,19 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/allOf": { [`${schemaUri}#/allOf`]: { valid: false, + value: [`${schemaUri}#/allOf/0`, `${schemaUri}#/allOf/1`], outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/allOf/0/type`]: { valid: true } + [`${schemaUri}#/allOf/0/type`]: { valid: true, value: "number" } } } }, { "#": { "https://json-schema.org/keyword/maximum": { - [`${schemaUri}#/allOf/1/maximum`]: { valid: false } + [`${schemaUri}#/allOf/1/maximum`]: { valid: false, value: 5 } } } } @@ -230,23 +239,24 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/type`]: { valid: true } + [`${schemaUri}#/type`]: { valid: true, value: "number" } }, "https://json-schema.org/keyword/allOf": { [`${schemaUri}#/allOf`]: { valid: false, + value: [`${schemaUri}#/allOf/0`, `${schemaUri}#/allOf/1`], outputs: [ { "#": { "https://json-schema.org/keyword/maximum": { - [`${schemaUri}#/allOf/0/maximum`]: { valid: false } + [`${schemaUri}#/allOf/0/maximum`]: { valid: false, value: 2 } } } }, { "#": { "https://json-schema.org/keyword/maximum": { - [`${schemaUri}#/allOf/1/maximum`]: { valid: false } + [`${schemaUri}#/allOf/1/maximum`]: { valid: false, value: 5 } } } } @@ -272,18 +282,19 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/allOf": { [`${schemaUri}#/allOf`]: { valid: true, + value: [`${schemaUri}#/allOf/0`, `${schemaUri}#/allOf/1`], outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/allOf/0/type`]: { valid: true } + [`${schemaUri}#/allOf/0/type`]: { valid: true, value: "number" } } } }, { "#": { "https://json-schema.org/keyword/maximum": { - [`${schemaUri}#/allOf/1/maximum`]: { valid: true } + [`${schemaUri}#/allOf/1/maximum`]: { valid: true, value: 5 } } } } @@ -311,18 +322,19 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/anyOf": { [`${schemaUri}#/anyOf`]: { valid: false, + value: [`${schemaUri}#/anyOf/0`, `${schemaUri}#/anyOf/1`], outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/anyOf/0/type`]: { valid: false } + [`${schemaUri}#/anyOf/0/type`]: { valid: false, value: "string" } } } }, { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/anyOf/1/type`]: { valid: false } + [`${schemaUri}#/anyOf/1/type`]: { valid: false, value: "number" } } } } @@ -348,18 +360,19 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/anyOf": { [`${schemaUri}#/anyOf`]: { valid: true, + value: [`${schemaUri}#/anyOf/0`, `${schemaUri}#/anyOf/1`], outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/anyOf/0/type`]: { valid: true } + [`${schemaUri}#/anyOf/0/type`]: { valid: true, value: "string" } } } }, { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/anyOf/1/type`]: { valid: false } + [`${schemaUri}#/anyOf/1/type`]: { valid: false, value: "number" } } } } @@ -387,18 +400,19 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/oneOf": { [`${schemaUri}#/oneOf`]: { valid: false, + value: [`${schemaUri}#/oneOf/0`, `${schemaUri}#/oneOf/1`], outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/oneOf/0/type`]: { valid: false } + [`${schemaUri}#/oneOf/0/type`]: { valid: false, value: "string" } } } }, { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/oneOf/1/type`]: { valid: false } + [`${schemaUri}#/oneOf/1/type`]: { valid: false, value: "number" } } } } @@ -424,18 +438,19 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/oneOf": { [`${schemaUri}#/oneOf`]: { valid: true, + value: [`${schemaUri}#/oneOf/0`, `${schemaUri}#/oneOf/1`], outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/oneOf/0/type`]: { valid: true } + [`${schemaUri}#/oneOf/0/type`]: { valid: true, value: "string" } } } }, { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/oneOf/1/type`]: { valid: false } + [`${schemaUri}#/oneOf/1/type`]: { valid: false, value: "number" } } } } @@ -460,11 +475,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/not": { [`${schemaUri}#/not`]: { valid: false, + value: `${schemaUri}#/not`, outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/not/type`]: { valid: true } + [`${schemaUri}#/not/type`]: { valid: true, value: "number" } } } } @@ -487,11 +503,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/not": { [`${schemaUri}#/not`]: { valid: true, + value: `${schemaUri}#/not`, outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/not/type`]: { valid: false } + [`${schemaUri}#/not/type`]: { valid: false, value: "number" } } } } @@ -516,18 +533,23 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/contains": { [`${schemaUri}#/contains`]: { valid: false, + value: { + contains: `${schemaUri}#/contains`, + minContains: 1, + maxContains: Number.MAX_SAFE_INTEGER + }, outputs: [ { "#/0": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/contains/type`]: { valid: false } + [`${schemaUri}#/contains/type`]: { valid: false, value: "string" } } } }, { "#/1": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/contains/type`]: { valid: false } + [`${schemaUri}#/contains/type`]: { valid: false, value: "string" } } } } @@ -550,18 +572,23 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/contains": { [`${schemaUri}#/contains`]: { valid: true, + value: { + contains: `${schemaUri}#/contains`, + minContains: 1, + maxContains: Number.MAX_SAFE_INTEGER + }, outputs: [ { "#/0": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/contains/type`]: { valid: false } + [`${schemaUri}#/contains/type`]: { valid: false, value: "string" } } } }, { "#/1": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/contains/type`]: { valid: true } + [`${schemaUri}#/contains/type`]: { valid: true, value: "string" } } } } @@ -588,11 +615,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/dependentSchemas": { [`${schemaUri}#/dependentSchemas`]: { valid: false, + value: [["foo", `${schemaUri}#/dependentSchemas/foo`]], outputs: [ { "#": { "https://json-schema.org/keyword/required": { - [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: false } + [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: false, value: ["a"] } } } } @@ -618,18 +646,22 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/dependentSchemas": { [`${schemaUri}#/dependentSchemas`]: { valid: false, + value: [ + ["foo", `${schemaUri}#/dependentSchemas/foo`], + ["bar", `${schemaUri}#/dependentSchemas/bar`] + ], outputs: [ { "#": { "https://json-schema.org/keyword/required": { - [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: false } + [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: false, value: ["a"] } } } }, { "#": { "https://json-schema.org/keyword/required": { - [`${schemaUri}#/dependentSchemas/bar/required`]: { valid: false } + [`${schemaUri}#/dependentSchemas/bar/required`]: { valid: false, value: ["b"] } } } } @@ -654,11 +686,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/dependentSchemas": { [`${schemaUri}#/dependentSchemas`]: { valid: true, + value: [["foo", `${schemaUri}#/dependentSchemas/foo`]], outputs: [ { "#": { "https://json-schema.org/keyword/required": { - [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: true } + [`${schemaUri}#/dependentSchemas/foo/required`]: { valid: true, value: ["a"] } } } } @@ -684,11 +717,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/if": { [`${schemaUri}#/if`]: { valid: true, + value: `${schemaUri}#/if`, outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/if/type`]: { valid: true } + [`${schemaUri}#/if/type`]: { valid: true, value: "string" } } } } @@ -698,11 +732,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/then": { [`${schemaUri}#/then`]: { valid: false, + value: [`${schemaUri}#/if`, `${schemaUri}#/then`], outputs: [ { "#": { "https://json-schema.org/keyword/minLength": { - [`${schemaUri}#/then/minLength`]: { valid: false } + [`${schemaUri}#/then/minLength`]: { valid: false, value: 1 } } } } @@ -726,11 +761,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/if": { [`${schemaUri}#/if`]: { valid: true, + value: `${schemaUri}#/if`, outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/if/type`]: { valid: true } + [`${schemaUri}#/if/type`]: { valid: true, value: "string" } } } } @@ -740,11 +776,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/then": { [`${schemaUri}#/then`]: { valid: true, + value: [`${schemaUri}#/if`, `${schemaUri}#/then`], outputs: [ { "#": { "https://json-schema.org/keyword/minLength": { - [`${schemaUri}#/then/minLength`]: { valid: true } + [`${schemaUri}#/then/minLength`]: { valid: true, value: 1 } } } } @@ -769,16 +806,17 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/type`]: { valid: true } + [`${schemaUri}#/type`]: { valid: true, value: ["string", "number"] } }, "https://json-schema.org/keyword/if": { [`${schemaUri}#/if`]: { valid: true, + value: `${schemaUri}#/if`, outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/if/type`]: { valid: false } + [`${schemaUri}#/if/type`]: { valid: false, value: "string" } } } } @@ -788,11 +826,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/else": { [`${schemaUri}#/else`]: { valid: false, + value: [`${schemaUri}#/if`, `${schemaUri}#/else`], outputs: [ { "#": { "https://json-schema.org/keyword/minimum": { - [`${schemaUri}#/else/minimum`]: { valid: false } + [`${schemaUri}#/else/minimum`]: { valid: false, value: 42 } } } } @@ -815,16 +854,17 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/type`]: { valid: true } + [`${schemaUri}#/type`]: { valid: true, value: ["string", "number"] } }, "https://json-schema.org/keyword/if": { [`${schemaUri}#/if`]: { valid: true, + value: `${schemaUri}#/if`, outputs: [ { "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/if/type`]: { valid: false } + [`${schemaUri}#/if/type`]: { valid: false, value: "string" } } } } @@ -834,11 +874,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/else": { [`${schemaUri}#/else`]: { valid: true, + value: [`${schemaUri}#/if`, `${schemaUri}#/else`], outputs: [ { "#": { "https://json-schema.org/keyword/minimum": { - [`${schemaUri}#/else/minimum`]: { valid: true } + [`${schemaUri}#/else/minimum`]: { valid: true, value: 5 } } } } @@ -863,18 +904,19 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/items": { [`${schemaUri}#/items`]: { valid: false, + value: [0, `${schemaUri}#/items`], outputs: [ { "#/0": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/items/type`]: { valid: false } + [`${schemaUri}#/items/type`]: { valid: false, value: "string" } } } }, { "#/1": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/items/type`]: { valid: false } + [`${schemaUri}#/items/type`]: { valid: false, value: "string" } } } } @@ -897,11 +939,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/items": { [`${schemaUri}#/items`]: { valid: true, + value: [0, `${schemaUri}#/items`], outputs: [ { "#/0": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/items/type`]: { valid: true } + [`${schemaUri}#/items/type`]: { valid: true, value: "string" } } } } @@ -929,18 +972,22 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/patternProperties": { [`${schemaUri}#/patternProperties`]: { valid: false, + value: [ + [/^f/u, `${schemaUri}#/patternProperties/%5Ef`], + [/^b/u, `${schemaUri}#/patternProperties/%5Eb`] + ], outputs: [ { "#/foo": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/patternProperties/%5Ef/type`]: { valid: false } + [`${schemaUri}#/patternProperties/%5Ef/type`]: { valid: false, value: "string" } } } }, { "#/bar": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/patternProperties/%5Eb/type`]: { valid: false } + [`${schemaUri}#/patternProperties/%5Eb/type`]: { valid: false, value: "number" } } } } @@ -966,18 +1013,22 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/patternProperties": { [`${schemaUri}#/patternProperties`]: { valid: true, + value: [ + [/^f/u, `${schemaUri}#/patternProperties/%5Ef`], + [/^b/u, `${schemaUri}#/patternProperties/%5Eb`] + ], outputs: [ { "#/foo": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/patternProperties/%5Ef/type`]: { valid: true } + [`${schemaUri}#/patternProperties/%5Ef/type`]: { valid: true, value: "string" } } } }, { "#/bar": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/patternProperties/%5Eb/type`]: { valid: true } + [`${schemaUri}#/patternProperties/%5Eb/type`]: { valid: true, value: "number" } } } } @@ -1005,18 +1056,22 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/prefixItems": { [`${schemaUri}#/prefixItems`]: { valid: false, + value: [ + `${schemaUri}#/prefixItems/0`, + `${schemaUri}#/prefixItems/1` + ], outputs: [ { "#/0": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/prefixItems/0/type`]: { valid: false } + [`${schemaUri}#/prefixItems/0/type`]: { valid: false, value: "string" } } } }, { "#/1": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/prefixItems/1/type`]: { valid: false } + [`${schemaUri}#/prefixItems/1/type`]: { valid: false, value: "number" } } } } @@ -1042,18 +1097,22 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/prefixItems": { [`${schemaUri}#/prefixItems`]: { valid: true, + value: [ + `${schemaUri}#/prefixItems/0`, + `${schemaUri}#/prefixItems/1` + ], outputs: [ { "#/0": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/prefixItems/0/type`]: { valid: true } + [`${schemaUri}#/prefixItems/0/type`]: { valid: true, value: "string" } } } }, { "#/1": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/prefixItems/1/type`]: { valid: true } + [`${schemaUri}#/prefixItems/1/type`]: { valid: true, value: "number" } } } } @@ -1081,18 +1140,22 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties`]: { valid: false, + value: { + foo: `${schemaUri}#/properties/foo`, + bar: `${schemaUri}#/properties/bar` + }, outputs: [ { "#/foo": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/foo/type`]: { valid: false } + [`${schemaUri}#/properties/foo/type`]: { valid: false, value: "string" } } } }, { "#/bar": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/bar/type`]: { valid: false } + [`${schemaUri}#/properties/bar/type`]: { valid: false, value: "number" } } } } @@ -1118,18 +1181,22 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties`]: { valid: true, + value: { + foo: `${schemaUri}#/properties/foo`, + bar: `${schemaUri}#/properties/bar` + }, outputs: [ { "#/foo": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/foo/type`]: { valid: true } + [`${schemaUri}#/properties/foo/type`]: { valid: true, value: "string" } } } }, { "#/bar": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/bar/type`]: { valid: true } + [`${schemaUri}#/properties/bar/type`]: { valid: true, value: "number" } } } } @@ -1154,18 +1221,19 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/propertyNames": { [`${schemaUri}#/propertyNames`]: { valid: false, + value: `${schemaUri}#/propertyNames`, outputs: [ { "#*/banana": { "https://json-schema.org/keyword/pattern": { - [`${schemaUri}#/propertyNames/pattern`]: { valid: false } + [`${schemaUri}#/propertyNames/pattern`]: { valid: false, value: /^a/u } } } }, { "#*/pear": { "https://json-schema.org/keyword/pattern": { - [`${schemaUri}#/propertyNames/pattern`]: { valid: false } + [`${schemaUri}#/propertyNames/pattern`]: { valid: false, value: /^a/u } } } } @@ -1188,11 +1256,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/propertyNames": { [`${schemaUri}#/propertyNames`]: { valid: true, + value: `${schemaUri}#/propertyNames`, outputs: [ { "#*/apple": { "https://json-schema.org/keyword/pattern": { - [`${schemaUri}#/propertyNames/pattern`]: { valid: true } + [`${schemaUri}#/propertyNames/pattern`]: { valid: true, value: /^a/u } } } } @@ -1215,11 +1284,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/unevaluatedProperties": { [`${schemaUri}#/unevaluatedProperties`]: { valid: false, + value: `${schemaUri}#/unevaluatedProperties`, outputs: [ { "#/foo": { "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedProperties`]: { valid: false } + [`${schemaUri}#/unevaluatedProperties`]: { valid: false, value: false } } } } @@ -1246,13 +1316,17 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties`]: { valid: false, + value: { + foo: `${schemaUri}#/properties/foo`, + bar: `${schemaUri}#/properties/bar` + }, outputs: [ { }, { "#/bar": { "https://json-schema.org/validation": { - [`${schemaUri}#/properties/bar`]: { valid: false } + [`${schemaUri}#/properties/bar`]: { valid: false, value: false } } } } @@ -1262,11 +1336,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/unevaluatedProperties": { [`${schemaUri}#/unevaluatedProperties`]: { valid: false, + value: `${schemaUri}#/unevaluatedProperties`, outputs: [ { "#/baz": { "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedProperties`]: { valid: false } + [`${schemaUri}#/unevaluatedProperties`]: { valid: false, value: false } } } } @@ -1289,11 +1364,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/unevaluatedProperties": { [`${schemaUri}#/unevaluatedProperties`]: { valid: false, + value: `${schemaUri}#/unevaluatedProperties`, outputs: [ { "#/foo": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/unevaluatedProperties/type`]: { valid: false } + [`${schemaUri}#/unevaluatedProperties/type`]: { valid: false, value: "string" } } } } @@ -1312,7 +1388,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/unevaluatedProperties": { - [`${schemaUri}#/unevaluatedProperties`]: { valid: true } + [`${schemaUri}#/unevaluatedProperties`]: { valid: true, value: `${schemaUri}#/unevaluatedProperties` } } } }); @@ -1330,11 +1406,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/unevaluatedItems": { [`${schemaUri}#/unevaluatedItems`]: { valid: false, + value: `${schemaUri}#/unevaluatedItems`, outputs: [ { "#/0": { "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedItems`]: { valid: false } + [`${schemaUri}#/unevaluatedItems`]: { valid: false, value: false } } } } @@ -1357,11 +1434,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/unevaluatedItems": { [`${schemaUri}#/unevaluatedItems`]: { valid: false, + value: `${schemaUri}#/unevaluatedItems`, outputs: [ { "#/0": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/unevaluatedItems/type`]: { valid: false } + [`${schemaUri}#/unevaluatedItems/type`]: { valid: false, value: "string" } } } } @@ -1385,13 +1463,17 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/prefixItems": { [`${schemaUri}#/prefixItems`]: { valid: false, + value: [ + `${schemaUri}#/prefixItems/0`, + `${schemaUri}#/prefixItems/1` + ], outputs: [ { }, { "#/1": { "https://json-schema.org/validation": { - [`${schemaUri}#/prefixItems/1`]: { valid: false } + [`${schemaUri}#/prefixItems/1`]: { valid: false, value: false } } } } @@ -1401,11 +1483,12 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/unevaluatedItems": { [`${schemaUri}#/unevaluatedItems`]: { valid: false, + value: `${schemaUri}#/unevaluatedItems`, outputs: [ { "#/2": { "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedItems`]: { valid: false } + [`${schemaUri}#/unevaluatedItems`]: { valid: false, value: false } } } } @@ -1424,7 +1507,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/unevaluatedItems": { - [`${schemaUri}#/unevaluatedItems`]: { valid: true } + [`${schemaUri}#/unevaluatedItems`]: { valid: true, value: `${schemaUri}#/unevaluatedItems` } } } }); @@ -1440,7 +1523,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/const": { - [`${schemaUri}#/const`]: { valid: false } + [`${schemaUri}#/const`]: { valid: false, value: "\"foo\"" } } } }); @@ -1454,7 +1537,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/const": { - [`${schemaUri}#/const`]: { valid: true } + [`${schemaUri}#/const`]: { valid: true, value: "\"foo\"" } } } }); @@ -1474,7 +1557,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/dependentRequired": { - [`${schemaUri}#/dependentRequired`]: { valid: false } + [`${schemaUri}#/dependentRequired`]: { valid: false, value: [["foo", ["a"]]] } } } }); @@ -1493,7 +1576,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/dependentRequired": { - [`${schemaUri}#/dependentRequired`]: { valid: false } + [`${schemaUri}#/dependentRequired`]: { valid: false, value: [["foo", ["a"]], ["bar", ["b"]]] } } } }); @@ -1511,7 +1594,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/dependentRequired": { - [`${schemaUri}#/dependentRequired`]: { valid: true } + [`${schemaUri}#/dependentRequired`]: { valid: true, value: [["foo", ["a"]]] } } } }); @@ -1527,7 +1610,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/enum": { - [`${schemaUri}#/enum`]: { valid: false } + [`${schemaUri}#/enum`]: { valid: false, value: ["\"foo\""] } } } }); @@ -1541,7 +1624,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/enum": { - [`${schemaUri}#/enum`]: { valid: true } + [`${schemaUri}#/enum`]: { valid: true, value: ["\"foo\""] } } } }); @@ -1557,7 +1640,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/exclusiveMaximum": { - [`${schemaUri}#/exclusiveMaximum`]: { valid: false } + [`${schemaUri}#/exclusiveMaximum`]: { valid: false, value: 5 } } } }); @@ -1571,7 +1654,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/exclusiveMaximum": { - [`${schemaUri}#/exclusiveMaximum`]: { valid: true } + [`${schemaUri}#/exclusiveMaximum`]: { valid: true, value: 42 } } } }); @@ -1587,7 +1670,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/exclusiveMinimum": { - [`${schemaUri}#/exclusiveMinimum`]: { valid: false } + [`${schemaUri}#/exclusiveMinimum`]: { valid: false, value: 42 } } } }); @@ -1601,7 +1684,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/exclusiveMinimum": { - [`${schemaUri}#/exclusiveMinimum`]: { valid: true } + [`${schemaUri}#/exclusiveMinimum`]: { valid: true, value: 5 } } } }); @@ -1617,7 +1700,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/maxItems": { - [`${schemaUri}#/maxItems`]: { valid: false } + [`${schemaUri}#/maxItems`]: { valid: false, value: 1 } } } }); @@ -1631,7 +1714,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/maxItems": { - [`${schemaUri}#/maxItems`]: { valid: true } + [`${schemaUri}#/maxItems`]: { valid: true, value: 1 } } } }); @@ -1647,7 +1730,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/minItems": { - [`${schemaUri}#/minItems`]: { valid: false } + [`${schemaUri}#/minItems`]: { valid: false, value: 1 } } } }); @@ -1661,7 +1744,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/minItems": { - [`${schemaUri}#/minItems`]: { valid: true } + [`${schemaUri}#/minItems`]: { valid: true, value: 1 } } } }); @@ -1677,7 +1760,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/maxLength": { - [`${schemaUri}#/maxLength`]: { valid: false } + [`${schemaUri}#/maxLength`]: { valid: false, value: 2 } } } }); @@ -1691,7 +1774,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/maxLength": { - [`${schemaUri}#/maxLength`]: { valid: true } + [`${schemaUri}#/maxLength`]: { valid: true, value: 2 } } } }); @@ -1707,7 +1790,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/minLength": { - [`${schemaUri}#/minLength`]: { valid: false } + [`${schemaUri}#/minLength`]: { valid: false, value: 2 } } } }); @@ -1721,7 +1804,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/minLength": { - [`${schemaUri}#/minLength`]: { valid: true } + [`${schemaUri}#/minLength`]: { valid: true, value: 1 } } } }); @@ -1737,7 +1820,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/maxProperties": { - [`${schemaUri}#/maxProperties`]: { valid: false } + [`${schemaUri}#/maxProperties`]: { valid: false, value: 1 } } } }); @@ -1751,7 +1834,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/maxProperties": { - [`${schemaUri}#/maxProperties`]: { valid: true } + [`${schemaUri}#/maxProperties`]: { valid: true, value: 1 } } } }); @@ -1767,7 +1850,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/minProperties": { - [`${schemaUri}#/minProperties`]: { valid: false } + [`${schemaUri}#/minProperties`]: { valid: false, value: 1 } } } }); @@ -1781,7 +1864,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/minProperties": { - [`${schemaUri}#/minProperties`]: { valid: true } + [`${schemaUri}#/minProperties`]: { valid: true, value: 1 } } } }); @@ -1797,7 +1880,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/maximum": { - [`${schemaUri}#/maximum`]: { valid: false } + [`${schemaUri}#/maximum`]: { valid: false, value: 5 } } } }); @@ -1811,7 +1894,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/maximum": { - [`${schemaUri}#/maximum`]: { valid: true } + [`${schemaUri}#/maximum`]: { valid: true, value: 42 } } } }); @@ -1827,7 +1910,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/minimum": { - [`${schemaUri}#/minimum`]: { valid: false } + [`${schemaUri}#/minimum`]: { valid: false, value: 42 } } } }); @@ -1841,7 +1924,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/minimum": { - [`${schemaUri}#/minimum`]: { valid: true } + [`${schemaUri}#/minimum`]: { valid: true, value: 5 } } } }); @@ -1857,7 +1940,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/multipleOf": { - [`${schemaUri}#/multipleOf`]: { valid: false } + [`${schemaUri}#/multipleOf`]: { valid: false, value: 2 } } } }); @@ -1871,7 +1954,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/multipleOf": { - [`${schemaUri}#/multipleOf`]: { valid: true } + [`${schemaUri}#/multipleOf`]: { valid: true, value: 2 } } } }); @@ -1887,7 +1970,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/pattern": { - [`${schemaUri}#/pattern`]: { valid: false } + [`${schemaUri}#/pattern`]: { valid: false, value: /^a/u } } } }); @@ -1901,7 +1984,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/pattern": { - [`${schemaUri}#/pattern`]: { valid: true } + [`${schemaUri}#/pattern`]: { valid: true, value: /^a/u } } } }); @@ -1917,7 +2000,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/required": { - [`${schemaUri}#/required`]: { valid: false } + [`${schemaUri}#/required`]: { valid: false, value: ["a"] } } } }); @@ -1931,7 +2014,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/required": { - [`${schemaUri}#/required`]: { valid: false } + [`${schemaUri}#/required`]: { valid: false, value: ["a", "b"] } } } }); @@ -1945,7 +2028,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/required": { - [`${schemaUri}#/required`]: { valid: true } + [`${schemaUri}#/required`]: { valid: true, value: ["a"] } } } }); @@ -1961,7 +2044,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/type`]: { valid: false } + [`${schemaUri}#/type`]: { valid: false, value: "string" } } } }); @@ -1975,7 +2058,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/type`]: { valid: false } + [`${schemaUri}#/type`]: { valid: false, value: ["string", "null"] } } } }); @@ -1989,7 +2072,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/type`]: { valid: true } + [`${schemaUri}#/type`]: { valid: true, value: "string" } } } }); @@ -2005,7 +2088,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/uniqueItems": { - [`${schemaUri}#/uniqueItems`]: { valid: false } + [`${schemaUri}#/uniqueItems`]: { valid: false, value: true } } } }); @@ -2019,7 +2102,7 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "https://json-schema.org/keyword/uniqueItems": { - [`${schemaUri}#/uniqueItems`]: { valid: true } + [`${schemaUri}#/uniqueItems`]: { valid: true, value: true } } } }); @@ -2042,11 +2125,15 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties`]: { valid: false, + value: { + foo: `${schemaUri}#/properties/foo`, + bar: `${schemaUri}#/properties/bar` + }, outputs: [ { "#/foo": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/foo/type`]: { valid: false } + [`${schemaUri}#/properties/foo/type`]: { valid: false, value: "string" } } } } @@ -2054,7 +2141,7 @@ describe("JSON Schema Errors Output Format", () => { } }, "https://json-schema.org/keyword/required": { - [`${schemaUri}#/required`]: { valid: false } + [`${schemaUri}#/required`]: { valid: false, value: ["foo", "bar"] } } } }); @@ -2078,17 +2165,19 @@ describe("JSON Schema Errors Output Format", () => { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties`]: { valid: false, + value: { foo: `${schemaUri}#/properties/foo` }, outputs: [ { "#/foo": { "https://json-schema.org/keyword/properties": { [`${schemaUri}#/properties/foo/properties`]: { valid: false, + value: { bar: `${schemaUri}#/properties/foo/properties/bar` }, outputs: [ { "#/foo/bar": { "https://json-schema.org/keyword/type": { - [`${schemaUri}#/properties/foo/properties/bar/type`]: { valid: false } + [`${schemaUri}#/properties/foo/properties/bar/type`]: { valid: false, value: "boolean" } } } } diff --git a/src/test-suite/tests/anyOf.json b/src/test-suite/tests/anyOf.json index 5fd451a..bb26a58 100644 --- a/src/test-suite/tests/anyOf.json +++ b/src/test-suite/tests/anyOf.json @@ -46,6 +46,99 @@ } ] }, + { + "description": "anyOf with more alternatives than are shown", + "schema": { + "anyOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" }, + { "type": "array" }, + { "type": "object" }, + { "type": "integer" } + ] + }, + "instance": null, + "errors": [ + { + "messageId": "anyOf-message", + "alternatives": [ + [ + { + "messageId": "type-message", + "messageParams": { + "count": 1, + "type": "string", + "expectedTypes": { "or": ["string"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/0/type"] + } + ], + [ + { + "messageId": "type-message", + "messageParams": { + "count": 1, + "type": "number", + "expectedTypes": { "or": ["number"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/1/type"] + } + ], + [ + { + "messageId": "type-message", + "messageParams": { + "count": 1, + "type": "boolean", + "expectedTypes": { "or": ["boolean"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/2/type"] + } + ], + [ + { + "messageId": "type-message", + "messageParams": { + "count": 1, + "type": "array", + "expectedTypes": { "or": ["array"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/3/type"] + } + ], + [ + { + "messageId": "type-message", + "messageParams": { + "count": 1, + "type": "object", + "expectedTypes": { "or": ["object"] } + }, + "instanceLocation": "#", + "schemaLocations": ["#/anyOf/4/type"] + } + ], + [ + { + "messageId": "not-shown-message", + "messageParams": { + "count": 1 + }, + "instanceLocation": "#", + "schemaLocations": [] + } + ] + ], + "instanceLocation": "#", + "schemaLocations": ["#/anyOf"] + } + ] + }, { "description": "anyOf pass", "schema": { From b0b6d1cf9093b13a43a5fc5257667db3dd20a4a1 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Mon, 5 Oct 2026 14:14:21 -0700 Subject: [PATCH 6/6] Define keywords with defineKeyword Supporting a keyword took a normalization handler and an error handler registered separately, even for keywords that only need a message for each occurrence that fails. defineKeyword adds a keyword with an error message for each failing occurrence and a requirement that describes what it requires. Applicators give an evaluate function, and annotations are marked as annotations. Every keyword is now defined with defineKeyword, so setNormalizationHandler is removed and the NormalizationHandler type is folded into KeywordDefinition. Keywords that only needed an empty normalization handler are defined inline, and the applicators' handlers moved to src/keywords. pattern's messages are part of its definition. Keywords that combine occurrences into one message or are described together still use error handlers. Keywords are defined after the error handlers because registration order determines message order, so pattern's messages now come last. Co-Authored-By: Claude Opus 5.5 --- README.md | 114 ++++----- src/define-keyword.test.js | 130 ++++++++++ src/error-handlers/contains.js | 2 +- src/error-handlers/pattern.js | 39 --- src/index.d.ts | 92 ++++--- src/index.js | 225 ++++++++---------- src/json-schema-errors.js | 59 ++++- .../additionalProperties.js | 8 +- .../allOf.js | 8 +- .../anyOf.js | 8 +- .../contains.js | 8 +- .../dependentSchemas.js | 8 +- .../draft-04/additionalItems.js | 8 +- .../draft-04/dependencies.js | 8 +- .../draft-04/items.js | 8 +- .../draft-06/contains.js | 8 +- .../dynamicRef.js | 8 +- .../else.js | 8 +- .../if.js | 8 +- .../items.js | 8 +- src/keywords/not.js | 14 ++ .../oneOf.js | 8 +- src/keywords/pattern.js | 11 + .../patternProperties.js | 8 +- .../prefixItems.js | 8 +- .../properties.js | 8 +- .../propertyNames.js | 8 +- src/keywords/ref.js | 14 ++ .../then.js | 8 +- .../unevaluatedItems.js | 8 +- .../unevaluatedProperties.js | 8 +- src/normalization-handlers/comment.js | 12 - src/normalization-handlers/const.js | 11 - src/normalization-handlers/default.js | 12 - src/normalization-handlers/definitions.js | 12 - .../dependentRequired.js | 11 - src/normalization-handlers/deprecated.js | 12 - src/normalization-handlers/description.js | 12 - .../draft-04/exclusiveMaximum.js | 11 - .../draft-04/exclusiveMinimum.js | 11 - .../draft-04/maximum.js | 11 - .../draft-04/minimum.js | 11 - src/normalization-handlers/enum.js | 11 - src/normalization-handlers/examples.js | 12 - .../exclusiveMaximum.js | 11 - .../exclusiveMinimum.js | 11 - src/normalization-handlers/format.js | 11 - src/normalization-handlers/maxContains.js | 11 - src/normalization-handlers/maxItems.js | 11 - src/normalization-handlers/maxLength.js | 11 - src/normalization-handlers/maxProperties.js | 11 - src/normalization-handlers/maximum.js | 11 - src/normalization-handlers/minContains.js | 11 - src/normalization-handlers/minItems.js | 11 - src/normalization-handlers/minLength.js | 11 - src/normalization-handlers/minProperties.js | 11 - src/normalization-handlers/minimum.js | 11 - src/normalization-handlers/multipleOf.js | 11 - src/normalization-handlers/not.js | 14 -- src/normalization-handlers/pattern.js | 11 - src/normalization-handlers/readOnly.js | 12 - src/normalization-handlers/ref.js | 14 -- src/normalization-handlers/required.js | 11 - src/normalization-handlers/title.js | 12 - src/normalization-handlers/type.js | 11 - src/normalization-handlers/uniqueItems.js | 11 - src/normalization-handlers/unknown.js | 12 - src/normalization-handlers/writeOnly.js | 12 - 68 files changed, 510 insertions(+), 781 deletions(-) create mode 100644 src/define-keyword.test.js delete mode 100644 src/error-handlers/pattern.js rename src/{normalization-handlers => keywords}/additionalProperties.js (75%) rename src/{normalization-handlers => keywords}/allOf.js (61%) rename src/{normalization-handlers => keywords}/anyOf.js (61%) rename src/{normalization-handlers => keywords}/contains.js (74%) rename src/{normalization-handlers => keywords}/dependentSchemas.js (74%) rename src/{normalization-handlers => keywords}/draft-04/additionalItems.js (71%) rename src/{normalization-handlers => keywords}/draft-04/dependencies.js (72%) rename src/{normalization-handlers => keywords}/draft-04/items.js (77%) rename src/{normalization-handlers => keywords}/draft-06/contains.js (68%) rename src/{normalization-handlers => keywords}/dynamicRef.js (71%) rename src/{normalization-handlers => keywords}/else.js (53%) rename src/{normalization-handlers => keywords}/if.js (51%) rename src/{normalization-handlers => keywords}/items.js (73%) create mode 100644 src/keywords/not.js rename src/{normalization-handlers => keywords}/oneOf.js (61%) create mode 100644 src/keywords/pattern.js rename src/{normalization-handlers => keywords}/patternProperties.js (76%) rename src/{normalization-handlers => keywords}/prefixItems.js (74%) rename src/{normalization-handlers => keywords}/properties.js (74%) rename src/{normalization-handlers => keywords}/propertyNames.js (70%) create mode 100644 src/keywords/ref.js rename src/{normalization-handlers => keywords}/then.js (51%) rename src/{normalization-handlers => keywords}/unevaluatedItems.js (69%) rename src/{normalization-handlers => keywords}/unevaluatedProperties.js (69%) delete mode 100644 src/normalization-handlers/comment.js delete mode 100644 src/normalization-handlers/const.js delete mode 100644 src/normalization-handlers/default.js delete mode 100644 src/normalization-handlers/definitions.js delete mode 100644 src/normalization-handlers/dependentRequired.js delete mode 100644 src/normalization-handlers/deprecated.js delete mode 100644 src/normalization-handlers/description.js delete mode 100644 src/normalization-handlers/draft-04/exclusiveMaximum.js delete mode 100644 src/normalization-handlers/draft-04/exclusiveMinimum.js delete mode 100644 src/normalization-handlers/draft-04/maximum.js delete mode 100644 src/normalization-handlers/draft-04/minimum.js delete mode 100644 src/normalization-handlers/enum.js delete mode 100644 src/normalization-handlers/examples.js delete mode 100644 src/normalization-handlers/exclusiveMaximum.js delete mode 100644 src/normalization-handlers/exclusiveMinimum.js delete mode 100644 src/normalization-handlers/format.js delete mode 100644 src/normalization-handlers/maxContains.js delete mode 100644 src/normalization-handlers/maxItems.js delete mode 100644 src/normalization-handlers/maxLength.js delete mode 100644 src/normalization-handlers/maxProperties.js delete mode 100644 src/normalization-handlers/maximum.js delete mode 100644 src/normalization-handlers/minContains.js delete mode 100644 src/normalization-handlers/minItems.js delete mode 100644 src/normalization-handlers/minLength.js delete mode 100644 src/normalization-handlers/minProperties.js delete mode 100644 src/normalization-handlers/minimum.js delete mode 100644 src/normalization-handlers/multipleOf.js delete mode 100644 src/normalization-handlers/not.js delete mode 100644 src/normalization-handlers/pattern.js delete mode 100644 src/normalization-handlers/readOnly.js delete mode 100644 src/normalization-handlers/ref.js delete mode 100644 src/normalization-handlers/required.js delete mode 100644 src/normalization-handlers/title.js delete mode 100644 src/normalization-handlers/type.js delete mode 100644 src/normalization-handlers/uniqueItems.js delete mode 100644 src/normalization-handlers/unknown.js delete mode 100644 src/normalization-handlers/writeOnly.js diff --git a/README.md b/README.md index 26d88e3..d1e9fa1 100644 --- a/README.md +++ b/README.md @@ -160,75 +160,95 @@ type-message = Une valeur de type {$expectedTypes} est attendue https://json-schema-errors.hyperjump.io -## Custom Keywords and Error Handlers +## Custom Keywords -`@hyperjump/json-schema-errors` uses a two phase process. In order to support a -custom keyword we'll need to register a handler for each phase of the process. - -1. **Normalization**: This phase takes the raw error output from the validator - and converts it to a `NormalizedOutput`. - -2. **Error Handling**: This phase takes the `NormalizedOutput` and uses it to - generate the error messages that will be presented to the user. - -Here's an example of adding support for a simple keyword called `startsWith`. -`startsWith` takes a string and asserts that a string JSON instance starts with -the value of `startsWith`. The keyword itself needs to be defined with -`@hyperjump/json-schema`'s `addKeyword` and included in a dialect. +A custom keyword needs to be defined with `@hyperjump/json-schema`'s +`addKeyword` and included in a dialect. Then use `defineKeyword` to add messages +for it. Here's an example for a keyword called `startsWith` that asserts that a +string starts with the keyword's value. Messages for a keyword use the ids `{keyword}-message` for errors and `{keyword}-success-message` and `{keyword}-negated-message` for describing what the keyword requires. ```TypeScript -import { addTranslation } from "@hyperjump/json-schema-errors"; +import { addTranslation, defineKeyword } from "@hyperjump/json-schema-errors"; addTranslation("en-US", ` startsWith-message = Expected a string that starts with '{$prefix}' startsWith-success-message = The value is either not a string or starts with '{$prefix}' startsWith-negated-message = The value is a string that doesn't start with '{$prefix}' `); + +defineKeyword("https://example.com/keyword/startsWith", { + error: (prefix, localization) => localization.format("startsWith-message", { prefix }), + requirement: (prefix, localization) => localization.formatRequirement("startsWith", { prefix }) +}); ``` -Every keyword needs a normalization handler. Keywords that aren't applicators -don't have anything to evaluate. +`error` is the message for each occurrence of the keyword that failed. It gets +the keyword's value as compiled by its `@hyperjump/json-schema` definition and +the value that failed. + +`requirement` describes what the keyword requires. It's used to explain failures +caused by a subschema passing, such as with `not`. Then, it describes what would +make the keyword fail instead, so `formatRequirement` picks the success or +negated message. A keyword without a `requirement` can't be described, so +messages for keywords like `not` and `oneOf` will be less specific. + +Keywords that are only annotations, like `title`, never fail and don't require +anything. ```TypeScript -import { setNormalizationHandler } from "@hyperjump/json-schema-errors"; +defineKeyword("https://example.com/keyword/note", { annotation: true }); +``` -const KEYWORD_URI = "https://example.com/keyword/startsWith"; +Simple applicator keywords that just evaluate subschemas and don't make any +assertions of their own only need to evaluate their subschemas. Whether a +keyword is a simple applicator comes from the `simpleApplicator` property of its +`@hyperjump/json-schema` keyword definition. The results of its subschemas are +treated as results of the parent schema. For example, support for the `allOf` +keyword could look like the following. -setNormalizationHandler(KEYWORD_URI, { - evaluate() { - // Only applicator keywords need to return a value +```TypeScript +import { defineKeyword, evaluateSchema } from "@hyperjump/json-schema-errors"; + +defineKeyword("https://json-schema.org/keyword/allOf", { + evaluate(allOf, instance, context) { + return allOf.map((schemaLocation) => evaluateSchema(schemaLocation, instance, context)); } }); ``` -An error handler turns the normalized results into messages. It's registered -with a URI that identifies the handler. A handler can handle any number of -keywords, so it gets the results of every keyword that applies to a location in -the instance and picks out the ones it handles. +### Error Handlers + +Some keywords need more control over their messages, such as a keyword that +combines its occurrences into one message, keywords that are described together, +or an applicator that makes assertions of their own. These keywords are still +defined with `defineKeyword`, but without `error` or `requirement`. Their +messages come from an error handler instead. -`error` describes keywords that failed. A result's `valid` is `false` if the -keyword failed, `true` if it passed, and `undefined` if the result isn't known. -Its `value` is the keyword's value as compiled by the keyword's -`@hyperjump/json-schema` definition. +An error handler, set with `setErrorHandler`, builds messages from the +`NormalizedOutput` of a schema. A handler can handle any number of keywords, so +it gets the results of every keyword that applies to a location in the instance +and picks out the ones it handles. A result's `valid` is `false` if the keyword +failed, `true` if it passed, and `undefined` if the result isn't known. Its +`value` is the keyword's compiled value. `error` describes keywords that failed +and `success` describes what keywords require. In a negated context, `success` +describes what would make the keywords fail instead. -`success` describes what keywords require. It's used to explain failures caused -by a subschema passing, such as with `not`. In a negated context, it describes -what would make the keyword fail instead. `context.localization.formatRequirement` -picks the success or negated message. A keyword without a `success` handler -can't be described, so messages for keywords like `not` and `oneOf` will be less -specific. +The `startsWith` keyword from the previous section could also be written with an +error handler. ```TypeScript import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { setErrorHandler } from "@hyperjump/json-schema-errors"; +import { defineKeyword, setErrorHandler } from "@hyperjump/json-schema-errors"; import type { ErrorObject } from "@hyperjump/json-schema-errors"; const KEYWORD_URI = "https://example.com/keyword/startsWith"; +defineKeyword(KEYWORD_URI, {}); + setErrorHandler("https://example.com/error-handler/startsWith", { error: (normalizedErrors, instance, context) => { const errors: ErrorObject[] = []; @@ -267,26 +287,6 @@ setErrorHandler("https://example.com/error-handler/startsWith", { }); ``` -Simple applicator keywords that just evaluate subschemas and don't make any -assertions of their own don't need an error handler, only a normalization -handler. Whether a keyword is a simple applicator comes from the -`simpleApplicator` property of its `@hyperjump/json-schema` keyword definition. -The results of its subschemas are flattened into the results of the parent -schema before they're passed to error handlers. -For example, support for the `allOf` keyword could look like the following. - -```TypeScript -import { setNormalizationHandler, evaluateSchema } from "@hyperjump/json-schema-errors"; - -const KEYWORD_URI = "https://json-schema.org/keyword/allOf"; - -setNormalizationHandler(KEYWORD_URI, { - evaluate(allOf, instance, context) { - return allOf.map((schemaLocation) => evaluateSchema(schemaLocation, instance, context)); - } -}); -``` - Applicators that make assertions of their own, like `not` or `anyOf`, need an error handler that describes their subschemas. A keyword result's `outputs` has the normalized output of each subschema. These functions help describe them. diff --git a/src/define-keyword.test.js b/src/define-keyword.test.js new file mode 100644 index 0000000..425e746 --- /dev/null +++ b/src/define-keyword.test.js @@ -0,0 +1,130 @@ +import { beforeAll, describe, expect, test } from "vitest"; +import { addKeyword, defineVocabulary } from "@hyperjump/json-schema/experimental"; +import { registerSchema, validate } from "@hyperjump/json-schema/draft-2020-12"; +import * as Instance from "@hyperjump/json-schema/instance/experimental"; +import * as Browser from "@hyperjump/browser"; +import { addTranslation, defineKeyword, JSE } from "./index.js"; + +/** + * @import { ErrorObject } from "./index.js" + */ + +const dialectUri = "https://example.com/define-keyword/dialect"; +const startsWithUri = "https://example.com/keyword/startsWith"; +const noteUri = "https://example.com/keyword/note"; +const neverUri = "https://example.com/keyword/never"; + +/** @type (errors: ErrorObject[]) => unknown[] */ +const messages = (errors) => errors.map(({ message, alternatives }) => { + return alternatives ? [message, alternatives.map(messages)] : message; +}); + +/** @type (schema: Record, instance: unknown) => Promise */ +const getMessages = async (schema, instance) => { + const schemaUri = `https://example.com/define-keyword/${crypto.randomUUID()}`; + registerSchema({ $schema: dialectUri, ...schema }, schemaUri); + const output = await validate(schemaUri, /** @type any */ (instance), JSE); + return output.valid ? [] : messages(output.errors); +}; + +describe("defineKeyword", () => { + beforeAll(() => { + addKeyword({ + id: startsWithUri, + compile: async (schema) => /** @type string */ (Browser.value(schema)), + interpret: (prefix, instance) => Instance.typeOf(instance) !== "string" + || /** @type string */ (Instance.value(instance)).startsWith(prefix) + }); + addKeyword({ + id: noteUri, + compile: async (schema) => Browser.value(schema), + interpret: () => true + }); + addKeyword({ + id: neverUri, + compile: async (schema) => Browser.value(schema), + interpret: () => false + }); + defineVocabulary("https://example.com/define-keyword/vocab", { + startsWith: startsWithUri, + note: noteUri, + never: neverUri + }); + registerSchema({ + $id: dialectUri, + $schema: "https://json-schema.org/draft/2020-12/schema", + $vocabulary: { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://example.com/define-keyword/vocab": true + }, + $dynamicAnchor: "meta", + $ref: "https://json-schema.org/draft/2020-12/schema" + }); + + addTranslation("en-US", ` +fx-startsWith-message = Expected a string that starts with '{$prefix}' +fx-startsWith-success-message = The value is either not a string or starts with '{$prefix}' +fx-startsWith-negated-message = The value is a string that doesn't start with '{$prefix}' +`); + + defineKeyword(startsWithUri, { + error: (/** @type string */ prefix, localization) => localization.format("fx-startsWith-message", { prefix }), + requirement: (/** @type string */ prefix, localization) => localization.formatRequirement("fx-startsWith", { prefix }) + }); + + defineKeyword(noteUri, { annotation: true }); + }); + + test("error", async () => { + expect(await getMessages({ startsWith: "foo" }, "bar")).to.eql([ + "Expected a string that starts with 'foo'" + ]); + }); + + test("passing", async () => { + expect(await getMessages({ startsWith: "foo" }, "foobar")).to.eql([]); + }); + + test("each occurrence that fails gets a message", async () => { + expect(await getMessages({ allOf: [{ startsWith: "foo" }, { startsWith: "ba" }, { startsWith: "baz" }] }, "bar")).to.eql([ + "Expected a string that starts with 'foo'", + "Expected a string that starts with 'baz'" + ]); + }); + + test("negated requirement", async () => { + expect(await getMessages({ not: { startsWith: "foo" } }, "foobar")).to.eql([ + ["Expected the following to be true", [["The value is a string that doesn't start with 'foo'"]]] + ]); + }); + + test("requirement", async () => { + expect(await getMessages({ not: { not: { startsWith: "foo" } } }, "bar")).to.eql([ + ["Expected the following to be true", [["The value is either not a string or starts with 'foo'"]]] + ]); + }); + + test("annotation", async () => { + // A schema with only annotations allows any value + expect(await getMessages({ oneOf: [{ note: "anything" }, { type: "string" }] }, "foo")).to.eql([ + ["Expected the value to satisfy only one of the following options", [ + ["Any value is allowed"], + ["The value is a string"] + ]] + ]); + }); + + test("redefining without messages removes them", async () => { + defineKeyword(neverUri, { + error: () => "Never passes" + }); + expect(await getMessages({ never: true }, 42)).to.eql([ + "Never passes" + ]); + + defineKeyword(neverUri, {}); + expect(await getMessages({ never: true }, 42)).to.eql([]); + }); +}); diff --git a/src/error-handlers/contains.js b/src/error-handlers/contains.js index 8831527..ae77105 100644 --- a/src/error-handlers/contains.js +++ b/src/error-handlers/contains.js @@ -2,7 +2,7 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import { allTrue, allowsAnyValue, countTrue, describeEach, getPlaceholder, getSiblingKeywordLocation, getSuccesses, getValidity, negate, someTrue } from "../json-schema-errors.js"; /** - * @import { ContainsAst } from "../normalization-handlers/contains.js" + * @import { ContainsAst } from "../keywords/contains.js" * @import { ContainsRange, ErrorHandler, ErrorHandlerContext, ErrorObject, NormalizedOutput } from "../index.d.ts" */ diff --git a/src/error-handlers/pattern.js b/src/error-handlers/pattern.js deleted file mode 100644 index 10cc126..0000000 --- a/src/error-handlers/pattern.js +++ /dev/null @@ -1,39 +0,0 @@ -import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword } from "../json-schema-errors.js"; - -/** - * @import { ErrorHandler, ErrorObject } from "../index.d.ts" - */ - -/** @type ErrorHandler */ -const patternErrorHandler = { - error: (normalizedErrors, instance, context) => { - /** @type ErrorObject[] */ - const errors = []; - - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/pattern"]) { - if (normalizedErrors["https://json-schema.org/keyword/pattern"][schemaLocation].valid !== false) { - continue; - } - - const compiledPattern = /** @type RegExp */ (normalizedErrors["https://json-schema.org/keyword/pattern"][schemaLocation].value); - const pattern = compiledPattern.source; - - errors.push({ - message: context.localization.getPatternErrorMessage(pattern), - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); - } - - return errors; - }, - - success: (normalizedOutput, instance, context) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/pattern", instance, (/** @type RegExp */ pattern) => { - return context.localization.getPatternSuccessMessage(pattern.source); - }); - } -}; - -export default patternErrorHandler; diff --git a/src/index.d.ts b/src/index.d.ts index 366914f..621a5ef 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -20,10 +20,65 @@ export const jsonSchemaErrors: ( ) => Promise; /** - * Sets a normalization handler for a specific keyword URI. Normalization handlers - * process keyword values during schema validation to produce normalized output. + * Adds support for a keyword. The keyword itself needs to be defined with + * `@hyperjump/json-schema`'s `addKeyword`. Every keyword the validator uses + * needs to be defined. Each occurrence of the keyword that fails gets its own + * message from `error`, and `requirement` describes what each occurrence + * requires. Use `setErrorHandler` instead for keywords that need to combine + * occurrences into one message, keywords that are described together, or + * applicators that make assertions of their own. + * + * Defining a keyword again replaces its definition. + * + * @param keywordUri - The keyword's id in its `@hyperjump/json-schema` definition + * @param definition */ -export const setNormalizationHandler: (keywordUri: string, handler: NormalizationHandler) => void; +export const defineKeyword: (keywordUri: string, definition: KeywordDefinition) => void; + +export type KeywordDefinition = { + /** + * Applicators call `evaluateSchema` on each subschema and return an array + * with each result. Other keywords don't need this. + */ + evaluate?(value: Value, instance: JsonNode, context: Context): NormalizedOutput[] | void; + + /** + * Conditional applicators, like `then` and `dependentSchemas`, are simple + * applicators whose subschemas only apply when a condition holds. Whether a + * keyword is a simple applicator comes from its `@hyperjump/json-schema` + * keyword definition. The results of a simple applicator's subschemas are + * flattened into the results of its parent schema, but the results of a + * conditional applicator are left out when describing the parent schema + * because its error handler describes them along with the condition. + */ + conditional?: true; + + /** + * Annotations, such as `title` and `description`, never affect validation. A + * schema that only has annotations allows any value. + */ + annotation?: true; + + /** + * The message for an occurrence of the keyword that failed. + * + * @param value - The keyword's value as compiled by its `@hyperjump/json-schema` definition + * @param localization + * @param instance - The value that failed + */ + error?: (value: Value, localization: Localization, instance: JsonNode) => string; + + /** + * What the keyword requires, or in a negated localization, what would make + * it fail. Use `localization.formatRequirement` to pick the message. Return + * `undefined` if the keyword doesn't require anything. It's used to explain + * failures caused by a subschema passing, such as with `not`. + * + * @param value - The keyword's value as compiled by its `@hyperjump/json-schema` definition + * @param localization + */ + requirement?: (value: Value, localization: Localization) => string | undefined; +}; /** * The standard JSON Schema output format. Supports the "basic", "detailed", and @@ -83,37 +138,6 @@ export type ErrorObject = { schemaLocations: string[]; }; -/** - * Used to convert a specific keyword to the normalized format used by the error - * handlers. - */ -export type NormalizationHandler = { - /** - * For non-applicator keywords, this doesn't need to do anything. Just return void. - * - * For applicator keywords, it should call `evaluateSchema` on each subschema and - * return an array with each result. - */ - evaluate(value: KeywordValue, instance: JsonNode, context: Context): NormalizedOutput[] | void; - - /** - * Conditional applicators, like `then` and `dependentSchemas`, are simple - * applicators whose subschemas only apply when a condition holds. Whether a - * keyword is a simple applicator comes from its `@hyperjump/json-schema` - * keyword definition. The results of a simple applicator's subschemas are - * flattened into the results of its parent schema, but the results of a - * conditional applicator are left out when describing the parent schema - * because its error handler describes them along with the condition. - */ - conditional?: true; - - /** - * Annotations, such as `title` and `description`, never affect validation. A - * schema that only has annotations allows any value. - */ - annotation?: true; -}; - export type EvaluationContext = { ast: AST; errorIndex: ErrorIndex; diff --git a/src/index.js b/src/index.js index 6366b80..d739b61 100644 --- a/src/index.js +++ b/src/index.js @@ -1,64 +1,30 @@ -import { setErrorHandler, setNormalizationHandler } from "./json-schema-errors.js"; +import { defineKeyword, setErrorHandler } from "./json-schema-errors.js"; -// Normalization Handlers -import additionalItemsNormalizationHandler from "./normalization-handlers/draft-04/additionalItems.js"; -import additionalPropertiesNormalizationHandler from "./normalization-handlers/additionalProperties.js"; -import allOfNormalizationHandler from "./normalization-handlers/allOf.js"; -import anyOfNormalizationHandler from "./normalization-handlers/anyOf.js"; -import commentNormalizationHandler from "./normalization-handlers/comment.js"; -import constNormalizationHandler from "./normalization-handlers/const.js"; -import containsNormalizationHandler from "./normalization-handlers/contains.js"; -import containsDraft06NormalizationHandler from "./normalization-handlers/draft-06/contains.js"; -import definitionsNormalizationHandler from "./normalization-handlers/definitions.js"; -import defaultNormalizationHandler from "./normalization-handlers/default.js"; -import dependenciesNormalizationHandler from "./normalization-handlers/draft-04/dependencies.js"; -import dependentRequiredNormalizationHandler from "./normalization-handlers/dependentRequired.js"; -import dependentSchemasNormalizationHandler from "./normalization-handlers/dependentSchemas.js"; -import deprecatedNormalizationHandler from "./normalization-handlers/deprecated.js"; -import descriptionNormalizationHandler from "./normalization-handlers/description.js"; -import dynamicRefNormalizationHandler from "./normalization-handlers/dynamicRef.js"; -import elseNormalizationHandler from "./normalization-handlers/else.js"; -import enumNormalizationHandler from "./normalization-handlers/enum.js"; -import examplesNormalizationHandler from "./normalization-handlers/examples.js"; -import exclusiveMaximumDraft04NormalizationHandler from "./normalization-handlers/draft-04/exclusiveMaximum.js"; -import exclusiveMaximumNormalizationHandler from "./normalization-handlers/exclusiveMaximum.js"; -import exclusiveMinimumDraft04NormalizationHandler from "./normalization-handlers/draft-04/exclusiveMinimum.js"; -import exclusiveMinimumNormalizationHandler from "./normalization-handlers/exclusiveMinimum.js"; -import formatNormalizationHandler from "./normalization-handlers/format.js"; -import ifNormalizationHandler from "./normalization-handlers/if.js"; -import itemsDraft04NormalizationHandler from "./normalization-handlers/draft-04/items.js"; -import itemsNormalizationHandler from "./normalization-handlers/items.js"; -import maximumDraft04NormalizationHandler from "./normalization-handlers/draft-04/maximum.js"; -import maximumNormalizationHandler from "./normalization-handlers/maximum.js"; -import maxContainsNormalizationHandler from "./normalization-handlers/maxContains.js"; -import maxItemsNormalizationHandler from "./normalization-handlers/maxItems.js"; -import maxLengthNormalizationHandler from "./normalization-handlers/maxLength.js"; -import maxPropertiesNormalizationHandler from "./normalization-handlers/maxProperties.js"; -import minimumDraft04NormalizationHandler from "./normalization-handlers/draft-04/minimum.js"; -import minimumNormalizationHandler from "./normalization-handlers/minimum.js"; -import minContainsNormalizationHandler from "./normalization-handlers/minContains.js"; -import minItemsNormalizationHandler from "./normalization-handlers/minItems.js"; -import minLengthNormalizationHandler from "./normalization-handlers/minLength.js"; -import minPropertiesNormalizationHandler from "./normalization-handlers/minProperties.js"; -import multipleOfNormalizationHandler from "./normalization-handlers/multipleOf.js"; -import notNormalizationHandler from "./normalization-handlers/not.js"; -import oneOfNormalizationHandler from "./normalization-handlers/oneOf.js"; -import patternNormalizationHandler from "./normalization-handlers/pattern.js"; -import patternPropertiesNormalizationHandler from "./normalization-handlers/patternProperties.js"; -import prefixItemsNormalizationHandler from "./normalization-handlers/prefixItems.js"; -import propertiesNormalizationHandler from "./normalization-handlers/properties.js"; -import propertyNamesNormalizationHandler from "./normalization-handlers/propertyNames.js"; -import readOnlyNormalizationHandler from "./normalization-handlers/readOnly.js"; -import refNormalizationHandler from "./normalization-handlers/ref.js"; -import requiredNormalizationHandler from "./normalization-handlers/required.js"; -import titleNormalizationHandler from "./normalization-handlers/title.js"; -import thenNormalizationHandler from "./normalization-handlers/then.js"; -import typeNormalizationHandler from "./normalization-handlers/type.js"; -import unevaluatedItemsNormalizationHandler from "./normalization-handlers/unevaluatedItems.js"; -import unevaluatedPropertiesNormalizationHandler from "./normalization-handlers/unevaluatedProperties.js"; -import uniqueItemsNormalizationHandler from "./normalization-handlers/uniqueItems.js"; -import unknownNormalizationHandler from "./normalization-handlers/unknown.js"; -import writeOnlyNormalizationHandler from "./normalization-handlers/writeOnly.js"; +// Keywords +import additionalItemsKeyword from "./keywords/draft-04/additionalItems.js"; +import additionalPropertiesKeyword from "./keywords/additionalProperties.js"; +import allOfKeyword from "./keywords/allOf.js"; +import anyOfKeyword from "./keywords/anyOf.js"; +import containsKeyword from "./keywords/contains.js"; +import containsDraft06Keyword from "./keywords/draft-06/contains.js"; +import dependenciesKeyword from "./keywords/draft-04/dependencies.js"; +import dependentSchemasKeyword from "./keywords/dependentSchemas.js"; +import dynamicRefKeyword from "./keywords/dynamicRef.js"; +import elseKeyword from "./keywords/else.js"; +import ifKeyword from "./keywords/if.js"; +import itemsDraft04Keyword from "./keywords/draft-04/items.js"; +import itemsKeyword from "./keywords/items.js"; +import notKeyword from "./keywords/not.js"; +import oneOfKeyword from "./keywords/oneOf.js"; +import patternKeyword from "./keywords/pattern.js"; +import patternPropertiesKeyword from "./keywords/patternProperties.js"; +import prefixItemsKeyword from "./keywords/prefixItems.js"; +import propertiesKeyword from "./keywords/properties.js"; +import propertyNamesKeyword from "./keywords/propertyNames.js"; +import refKeyword from "./keywords/ref.js"; +import thenKeyword from "./keywords/then.js"; +import unevaluatedItemsKeyword from "./keywords/unevaluatedItems.js"; +import unevaluatedPropertiesKeyword from "./keywords/unevaluatedProperties.js"; // Error Handlers import anyOfErrorHandler from "./error-handlers/anyOf.js"; @@ -83,80 +49,14 @@ import notErrorHandler from "./error-handlers/not.js"; import oneOfErrorHandler from "./error-handlers/oneOf.js"; import prefixItemsErrorHandler from "./error-handlers/prefixItems.js"; import propertiesErrorHandler from "./error-handlers/properties.js"; -import patternErrorHandler from "./error-handlers/pattern.js"; import requiredErrorHandler from "./error-handlers/required.js"; import typeConstEnumErrorHandler from "./error-handlers/typeConstEnum.js"; import uniqueItemsErrorHandler from "./error-handlers/uniqueItems.js"; import unknownErrorHandler from "./error-handlers/unknown.js"; -setNormalizationHandler("https://json-schema.org/keyword/draft-04/additionalItems", additionalItemsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/additionalProperties", additionalPropertiesNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/allOf", allOfNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/anyOf", anyOfNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/comment", commentNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/const", constNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/contains", containsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-06/contains", containsDraft06NormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/default", defaultNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/definitions", definitionsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-04/dependencies", dependenciesNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/dependentRequired", dependentRequiredNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/dependentSchemas", dependentSchemasNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/deprecated", deprecatedNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/description", descriptionNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-2020-12/dynamicRef", dynamicRefNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/else", elseNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/enum", enumNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/examples", examplesNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/format", formatNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-2020-12/format", formatNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-2020-12/format-assertion", formatNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-2019-09/format", formatNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-2019-09/format-assertion", formatNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-07/format", formatNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-06/format", formatNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-04/format", formatNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/if", ifNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-04/items", itemsDraft04NormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/items", itemsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-04/exclusiveMaximum", exclusiveMaximumDraft04NormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/exclusiveMaximum", exclusiveMaximumNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-04/exclusiveMinimum", exclusiveMinimumDraft04NormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/exclusiveMinimum", exclusiveMinimumNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-04/maximum", maximumDraft04NormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/maximum", maximumNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/maxContains", maxContainsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/maxItems", maxItemsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/maxLength", maxLengthNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/maxProperties", maxPropertiesNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/draft-04/minimum", minimumDraft04NormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/minimum", minimumNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/minContains", minContainsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/minItems", minItemsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/minLength", minLengthNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/minProperties", minPropertiesNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/multipleOf", multipleOfNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/not", notNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/oneOf", oneOfNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/pattern", patternNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/patternProperties", patternPropertiesNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/prefixItems", prefixItemsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/properties", propertiesNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/propertyNames", propertyNamesNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/readOnly", readOnlyNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/ref", refNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/required", requiredNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/title", titleNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/then", thenNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/type", typeNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/unevaluatedItems", unevaluatedItemsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/unevaluatedProperties", unevaluatedPropertiesNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/uniqueItems", uniqueItemsNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/unknown", unknownNormalizationHandler); -setNormalizationHandler("https://json-schema.org/keyword/writeOnly", writeOnlyNormalizationHandler); - // Registration order determines message order. Type comes first because it's the -// most fundamental thing to know about a value. +// most fundamental thing to know about a value. Keywords with their own messages +// are defined after the error handlers, so their messages come last. setErrorHandler("https://hyperjump.io/error-handler/typeConstEnum", typeConstEnumErrorHandler); setErrorHandler("https://hyperjump.io/error-handler/anyOf", anyOfErrorHandler); setErrorHandler("https://hyperjump.io/error-handler/boolean-schema", booleanSchemaErrorHandler); @@ -180,14 +80,80 @@ setErrorHandler("https://hyperjump.io/error-handler/not", notErrorHandler); setErrorHandler("https://hyperjump.io/error-handler/oneOf", oneOfErrorHandler); setErrorHandler("https://hyperjump.io/error-handler/prefixItems", prefixItemsErrorHandler); setErrorHandler("https://hyperjump.io/error-handler/properties", propertiesErrorHandler); -setErrorHandler("https://hyperjump.io/error-handler/pattern", patternErrorHandler); setErrorHandler("https://hyperjump.io/error-handler/required", requiredErrorHandler); setErrorHandler("https://hyperjump.io/error-handler/uniqueItems", uniqueItemsErrorHandler); setErrorHandler("https://hyperjump.io/error-handler/unknown", unknownErrorHandler); +defineKeyword("https://json-schema.org/keyword/draft-04/additionalItems", additionalItemsKeyword); +defineKeyword("https://json-schema.org/keyword/additionalProperties", additionalPropertiesKeyword); +defineKeyword("https://json-schema.org/keyword/allOf", allOfKeyword); +defineKeyword("https://json-schema.org/keyword/anyOf", anyOfKeyword); +defineKeyword("https://json-schema.org/keyword/comment", { annotation: true }); +defineKeyword("https://json-schema.org/keyword/const", {}); +defineKeyword("https://json-schema.org/keyword/contains", containsKeyword); +defineKeyword("https://json-schema.org/keyword/draft-06/contains", containsDraft06Keyword); +defineKeyword("https://json-schema.org/keyword/default", { annotation: true }); +defineKeyword("https://json-schema.org/keyword/definitions", { annotation: true }); +defineKeyword("https://json-schema.org/keyword/draft-04/dependencies", dependenciesKeyword); +defineKeyword("https://json-schema.org/keyword/dependentRequired", {}); +defineKeyword("https://json-schema.org/keyword/dependentSchemas", dependentSchemasKeyword); +defineKeyword("https://json-schema.org/keyword/deprecated", { annotation: true }); +defineKeyword("https://json-schema.org/keyword/description", { annotation: true }); +defineKeyword("https://json-schema.org/keyword/draft-2020-12/dynamicRef", dynamicRefKeyword); +defineKeyword("https://json-schema.org/keyword/else", elseKeyword); +defineKeyword("https://json-schema.org/keyword/enum", {}); +defineKeyword("https://json-schema.org/keyword/examples", { annotation: true }); +defineKeyword("https://json-schema.org/keyword/format", {}); +defineKeyword("https://json-schema.org/keyword/draft-2020-12/format", {}); +defineKeyword("https://json-schema.org/keyword/draft-2020-12/format-assertion", {}); +defineKeyword("https://json-schema.org/keyword/draft-2019-09/format", {}); +defineKeyword("https://json-schema.org/keyword/draft-2019-09/format-assertion", {}); +defineKeyword("https://json-schema.org/keyword/draft-07/format", {}); +defineKeyword("https://json-schema.org/keyword/draft-06/format", {}); +defineKeyword("https://json-schema.org/keyword/draft-04/format", {}); +defineKeyword("https://json-schema.org/keyword/if", ifKeyword); +defineKeyword("https://json-schema.org/keyword/draft-04/items", itemsDraft04Keyword); +defineKeyword("https://json-schema.org/keyword/items", itemsKeyword); +defineKeyword("https://json-schema.org/keyword/draft-04/exclusiveMaximum", {}); +defineKeyword("https://json-schema.org/keyword/exclusiveMaximum", {}); +defineKeyword("https://json-schema.org/keyword/draft-04/exclusiveMinimum", {}); +defineKeyword("https://json-schema.org/keyword/exclusiveMinimum", {}); +defineKeyword("https://json-schema.org/keyword/draft-04/maximum", {}); +defineKeyword("https://json-schema.org/keyword/maximum", {}); +defineKeyword("https://json-schema.org/keyword/maxContains", {}); +defineKeyword("https://json-schema.org/keyword/maxItems", {}); +defineKeyword("https://json-schema.org/keyword/maxLength", {}); +defineKeyword("https://json-schema.org/keyword/maxProperties", {}); +defineKeyword("https://json-schema.org/keyword/draft-04/minimum", {}); +defineKeyword("https://json-schema.org/keyword/minimum", {}); +defineKeyword("https://json-schema.org/keyword/minContains", {}); +defineKeyword("https://json-schema.org/keyword/minItems", {}); +defineKeyword("https://json-schema.org/keyword/minLength", {}); +defineKeyword("https://json-schema.org/keyword/minProperties", {}); +defineKeyword("https://json-schema.org/keyword/multipleOf", {}); +defineKeyword("https://json-schema.org/keyword/not", notKeyword); +defineKeyword("https://json-schema.org/keyword/oneOf", oneOfKeyword); +defineKeyword("https://json-schema.org/keyword/pattern", patternKeyword); +defineKeyword("https://json-schema.org/keyword/patternProperties", patternPropertiesKeyword); +defineKeyword("https://json-schema.org/keyword/prefixItems", prefixItemsKeyword); +defineKeyword("https://json-schema.org/keyword/properties", propertiesKeyword); +defineKeyword("https://json-schema.org/keyword/propertyNames", propertyNamesKeyword); +defineKeyword("https://json-schema.org/keyword/readOnly", { annotation: true }); +defineKeyword("https://json-schema.org/keyword/ref", refKeyword); +defineKeyword("https://json-schema.org/keyword/required", {}); +defineKeyword("https://json-schema.org/keyword/title", { annotation: true }); +defineKeyword("https://json-schema.org/keyword/then", thenKeyword); +defineKeyword("https://json-schema.org/keyword/type", {}); +defineKeyword("https://json-schema.org/keyword/unevaluatedItems", unevaluatedItemsKeyword); +defineKeyword("https://json-schema.org/keyword/unevaluatedProperties", unevaluatedPropertiesKeyword); +defineKeyword("https://json-schema.org/keyword/uniqueItems", {}); +defineKeyword("https://json-schema.org/keyword/unknown", { annotation: true }); +defineKeyword("https://json-schema.org/keyword/writeOnly", { annotation: true }); + export { allTrue, countTrue, + defineKeyword, describeConditional, describeScope, evaluateSchema, @@ -200,7 +166,6 @@ export { negate, removeErrorHandler, setErrorHandler, - setNormalizationHandler, someTrue } from "./json-schema-errors.js"; export { addTranslation } from "./localization.js"; diff --git a/src/json-schema-errors.js b/src/json-schema-errors.js index 1e1736e..b7f3a18 100644 --- a/src/json-schema-errors.js +++ b/src/json-schema-errors.js @@ -32,8 +32,8 @@ export const jsonSchemaErrors = async (errorOutput, schemaUri, instance, options return limitMessages(getErrors(normalizedErrors, rootInstance, context), context); }; -/** @type Record */ -const normalizationHandlers = {}; +/** @type Record> */ +const keywordDefinitions = {}; /** @type (schemaLocation: string, ast: AST) => boolean */ export const allowsAnyValue = (schemaLocation, ast) => { @@ -42,12 +42,7 @@ export const allowsAnyValue = (schemaLocation, ast) => { return schemaNode; } - return schemaNode.every(([keywordUri]) => normalizationHandlers[toAbsoluteIri(keywordUri)]?.annotation); -}; - -/** @type API.setNormalizationHandler */ -export const setNormalizationHandler = (schemaUri, handler) => { - normalizationHandlers[schemaUri] = handler; + return schemaNode.every(([keywordUri]) => keywordDefinitions[toAbsoluteIri(keywordUri)]?.annotation); }; /** @type (outputUnit: API.OutputUnit, schema: Browser, errorIndex?: API.ErrorIndex) => Promise */ @@ -137,10 +132,10 @@ export const evaluateSchema = (schemaLocation, instance, context) => { const [keywordUri, keywordLocation, keywordValue] = node; const normalizedKeywordUri = toAbsoluteIri(keywordUri); - if (!(normalizedKeywordUri in normalizationHandlers)) { - throw Error(`Encountered unsupported keyword ${keywordUri}. Use the 'setNormalizationHandler' function to add support for this keyword.`); + if (!(normalizedKeywordUri in keywordDefinitions)) { + throw Error(`Encountered unsupported keyword ${keywordUri}. Use the 'defineKeyword' function to add support for this keyword.`); } - const keyword = normalizationHandlers[normalizedKeywordUri]; + const keyword = keywordDefinitions[normalizedKeywordUri]; const validationKeyword = getKeyword(keywordUri); @@ -161,7 +156,7 @@ export const evaluateSchema = (schemaLocation, instance, context) => { plugin.beforeKeyword?.(node, instance, keywordContext, context, validationKeyword); } - const keywordOutput = keyword.evaluate(keywordValue, instance, keywordContext); + const keywordOutput = keyword.evaluate?.(keywordValue, instance, keywordContext); const isReported = context.errorIndex[keywordLocation]?.[keywordInstanceLocation] !== undefined; if (validationKeyword.simpleApplicator && !isReported && keywordOutput?.some((suboutput) => getValidity(suboutput) === false)) { @@ -398,6 +393,44 @@ const relocate = (errorObject, from, to) => { /** @type Record */ const errorHandlers = {}; +/** @type API.defineKeyword */ +export const defineKeyword = (keywordUri, definition) => { + keywordDefinitions[keywordUri] = definition; + + const { error, requirement } = definition; + if (!error && !requirement) { + removeErrorHandler(keywordUri); + return; + } + + setErrorHandler(keywordUri, { + // Each occurrence of the keyword that failed gets its own message + error: error && ((normalizedErrors, instance, context) => { + /** @type API.ErrorObject[] */ + const errors = []; + + for (const schemaLocation in normalizedErrors[keywordUri]) { + const { valid, value } = normalizedErrors[keywordUri][schemaLocation]; + if (valid === false) { + errors.push({ + message: error(/** @type any */ (value), context.localization, instance), + instanceLocation: Instance.uri(instance), + schemaLocations: [schemaLocation] + }); + } + } + + return errors; + }), + + success: requirement && ((normalizedOutput, instance, context) => { + return describeKeyword(normalizedOutput, keywordUri, instance, (value) => { + return requirement(/** @type any */ (value), context.localization); + }); + }) + }); +}; + /** @type API.setErrorHandler */ export const setErrorHandler = (errorHandlerUri, errorHandler) => { errorHandlers[errorHandlerUri] = errorHandler; @@ -474,7 +507,7 @@ const isApplied = () => true; * * @type (keywordUri: string) => boolean */ -const isRequired = (keywordUri) => !normalizationHandlers[toAbsoluteIri(keywordUri)]?.conditional; +const isRequired = (keywordUri) => !keywordDefinitions[toAbsoluteIri(keywordUri)]?.conditional; /** @type API.getSuccesses */ export const getSuccesses = (subschema, rootInstance, context) => { diff --git a/src/normalization-handlers/additionalProperties.js b/src/keywords/additionalProperties.js similarity index 75% rename from src/normalization-handlers/additionalProperties.js rename to src/keywords/additionalProperties.js index 9e9dad5..7d6c589 100644 --- a/src/normalization-handlers/additionalProperties.js +++ b/src/keywords/additionalProperties.js @@ -2,11 +2,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler<[RegExp, string]> */ -const additionalPropertiesNormalizationHandler = { +/** @type KeywordDefinition<[RegExp, string]> */ +const additionalPropertiesKeyword = { evaluate([isDefinedProperty, additionalProperties], instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -28,4 +28,4 @@ const additionalPropertiesNormalizationHandler = { } }; -export default additionalPropertiesNormalizationHandler; +export default additionalPropertiesKeyword; diff --git a/src/normalization-handlers/allOf.js b/src/keywords/allOf.js similarity index 61% rename from src/normalization-handlers/allOf.js rename to src/keywords/allOf.js index 6ea83ab..ba534fe 100644 --- a/src/normalization-handlers/allOf.js +++ b/src/keywords/allOf.js @@ -1,11 +1,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler */ -const allOfNormalizationHandler = { +/** @type KeywordDefinition */ +const allOfKeyword = { evaluate(allOf, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -18,4 +18,4 @@ const allOfNormalizationHandler = { } }; -export default allOfNormalizationHandler; +export default allOfKeyword; diff --git a/src/normalization-handlers/anyOf.js b/src/keywords/anyOf.js similarity index 61% rename from src/normalization-handlers/anyOf.js rename to src/keywords/anyOf.js index cb2c70e..6943ffd 100644 --- a/src/normalization-handlers/anyOf.js +++ b/src/keywords/anyOf.js @@ -1,11 +1,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler */ -const anyOfNormalizationHandler = { +/** @type KeywordDefinition */ +const anyOfKeyword = { evaluate(anyOf, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -18,4 +18,4 @@ const anyOfNormalizationHandler = { } }; -export default anyOfNormalizationHandler; +export default anyOfKeyword; diff --git a/src/normalization-handlers/contains.js b/src/keywords/contains.js similarity index 74% rename from src/normalization-handlers/contains.js rename to src/keywords/contains.js index 27e08a9..c0065fb 100644 --- a/src/normalization-handlers/contains.js +++ b/src/keywords/contains.js @@ -2,7 +2,7 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import { evaluateSchema } from "../json-schema-errors.js"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ /** @@ -13,8 +13,8 @@ import { evaluateSchema } from "../json-schema-errors.js"; * }} ContainsAst */ -/** @type NormalizationHandler */ -const containsNormalizationHandler = { +/** @type KeywordDefinition */ +const containsKeyword = { evaluate({ contains }, instance, context) { /** @type NormalizedOutput[] */ const output = []; @@ -31,4 +31,4 @@ const containsNormalizationHandler = { } }; -export default containsNormalizationHandler; +export default containsKeyword; diff --git a/src/normalization-handlers/dependentSchemas.js b/src/keywords/dependentSchemas.js similarity index 74% rename from src/normalization-handlers/dependentSchemas.js rename to src/keywords/dependentSchemas.js index 8825645..0f55ae9 100644 --- a/src/normalization-handlers/dependentSchemas.js +++ b/src/keywords/dependentSchemas.js @@ -2,11 +2,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { NormalizationHandler, NormalizedOutput} from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput} from "../index.d.ts" */ -/** @type NormalizationHandler<[string, string][]> */ -const dependentSchemaNormalizationHandler = { +/** @type KeywordDefinition<[string, string][]> */ +const dependentSchemaKeyword = { evaluate(dependentSchemas, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -27,4 +27,4 @@ const dependentSchemaNormalizationHandler = { conditional: true }; -export default dependentSchemaNormalizationHandler; +export default dependentSchemaKeyword; diff --git a/src/normalization-handlers/draft-04/additionalItems.js b/src/keywords/draft-04/additionalItems.js similarity index 71% rename from src/normalization-handlers/draft-04/additionalItems.js rename to src/keywords/draft-04/additionalItems.js index c9d1497..965c26b 100644 --- a/src/normalization-handlers/draft-04/additionalItems.js +++ b/src/keywords/draft-04/additionalItems.js @@ -3,11 +3,11 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import * as Pact from "@hyperjump/pact"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../../index.d.ts" */ -/** @type NormalizationHandler<[number, string]> */ -const additionalItemsNormalizationHandler = { +/** @type KeywordDefinition<[number, string]> */ +const additionalItemsKeyword = { evaluate([numberOfItems, additionalItems], instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -24,4 +24,4 @@ const additionalItemsNormalizationHandler = { } }; -export default additionalItemsNormalizationHandler; +export default additionalItemsKeyword; diff --git a/src/normalization-handlers/draft-04/dependencies.js b/src/keywords/draft-04/dependencies.js similarity index 72% rename from src/normalization-handlers/draft-04/dependencies.js rename to src/keywords/draft-04/dependencies.js index e2cf5c9..d32eb80 100644 --- a/src/normalization-handlers/draft-04/dependencies.js +++ b/src/keywords/draft-04/dependencies.js @@ -2,11 +2,11 @@ import { evaluateSchema } from "../../json-schema-errors.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../../index.d.ts" */ -/** @type NormalizationHandler<[string, string | string[]][]> */ -const dependenciesNormalizationHandler = { +/** @type KeywordDefinition<[string, string | string[]][]> */ +const dependenciesKeyword = { evaluate(dependencies, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -27,4 +27,4 @@ const dependenciesNormalizationHandler = { } }; -export default dependenciesNormalizationHandler; +export default dependenciesKeyword; diff --git a/src/normalization-handlers/draft-04/items.js b/src/keywords/draft-04/items.js similarity index 77% rename from src/normalization-handlers/draft-04/items.js rename to src/keywords/draft-04/items.js index bcc1101..574133a 100644 --- a/src/normalization-handlers/draft-04/items.js +++ b/src/keywords/draft-04/items.js @@ -2,11 +2,11 @@ import { evaluateSchema } from "../../json-schema-errors.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../../index.d.ts" */ -/** @type NormalizationHandler */ -const itemsDraft04NormalizationHandler = { +/** @type KeywordDefinition */ +const itemsDraft04Keyword = { evaluate(items, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -32,4 +32,4 @@ const itemsDraft04NormalizationHandler = { } }; -export default itemsDraft04NormalizationHandler; +export default itemsDraft04Keyword; diff --git a/src/normalization-handlers/draft-06/contains.js b/src/keywords/draft-06/contains.js similarity index 68% rename from src/normalization-handlers/draft-06/contains.js rename to src/keywords/draft-06/contains.js index 60d53f0..1943f1d 100644 --- a/src/normalization-handlers/draft-06/contains.js +++ b/src/keywords/draft-06/contains.js @@ -2,11 +2,11 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import { evaluateSchema } from "../../json-schema-errors.js"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../../index.d.ts" */ -/** @type NormalizationHandler */ -const containsDraft06NormalizationHandler = { +/** @type KeywordDefinition */ +const containsDraft06Keyword = { evaluate(contains, instance, context) { /** @type NormalizedOutput[] */ const output = []; @@ -23,4 +23,4 @@ const containsDraft06NormalizationHandler = { } }; -export default containsDraft06NormalizationHandler; +export default containsDraft06Keyword; diff --git a/src/normalization-handlers/dynamicRef.js b/src/keywords/dynamicRef.js similarity index 71% rename from src/normalization-handlers/dynamicRef.js rename to src/keywords/dynamicRef.js index bfb7ea3..cff8346 100644 --- a/src/normalization-handlers/dynamicRef.js +++ b/src/keywords/dynamicRef.js @@ -1,7 +1,7 @@ import { evaluateSchema } from "../json-schema-errors.js"; /** - * @import { EvaluationContext, NormalizationHandler } from "../index.d.ts" + * @import { EvaluationContext, KeywordDefinition } from "../index.d.ts" */ /** @@ -10,8 +10,8 @@ import { evaluateSchema } from "../json-schema-errors.js"; * } & EvaluationContext} DynamicContext */ -/** @type NormalizationHandler */ -const dynamicRefNormalizationHandler = { +/** @type KeywordDefinition */ +const dynamicRefKeyword = { evaluate([id, fragment, ref], instance, context) { if (fragment in context.ast.metaData[id].dynamicAnchors) { context.dynamicAnchors = { ...context.ast.metaData[id].dynamicAnchors, ...context.dynamicAnchors }; @@ -22,4 +22,4 @@ const dynamicRefNormalizationHandler = { } }; -export default dynamicRefNormalizationHandler; +export default dynamicRefKeyword; diff --git a/src/normalization-handlers/else.js b/src/keywords/else.js similarity index 53% rename from src/normalization-handlers/else.js rename to src/keywords/else.js index 3376c49..f86b5bc 100644 --- a/src/normalization-handlers/else.js +++ b/src/keywords/else.js @@ -1,15 +1,15 @@ import { evaluateSchema } from "../json-schema-errors.js"; /** - * @import { NormalizationHandler } from "../index.d.ts" + * @import { KeywordDefinition } from "../index.d.ts" */ -/** @type NormalizationHandler<[string, string]> */ -const elseNormalizationHandler = { +/** @type KeywordDefinition<[string, string]> */ +const elseKeyword = { evaluate([, elseLocation], instance, context) { return [evaluateSchema(elseLocation, instance, context)]; }, conditional: true }; -export default elseNormalizationHandler; +export default elseKeyword; diff --git a/src/normalization-handlers/if.js b/src/keywords/if.js similarity index 51% rename from src/normalization-handlers/if.js rename to src/keywords/if.js index cd5a561..c896844 100644 --- a/src/normalization-handlers/if.js +++ b/src/keywords/if.js @@ -1,14 +1,14 @@ import { evaluateSchema } from "../json-schema-errors.js"; /** - * @import { NormalizationHandler } from "../index.d.ts" + * @import { KeywordDefinition } from "../index.d.ts" */ -/** @type NormalizationHandler */ -const ifNormalizationHandler = { +/** @type KeywordDefinition */ +const ifKeyword = { evaluate(ifLocation, instance, context) { return [evaluateSchema(ifLocation, instance, context)]; } }; -export default ifNormalizationHandler; +export default ifKeyword; diff --git a/src/normalization-handlers/items.js b/src/keywords/items.js similarity index 73% rename from src/normalization-handlers/items.js rename to src/keywords/items.js index 1ea6e27..f9dea31 100644 --- a/src/normalization-handlers/items.js +++ b/src/keywords/items.js @@ -3,11 +3,11 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import * as Pact from "@hyperjump/pact"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler<[number, string]> */ -const itemsNormalizationHandler = { +/** @type KeywordDefinition<[number, string]> */ +const itemsKeyword = { evaluate([numberOfPrefixItems, items], instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -24,4 +24,4 @@ const itemsNormalizationHandler = { } }; -export default itemsNormalizationHandler; +export default itemsKeyword; diff --git a/src/keywords/not.js b/src/keywords/not.js new file mode 100644 index 0000000..4c3803d --- /dev/null +++ b/src/keywords/not.js @@ -0,0 +1,14 @@ +import { evaluateSchema } from "../json-schema-errors.js"; + +/** + * @import { KeywordDefinition } from "../index.d.ts" + */ + +/** @type KeywordDefinition */ +const notKeyword = { + evaluate(not, instance, context) { + return [evaluateSchema(not, instance, context)]; + } +}; + +export default notKeyword; diff --git a/src/normalization-handlers/oneOf.js b/src/keywords/oneOf.js similarity index 61% rename from src/normalization-handlers/oneOf.js rename to src/keywords/oneOf.js index d1a2244..3e3e291 100644 --- a/src/normalization-handlers/oneOf.js +++ b/src/keywords/oneOf.js @@ -1,11 +1,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler */ -const oneOfNormalizationHandler = { +/** @type KeywordDefinition */ +const oneOfKeyword = { evaluate(oneOf, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -18,4 +18,4 @@ const oneOfNormalizationHandler = { } }; -export default oneOfNormalizationHandler; +export default oneOfKeyword; diff --git a/src/keywords/pattern.js b/src/keywords/pattern.js new file mode 100644 index 0000000..7cc7286 --- /dev/null +++ b/src/keywords/pattern.js @@ -0,0 +1,11 @@ +/** + * @import { KeywordDefinition } from "../index.d.ts" + */ + +/** @type KeywordDefinition */ +const patternKeyword = { + error: (pattern, localization) => localization.getPatternErrorMessage(pattern.source), + requirement: (pattern, localization) => localization.getPatternSuccessMessage(pattern.source) +}; + +export default patternKeyword; diff --git a/src/normalization-handlers/patternProperties.js b/src/keywords/patternProperties.js similarity index 76% rename from src/normalization-handlers/patternProperties.js rename to src/keywords/patternProperties.js index e1fe699..5811582 100644 --- a/src/normalization-handlers/patternProperties.js +++ b/src/keywords/patternProperties.js @@ -2,11 +2,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler<[RegExp, string][]> */ -const patternPropertiesNormalizationHandler = { +/** @type KeywordDefinition<[RegExp, string][]> */ +const patternPropertiesKeyword = { evaluate(patternProperties, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -28,4 +28,4 @@ const patternPropertiesNormalizationHandler = { } }; -export default patternPropertiesNormalizationHandler; +export default patternPropertiesKeyword; diff --git a/src/normalization-handlers/prefixItems.js b/src/keywords/prefixItems.js similarity index 74% rename from src/normalization-handlers/prefixItems.js rename to src/keywords/prefixItems.js index 8641dde..0c51a87 100644 --- a/src/normalization-handlers/prefixItems.js +++ b/src/keywords/prefixItems.js @@ -2,11 +2,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler */ -const prefixItemsNormalizationHandler = { +/** @type KeywordDefinition */ +const prefixItemsKeyword = { evaluate(prefixItems, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -26,4 +26,4 @@ const prefixItemsNormalizationHandler = { } }; -export default prefixItemsNormalizationHandler; +export default prefixItemsKeyword; diff --git a/src/normalization-handlers/properties.js b/src/keywords/properties.js similarity index 74% rename from src/normalization-handlers/properties.js rename to src/keywords/properties.js index e6dc7d2..7b56f11 100644 --- a/src/normalization-handlers/properties.js +++ b/src/keywords/properties.js @@ -2,11 +2,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler> */ -const propertiesNormalizationHandler = { +/** @type KeywordDefinition> */ +const propertiesKeyword = { evaluate(properties, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -27,4 +27,4 @@ const propertiesNormalizationHandler = { } }; -export default propertiesNormalizationHandler; +export default propertiesKeyword; diff --git a/src/normalization-handlers/propertyNames.js b/src/keywords/propertyNames.js similarity index 70% rename from src/normalization-handlers/propertyNames.js rename to src/keywords/propertyNames.js index 1e3a473..ba52fb9 100644 --- a/src/normalization-handlers/propertyNames.js +++ b/src/keywords/propertyNames.js @@ -2,11 +2,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler */ -const propertyNamesNormalizationHandler = { +/** @type KeywordDefinition */ +const propertyNamesKeyword = { evaluate(propertyNames, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -23,4 +23,4 @@ const propertyNamesNormalizationHandler = { } }; -export default propertyNamesNormalizationHandler; +export default propertyNamesKeyword; diff --git a/src/keywords/ref.js b/src/keywords/ref.js new file mode 100644 index 0000000..e094073 --- /dev/null +++ b/src/keywords/ref.js @@ -0,0 +1,14 @@ +import { evaluateSchema } from "../json-schema-errors.js"; + +/** + * @import { KeywordDefinition } from "../index.d.ts" + */ + +/** @type KeywordDefinition */ +const refKeyword = { + evaluate(ref, instance, context) { + return [evaluateSchema(ref, instance, context)]; + } +}; + +export default refKeyword; diff --git a/src/normalization-handlers/then.js b/src/keywords/then.js similarity index 51% rename from src/normalization-handlers/then.js rename to src/keywords/then.js index 1c7c468..2261f91 100644 --- a/src/normalization-handlers/then.js +++ b/src/keywords/then.js @@ -1,15 +1,15 @@ import { evaluateSchema } from "../json-schema-errors.js"; /** - * @import { NormalizationHandler } from "../index.d.ts" + * @import { KeywordDefinition } from "../index.d.ts" */ -/** @type NormalizationHandler<[string, string]> */ -const thenNormalizationHandler = { +/** @type KeywordDefinition<[string, string]> */ +const thenKeyword = { evaluate([, then], instance, context) { return [evaluateSchema(then, instance, context)]; }, conditional: true }; -export default thenNormalizationHandler; +export default thenKeyword; diff --git a/src/normalization-handlers/unevaluatedItems.js b/src/keywords/unevaluatedItems.js similarity index 69% rename from src/normalization-handlers/unevaluatedItems.js rename to src/keywords/unevaluatedItems.js index ae177be..8a02506 100644 --- a/src/normalization-handlers/unevaluatedItems.js +++ b/src/keywords/unevaluatedItems.js @@ -2,11 +2,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler */ -const unevaluatedItemsNormalizationHandler = { +/** @type KeywordDefinition */ +const unevaluatedItemsKeyword = { evaluate(unevaluatedItems, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -23,4 +23,4 @@ const unevaluatedItemsNormalizationHandler = { } }; -export default unevaluatedItemsNormalizationHandler; +export default unevaluatedItemsKeyword; diff --git a/src/normalization-handlers/unevaluatedProperties.js b/src/keywords/unevaluatedProperties.js similarity index 69% rename from src/normalization-handlers/unevaluatedProperties.js rename to src/keywords/unevaluatedProperties.js index e749876..0f3c32c 100644 --- a/src/normalization-handlers/unevaluatedProperties.js +++ b/src/keywords/unevaluatedProperties.js @@ -2,11 +2,11 @@ import { evaluateSchema } from "../json-schema-errors.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; /** - * @import { NormalizationHandler, NormalizedOutput } from "../index.d.ts" + * @import { KeywordDefinition, NormalizedOutput } from "../index.d.ts" */ -/** @type NormalizationHandler */ -const unevaluatedPropertiesNormalizationHandler = { +/** @type KeywordDefinition */ +const unevaluatedPropertiesKeyword = { evaluate(unevaluatedProperties, instance, context) { /** @type NormalizedOutput[] */ const outputs = []; @@ -23,4 +23,4 @@ const unevaluatedPropertiesNormalizationHandler = { } }; -export default unevaluatedPropertiesNormalizationHandler; +export default unevaluatedPropertiesKeyword; diff --git a/src/normalization-handlers/comment.js b/src/normalization-handlers/comment.js deleted file mode 100644 index df577cb..0000000 --- a/src/normalization-handlers/comment.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const commentNormalizationHandler = { - evaluate() { - }, - annotation: true -}; - -export default commentNormalizationHandler; diff --git a/src/normalization-handlers/const.js b/src/normalization-handlers/const.js deleted file mode 100644 index 3486c02..0000000 --- a/src/normalization-handlers/const.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const constNormalizationHandler = { - evaluate() { - } -}; - -export default constNormalizationHandler; diff --git a/src/normalization-handlers/default.js b/src/normalization-handlers/default.js deleted file mode 100644 index 961a550..0000000 --- a/src/normalization-handlers/default.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const defaultNormalizationHandler = { - evaluate() { - }, - annotation: true -}; - -export default defaultNormalizationHandler; diff --git a/src/normalization-handlers/definitions.js b/src/normalization-handlers/definitions.js deleted file mode 100644 index 7e03914..0000000 --- a/src/normalization-handlers/definitions.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const definitionsNormalizationHandler = { - evaluate() { - }, - annotation: true -}; - -export default definitionsNormalizationHandler; diff --git a/src/normalization-handlers/dependentRequired.js b/src/normalization-handlers/dependentRequired.js deleted file mode 100644 index fa1d0da..0000000 --- a/src/normalization-handlers/dependentRequired.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const dependentRequiredNormalizationHandler = { - evaluate() { - } -}; - -export default dependentRequiredNormalizationHandler; diff --git a/src/normalization-handlers/deprecated.js b/src/normalization-handlers/deprecated.js deleted file mode 100644 index 2a174ff..0000000 --- a/src/normalization-handlers/deprecated.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const deprecatedNormalizationHandler = { - evaluate() { - }, - annotation: true -}; - -export default deprecatedNormalizationHandler; diff --git a/src/normalization-handlers/description.js b/src/normalization-handlers/description.js deleted file mode 100644 index b068d7a..0000000 --- a/src/normalization-handlers/description.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const descriptionNormalizationHandler = { - evaluate() { - }, - annotation: true -}; - -export default descriptionNormalizationHandler; diff --git a/src/normalization-handlers/draft-04/exclusiveMaximum.js b/src/normalization-handlers/draft-04/exclusiveMaximum.js deleted file mode 100644 index 9bdffaf..0000000 --- a/src/normalization-handlers/draft-04/exclusiveMaximum.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../../index.d.ts" - */ - -/** @type NormalizationHandler */ -const exclusiveMaximumNormalizationHandler = { - evaluate() { - } -}; - -export default exclusiveMaximumNormalizationHandler; diff --git a/src/normalization-handlers/draft-04/exclusiveMinimum.js b/src/normalization-handlers/draft-04/exclusiveMinimum.js deleted file mode 100644 index 70fa029..0000000 --- a/src/normalization-handlers/draft-04/exclusiveMinimum.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../../index.d.ts" - */ - -/** @type NormalizationHandler */ -const exclusiveMinimumNormalizationHandler = { - evaluate() { - } -}; - -export default exclusiveMinimumNormalizationHandler; diff --git a/src/normalization-handlers/draft-04/maximum.js b/src/normalization-handlers/draft-04/maximum.js deleted file mode 100644 index 1e9b441..0000000 --- a/src/normalization-handlers/draft-04/maximum.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../../index.d.ts" - */ - -/** @type NormalizationHandler */ -const maximumNormalizationHandler = { - evaluate() { - } -}; - -export default maximumNormalizationHandler; diff --git a/src/normalization-handlers/draft-04/minimum.js b/src/normalization-handlers/draft-04/minimum.js deleted file mode 100644 index ce09e84..0000000 --- a/src/normalization-handlers/draft-04/minimum.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../../index.d.ts" - */ - -/** @type NormalizationHandler */ -const minimumNormalizationHandler = { - evaluate() { - } -}; - -export default minimumNormalizationHandler; diff --git a/src/normalization-handlers/enum.js b/src/normalization-handlers/enum.js deleted file mode 100644 index 156ed07..0000000 --- a/src/normalization-handlers/enum.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const enumNormalizationHandler = { - evaluate() { - } -}; - -export default enumNormalizationHandler; diff --git a/src/normalization-handlers/examples.js b/src/normalization-handlers/examples.js deleted file mode 100644 index 85e8eef..0000000 --- a/src/normalization-handlers/examples.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const examplesNormalizationHandler = { - evaluate() { - }, - annotation: true -}; - -export default examplesNormalizationHandler; diff --git a/src/normalization-handlers/exclusiveMaximum.js b/src/normalization-handlers/exclusiveMaximum.js deleted file mode 100644 index 45b677f..0000000 --- a/src/normalization-handlers/exclusiveMaximum.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const exclusiveMaximumNormalizationHandler = { - evaluate() { - } -}; - -export default exclusiveMaximumNormalizationHandler; diff --git a/src/normalization-handlers/exclusiveMinimum.js b/src/normalization-handlers/exclusiveMinimum.js deleted file mode 100644 index b822de9..0000000 --- a/src/normalization-handlers/exclusiveMinimum.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const exclusiveMinimumNormalizationHandler = { - evaluate() { - } -}; - -export default exclusiveMinimumNormalizationHandler; diff --git a/src/normalization-handlers/format.js b/src/normalization-handlers/format.js deleted file mode 100644 index fa6e872..0000000 --- a/src/normalization-handlers/format.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const formatNormalizationHandler = { - evaluate() { - } -}; - -export default formatNormalizationHandler; diff --git a/src/normalization-handlers/maxContains.js b/src/normalization-handlers/maxContains.js deleted file mode 100644 index 7a2c0df..0000000 --- a/src/normalization-handlers/maxContains.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const maxContainsNormalizationHandler = { - evaluate() { - } -}; - -export default maxContainsNormalizationHandler; diff --git a/src/normalization-handlers/maxItems.js b/src/normalization-handlers/maxItems.js deleted file mode 100644 index 3804054..0000000 --- a/src/normalization-handlers/maxItems.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const maxItemsNormalizationHandler = { - evaluate() { - } -}; - -export default maxItemsNormalizationHandler; diff --git a/src/normalization-handlers/maxLength.js b/src/normalization-handlers/maxLength.js deleted file mode 100644 index 1029382..0000000 --- a/src/normalization-handlers/maxLength.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const maxLengthNormalizationHandler = { - evaluate() { - } -}; - -export default maxLengthNormalizationHandler; diff --git a/src/normalization-handlers/maxProperties.js b/src/normalization-handlers/maxProperties.js deleted file mode 100644 index ceb0e20..0000000 --- a/src/normalization-handlers/maxProperties.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const maxPropertiesNormalizationHandler = { - evaluate() { - } -}; - -export default maxPropertiesNormalizationHandler; diff --git a/src/normalization-handlers/maximum.js b/src/normalization-handlers/maximum.js deleted file mode 100644 index 0d81dec..0000000 --- a/src/normalization-handlers/maximum.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const maximumNormalizationHandler = { - evaluate() { - } -}; - -export default maximumNormalizationHandler; diff --git a/src/normalization-handlers/minContains.js b/src/normalization-handlers/minContains.js deleted file mode 100644 index 1c3229a..0000000 --- a/src/normalization-handlers/minContains.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const minContainsNormalizationHandler = { - evaluate() { - } -}; - -export default minContainsNormalizationHandler; diff --git a/src/normalization-handlers/minItems.js b/src/normalization-handlers/minItems.js deleted file mode 100644 index 36b7ab1..0000000 --- a/src/normalization-handlers/minItems.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const minItemsNormalizationHandler = { - evaluate() { - } -}; - -export default minItemsNormalizationHandler; diff --git a/src/normalization-handlers/minLength.js b/src/normalization-handlers/minLength.js deleted file mode 100644 index 2d0b5f0..0000000 --- a/src/normalization-handlers/minLength.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const minLengthNormalizationHandler = { - evaluate() { - } -}; - -export default minLengthNormalizationHandler; diff --git a/src/normalization-handlers/minProperties.js b/src/normalization-handlers/minProperties.js deleted file mode 100644 index 6a5b008..0000000 --- a/src/normalization-handlers/minProperties.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const minPropertiesNormalizationHandler = { - evaluate() { - } -}; - -export default minPropertiesNormalizationHandler; diff --git a/src/normalization-handlers/minimum.js b/src/normalization-handlers/minimum.js deleted file mode 100644 index c04a638..0000000 --- a/src/normalization-handlers/minimum.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const minimumNormalizationHandler = { - evaluate() { - } -}; - -export default minimumNormalizationHandler; diff --git a/src/normalization-handlers/multipleOf.js b/src/normalization-handlers/multipleOf.js deleted file mode 100644 index a9c5a63..0000000 --- a/src/normalization-handlers/multipleOf.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const multipleOfNormalizationHandler = { - evaluate() { - } -}; - -export default multipleOfNormalizationHandler; diff --git a/src/normalization-handlers/not.js b/src/normalization-handlers/not.js deleted file mode 100644 index 17c5744..0000000 --- a/src/normalization-handlers/not.js +++ /dev/null @@ -1,14 +0,0 @@ -import { evaluateSchema } from "../json-schema-errors.js"; - -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const notNormalizationHandler = { - evaluate(not, instance, context) { - return [evaluateSchema(not, instance, context)]; - } -}; - -export default notNormalizationHandler; diff --git a/src/normalization-handlers/pattern.js b/src/normalization-handlers/pattern.js deleted file mode 100644 index 471728a..0000000 --- a/src/normalization-handlers/pattern.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const patternNormalizationHandler = { - evaluate() { - } -}; - -export default patternNormalizationHandler; diff --git a/src/normalization-handlers/readOnly.js b/src/normalization-handlers/readOnly.js deleted file mode 100644 index 86a5f3c..0000000 --- a/src/normalization-handlers/readOnly.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const readOnlyNormalizationHandler = { - evaluate() { - }, - annotation: true -}; - -export default readOnlyNormalizationHandler; diff --git a/src/normalization-handlers/ref.js b/src/normalization-handlers/ref.js deleted file mode 100644 index cf00a63..0000000 --- a/src/normalization-handlers/ref.js +++ /dev/null @@ -1,14 +0,0 @@ -import { evaluateSchema } from "../json-schema-errors.js"; - -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const refNormalizationHandler = { - evaluate(ref, instance, context) { - return [evaluateSchema(ref, instance, context)]; - } -}; - -export default refNormalizationHandler; diff --git a/src/normalization-handlers/required.js b/src/normalization-handlers/required.js deleted file mode 100644 index 0e8449c..0000000 --- a/src/normalization-handlers/required.js +++ /dev/null @@ -1,11 +0,0 @@ -/** -* @import { NormalizationHandler } from "../index.d.ts" -*/ - -/** @type NormalizationHandler */ -const requiredNormalizationHandler = { - evaluate() { - } -}; - -export default requiredNormalizationHandler; diff --git a/src/normalization-handlers/title.js b/src/normalization-handlers/title.js deleted file mode 100644 index 36411f9..0000000 --- a/src/normalization-handlers/title.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const titleNormalizationHandler = { - evaluate() { - }, - annotation: true -}; - -export default titleNormalizationHandler; diff --git a/src/normalization-handlers/type.js b/src/normalization-handlers/type.js deleted file mode 100644 index 1747f78..0000000 --- a/src/normalization-handlers/type.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const typeNormalizationHandler = { - evaluate() { - } -}; - -export default typeNormalizationHandler; diff --git a/src/normalization-handlers/uniqueItems.js b/src/normalization-handlers/uniqueItems.js deleted file mode 100644 index d28e07e..0000000 --- a/src/normalization-handlers/uniqueItems.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const uniqueItemsNormalizationHandler = { - evaluate() { - } -}; - -export default uniqueItemsNormalizationHandler; diff --git a/src/normalization-handlers/unknown.js b/src/normalization-handlers/unknown.js deleted file mode 100644 index 18afe91..0000000 --- a/src/normalization-handlers/unknown.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const unknownNormalizationHandler = { - evaluate() { - }, - annotation: true -}; - -export default unknownNormalizationHandler; diff --git a/src/normalization-handlers/writeOnly.js b/src/normalization-handlers/writeOnly.js deleted file mode 100644 index b298768..0000000 --- a/src/normalization-handlers/writeOnly.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @import { NormalizationHandler } from "../index.d.ts" - */ - -/** @type NormalizationHandler */ -const writeOnlyNormalizationHandler = { - evaluate() { - }, - annotation: true -}; - -export default writeOnlyNormalizationHandler;