diff --git a/README.md b/README.md index cc77444..d1e9fa1 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,83 +130,181 @@ applies if formats are validated. The `JSE` output format works this out from const errors = await jsonSchemaErrors(output, schemaUri, instance, { isFormatAsserted: true }); ``` -## API +## Messages and Translations -https://json-schema-errors.hyperjump.io +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. -## Custom Keywords and Error Handlers +```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 +`); +``` -`@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. +## API -1. **Normalization**: This phase takes the raw error output from the validator - and converts it to a `NormalizedOutput`. +https://json-schema-errors.hyperjump.io -2. **Error Handling**: This phase takes the `NormalizedOutput` and uses it to - generate the error messages that will be presented to the user. +## Custom Keywords -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`. +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 { 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 { addTranslation, defineKeyword } from "@hyperjump/json-schema-errors"; -const KEYWORD_URI = "https://example.com/keyword/startsWith"; +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}' +`); -setNormalizationHandler(KEYWORD_URI, { - evaluate() { - // Only applicator keywords need to return a value - } +defineKeyword("https://example.com/keyword/startsWith", { + error: (prefix, localization) => localization.format("startsWith-message", { prefix }), + requirement: (prefix, localization) => localization.formatRequirement("startsWith", { prefix }) }); +``` -addErrorHandler(async (normalizedErrors, instance, localization) => { - const errors: ErrorObject = []; +`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. - for (const schemaLocation in normalizedErrors[KEYWORD_URI]) { - if (normalizedErrors[KEYWORD_URI][schemaLocation]) { - continue; - } +`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. - const keyword = await getSchema(schemaLocation); - const startsWith = Schema.value(keyword) as string; +Keywords that are only annotations, like `title`, never fail and don't require +anything. - errors.push({ - message: "Expected a string that starts with '${startsWith}'", - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); - } - - return errors; -}); +```TypeScript +defineKeyword("https://example.com/keyword/note", { annotation: true }); ``` 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. -For example, support for the `allOf` keyword could look like the following. +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. ```TypeScript -import { setNormalizationHandler, evaluateSchema } from "@hyperjump/json-schema-errors"; - -const KEYWORD_URI = "https://json-schema.org/keyword/allOf"; +import { defineKeyword, evaluateSchema } from "@hyperjump/json-schema-errors"; -setNormalizationHandler(KEYWORD_URI, { +defineKeyword("https://json-schema.org/keyword/allOf", { evaluate(allOf, instance, context) { return allOf.map((schemaLocation) => evaluateSchema(schemaLocation, instance, context)); } }); ``` -See the `anyOf` or `oneOf` normalization and error handlers for an example of -implementing an applicator that also asserts. +### 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. + +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. + +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 { 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[] = []; + + for (const schemaLocation in normalizedErrors[KEYWORD_URI]) { + const { valid, value } = normalizedErrors[KEYWORD_URI][schemaLocation]; + if (valid !== false) { + continue; + } + + const prefix = value as string; + errors.push({ + message: context.localization.format("startsWith-message", { prefix }), + instanceLocation: Instance.uri(instance), + schemaLocations: [schemaLocation] + }); + } + + return errors; + }, + + success: (normalizedOutput, instance, context) => { + const successes: ErrorObject[] = []; + + for (const schemaLocation in normalizedOutput[KEYWORD_URI]) { + const prefix = normalizedOutput[KEYWORD_URI][schemaLocation].value as string; + successes.push({ + message: context.localization.formatRequirement("startsWith", { prefix }), + instanceLocation: Instance.uri(instance), + schemaLocations: [schemaLocation] + }); + } + + return successes; + } +}); +``` + +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/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/anyOf.js b/src/error-handlers/anyOf.js index 9f97a7a..b5c6707 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" @@ -8,7 +8,7 @@ import { allTrue, getErrors, getSuccesses, someTrue } from "../json-schema-error /** @type ErrorHandler */ const anyOfErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -25,41 +25,44 @@ 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; } } // 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)); } } @@ -67,7 +70,7 @@ const anyOfErrorHandler = { errors.push(...alternatives[0]); } else { errors.push({ - message: localization.getAnyOfErrorMessage(), + message: context.localization.getAnyOfErrorMessage(), alternatives, instanceLocation, schemaLocations: [schemaLocation] @@ -78,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 @@ -103,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..ae77105 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, getPlaceholder, getSiblingKeywordLocation, getSuccesses, getValidity, 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 { ContainsAst } from "../keywords/contains.js" + * @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 = []; @@ -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,19 +49,19 @@ 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 }) => { - 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: [descriptions[index]], 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: [description], 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 = []; @@ -121,68 +121,68 @@ 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, 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 3b52721..7ec9a9d 100644 --- a/src/error-handlers/dependentSchemas.js +++ b/src/error-handlers/dependentSchemas.js @@ -1,29 +1,27 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; import { describeConditional, - evaluateRequirements, - getCompiledKeywordValue, getSuccesses } from "../json-schema-errors.js"; /** * @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 */ 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) => { + 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 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, localization, context)); + successes.push(...describeSchemaDependencies(dependencies, outputs, instance, context)); } return successes; @@ -35,9 +33,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 = []; @@ -46,18 +44,17 @@ export const describeSchemaDependencies = (dependencies, outputs, instance, loca 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({ - 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..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"; /** @@ -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,16 +26,16 @@ const dependenciesErrorHandler = { 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/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, 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..520ca32 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 = []; @@ -37,19 +37,19 @@ 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, placeholder: property, each: (localization, count) => localization.getEachMatchingPropertySuccessMessage(pattern.source, count), none: (localization) => localization.getNoMatchingPropertySuccessMessage(pattern.source) - }, instance, schemaLocation, localization, context)); + }, instance, schemaLocation, context)); } } 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") ?? {} )); @@ -61,19 +61,19 @@ 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 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, 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..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" @@ -18,7 +18,7 @@ const keywordUris = [ /** @type ErrorHandler */ const formatErrorHandler = { - error: (normalizedErrors, instance, localization, context) => { + error: (normalizedErrors, instance, context) => { /** @type ErrorObject[] */ const errors = []; @@ -28,10 +28,10 @@ const formatErrorHandler = { continue; } - const format = /** @type string */ (getCompiledKeywordValue(context.ast, schemaLocation)); + const format = /** @type string */ (normalizedErrors[keywordUri][schemaLocation].value); 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) => { + return describeKeyword(normalizedOutput, keywordUri, instance, (/** @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 22a1cad..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"; @@ -9,42 +7,42 @@ 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 */ 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) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; 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) => (localization: Localization) => ErrorObject[] */ - const describe = (output) => (localization) => getSuccesses(output, instance, localization, context); + /** @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, // 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; @@ -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 341440d..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" */ @@ -14,7 +13,7 @@ import { describeScope, getCompiledKeywordValue, getPlaceholder, isPlaceholder } * @type ErrorHandler */ const itemsErrorHandler = { - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -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({ @@ -32,7 +31,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; @@ -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 5b530a4..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" @@ -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; @@ -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; @@ -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) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxItems", instance, context.ast, (/** @type number */ maxItems) => { - return localization.getMaxItemsSuccessMessage(maxItems); + success: (normalizedOutput, instance, context) => { + 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 463910e..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" @@ -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; @@ -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; @@ -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) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxLength", instance, context.ast, (/** @type number */ maxLength) => { - return localization.getMaxLengthSuccessMessage(maxLength); + success: (normalizedOutput, instance, context) => { + 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 b9c48c7..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" @@ -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; @@ -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; @@ -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) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxProperties", instance, context.ast, (/** @type number */ maxProperties) => { - return localization.getMaxPropertiesSuccessMessage(maxProperties); + success: (normalizedOutput, instance, context) => { + 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 4a19ab8..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" @@ -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; @@ -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; @@ -60,44 +60,44 @@ 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); + 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) => { - return localization.getExclusiveMaximumSuccessMessage(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({ - 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..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" @@ -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; @@ -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; @@ -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) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minItems", instance, context.ast, (/** @type number */ minItems) => { - return localization.getMinItemsSuccessMessage(minItems); + success: (normalizedOutput, instance, context) => { + 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 2d1c247..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" @@ -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; @@ -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; @@ -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) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minLength", instance, context.ast, (/** @type number */ minLength) => { - return localization.getMinLengthSuccessMessage(minLength); + success: (normalizedOutput, instance, context) => { + 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 6e1bc23..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" @@ -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 = []; @@ -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; @@ -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) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minProperties", instance, context.ast, (/** @type number */ minProperties) => { - return localization.getMinPropertiesSuccessMessage(minProperties); + success: (normalizedOutput, instance, context) => { + 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 8ec4cab..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" @@ -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[] */ @@ -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; @@ -61,44 +61,44 @@ 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); + 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) => { - return localization.getExclusiveMinimumSuccessMessage(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({ - 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..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" @@ -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 = []; @@ -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); @@ -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) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/multipleOf", instance, context.ast, (/** @type number */ multipleOf) => { - return localization.getMultipleOfSuccessMessage(multipleOf); + success: (normalizedOutput, instance, context) => { + 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 f8da16d..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, 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" @@ -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,39 +19,39 @@ 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), + errors.push(showSmallestFirst({ + message: context.localization.getNotErrorMessage(options.length === 1 ? "one" : "some"), + alternatives: options.map((option) => [option]), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] - }); + })); } } 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 5ce7cf0..4e1c52f 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 { allTrue, allowsAnyValue, countTrue, getCompiledKeywordValue, getErrors, getSuccesses, isPassing, limitItems, limitOptions, 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" - * @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 = []; @@ -20,12 +20,12 @@ 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, localization, context)); + errors.push(multipleMatchesError(matches, schemaLocation, instance, context)); continue; } @@ -35,34 +35,37 @@ 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; } } // 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); } @@ -71,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); } @@ -83,7 +86,7 @@ const oneOfErrorHandler = { } else { /** @type ErrorObject */ const alternativeErrors = { - message: localization.getOneOfErrorMessage(), + message: context.localization.getOneOfErrorMessage(), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }; @@ -97,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)); } } } @@ -143,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] }]; @@ -162,18 +165,16 @@ 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), + alternatives: removeCommonSuccesses(alternatives), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }; diff --git a/src/error-handlers/pattern.js b/src/error-handlers/pattern.js deleted file mode 100644 index 3c69e9e..0000000 --- a/src/error-handlers/pattern.js +++ /dev/null @@ -1,39 +0,0 @@ -import * as Instance from "@hyperjump/json-schema/instance/experimental"; -import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors.js"; - -/** - * @import { ErrorHandler, ErrorObject } from "../index.d.ts" - */ - -/** @type ErrorHandler */ -const patternErrorHandler = { - error: (normalizedErrors, instance, localization, 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 */ (getCompiledKeywordValue(context.ast, schemaLocation)); - const pattern = compiledPattern.source; - - errors.push({ - message: localization.getPatternErrorMessage(pattern), - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); - } - - return errors; - }, - - success: (normalizedOutput, instance, localization, context) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/pattern", instance, context.ast, (/** @type RegExp */ pattern) => { - return localization.getPatternSuccessMessage(pattern.source); - }); - } -}; - -export default patternErrorHandler; diff --git a/src/error-handlers/prefixItems.js b/src/error-handlers/prefixItems.js index 59e34e0..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 @@ -20,7 +18,7 @@ import { * @type ErrorHandler */ const prefixItemsErrorHandler = { - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -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,15 +43,14 @@ 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: (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(prefixItems[index], item, context) + }, instance, schemaLocation, context)); } } diff --git a/src/error-handlers/properties.js b/src/error-handlers/properties.js index 70b7b01..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 @@ -19,7 +17,7 @@ import { * @type ErrorHandler */ const propertiesErrorHandler = { - success: (normalizedOutput, instance, localization, context) => { + success: (normalizedOutput, instance, context) => { /** @type ErrorObject[] */ const successes = []; @@ -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,15 +38,14 @@ 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: (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(properties[propertyName], property, context) + }, instance, schemaLocation, context)); } } diff --git a/src/error-handlers/required.js b/src/error-handlers/required.js index 0f0b020..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" @@ -8,7 +7,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 = []; @@ -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) { @@ -68,13 +67,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 = []; @@ -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); @@ -91,18 +90,18 @@ const requiredErrorHandler = { if (allRequired.size > 0) { successes.push({ - message: localization.getRequiredSuccessMessage([...allRequired]), + message: context.localization.getRequiredSuccessMessage([...allRequired]), instanceLocation: Instance.uri(instance), schemaLocations: requiredSchemaLocations }); } 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({ - message: localization.getDependentRequiredSuccessMessage(propertyName, requiredProperties), + message: context.localization.getDependentRequiredSuccessMessage(propertyName, requiredProperties), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }); @@ -111,11 +110,11 @@ 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({ - 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..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" @@ -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 = []; @@ -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); @@ -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]); + ...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) => { - return localization.getEnumSuccessMessage([JSON.parse(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) => { - return localization.getEnumSuccessMessage(enumJson.map((json) => JSON.parse(json))); + ...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 02c9313..b84b85d 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 describeKeyword(normalizedOutput, "https://json-schema.org/keyword/uniqueItems", instance, (/** @type boolean */ uniqueItems) => { + 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 53cbc20..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 - * merged into the results of its parent schema, but the merged 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; @@ -139,11 +163,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 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[]; }; @@ -197,16 +224,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 @@ -215,21 +250,132 @@ export type ErrorHandlerContext = { isFormatAsserted: (keywordUri: string, format: string) => boolean | undefined; }; +/** + * 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 getValidity: (normalizedOutput: NormalizedOutput) => boolean | undefined; + /** * 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[]; + +/** + * 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: (subschema: NormalizedOutput | string, 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; + +/** + * 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; /** - * 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 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 getSuccesses: (normalizedOutput: NormalizedOutput, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[]; +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 }; +/** + * 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 47e4c6f..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,18 +80,93 @@ 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, getErrors, + getPlaceholder, getSuccesses, + getValidity, + isPlaceholder, jsonSchemaErrors, + negate, removeErrorHandler, setErrorHandler, - 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 90ab9de..b7f3a18 100644 --- a/src/json-schema-errors.js +++ b/src/json-schema-errors.js @@ -23,15 +23,17 @@ 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, { + /** @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 */ -const normalizationHandlers = {}; +/** @type Record> */ +const keywordDefinitions = {}; /** @type (schemaLocation: string, ast: AST) => boolean */ export const allowsAnyValue = (schemaLocation, ast) => { @@ -40,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 */ @@ -121,12 +118,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 } } }; } @@ -135,15 +132,15 @@ 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); const keywordInstanceLocation = instanceLocation.replace(/^#\*/, "#"); - let isKeywordValid = getValidity(keywordLocation, keywordInstanceLocation, context); + let isKeywordValid = getReportedValidity(keywordLocation, keywordInstanceLocation, context); /** @type API.EvaluationContext */ const keywordContext = { @@ -159,10 +156,10 @@ 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(isFailing)) { + if (validationKeyword.simpleApplicator && !isReported && keywordOutput?.some((suboutput) => getValidity(suboutput) === false)) { isKeywordValid = false; } @@ -170,17 +167,11 @@ 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 - ? { 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); @@ -201,7 +192,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; @@ -214,7 +205,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); @@ -234,7 +225,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"; /** @@ -273,29 +264,23 @@ 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))); }; -/** - * @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 @@ -303,88 +288,88 @@ 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 API.describeScope */ -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), - // 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)], - 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: (localization: Localization) => API.ErrorObject[]; - * then?: (localization: Localization) => API.ErrorObject[]; - * else?: (localization: Localization) => 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 API.describeConditional */ -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); } }; @@ -405,21 +390,47 @@ 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 = {}; +/** @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; @@ -431,14 +442,15 @@ 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 = []; - 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, context) ?? []; errors.push(...errorObjects); } } @@ -446,53 +458,32 @@ export const getErrors = (normalizedErrors, rootInstance, localization, context) return errors; }; +/** @type (normalizedOutput: API.NormalizedOutput) => API.NormalizedOutput */ +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,17 +491,32 @@ 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) => !keywordDefinitions[toAbsoluteIri(keywordUri)]?.conditional; /** @type API.getSuccesses */ -export const getSuccesses = (normalizedOutput, rootInstance, localization, 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) { /** @type API.ErrorObject */ const detailsNotShown = { - message: localization.getDetailsNotShownMessage(), + message: context.localization.getDetailsNotShownMessage(), instanceLocation: Instance.uri(rootInstance), schemaLocations: [] }; @@ -518,10 +524,16 @@ export const getSuccesses = (normalizedOutput, rootInstance, localization, conte 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 = []; - const describedOutput = withoutConditionalResults(normalizedOutput); + const describedOutput = flatten(normalizedOutput, isRequired); descriptionDepth++; try { @@ -530,7 +542,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); } } @@ -573,32 +585,63 @@ const collapseDetailsNotShown = (items) => { const MAX_ENTRIES = 5; +/** @type WeakSet */ +const smallestFirstGroups = new WeakSet(); + /** - * 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. + * 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 (allItems: API.ErrorObject[], instance: JsonNode, localization: Localization) => API.ErrorObject[] + * @type (errorObject: API.ErrorObject) => API.ErrorObject */ -export const limitItems = (allItems, instance, localization) => { +export const showSmallestFirst = (errorObject) => { + smallestFirstGroups.add(errorObject); + return errorObject; +}; + +/** + * 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 (errorObjects: API.ErrorObject[], context: API.ErrorHandlerContext) => API.ErrorObject[] + */ +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, localization)]; + 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, localization: Localization, isSorted?: boolean) => API.ErrorObject[][] - */ -export const limitOptions = (allOptions, instance, localization, 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; @@ -611,13 +654,13 @@ 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, instanceLocation, context)]]; }; -/** @type (count: number, instance: JsonNode, localization: Localization) => API.ErrorObject */ -const notShown = (count, instance, localization) => ({ - message: localization.getNotShownMessage(count), - instanceLocation: Instance.uri(instance), +/** @type (count: number, instanceLocation: string, context: API.ErrorHandlerContext) => API.ErrorObject */ +const notShown = (count, instanceLocation, context) => ({ + message: context.localization.getNotShownMessage(count), + instanceLocation, schemaLocations: [] }); @@ -628,34 +671,32 @@ const sizeOf = (errorObjects) => errorObjects.reduce((size, errorObject) => { }, 0); }, 0); -/** @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) { +/** + * 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; + + const results = flattenOutput(normalizedOutput); + for (const instanceLocation in results) { + for (const keywordUri in results[instanceLocation]) { + for (const schemaLocation in results[instanceLocation][keywordUri]) { + const { valid } = results[instanceLocation][keywordUri][schemaLocation]; + if (valid === false) { return false; + } else if (valid === undefined) { + validity = undefined; } } } } - return true; -}; - -/** @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) { - return true; - } - } - } - } - - return false; + return validity; }; /** @type WeakSet */ @@ -666,9 +707,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 API.countTrue */ -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)) { @@ -676,37 +717,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 options.flat(); } - return [{ - message: localization.getCountTrueMessage(min, max === options.length ? Infinity : max), - alternatives: limitOptions(options.map((option) => limitItems(option, instance, localization)), instance, localization), + return [showSmallestFirst({ + message: context.localization.getCountTrueMessage(min, max === options.length ? Infinity : max), + alternatives: options, 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 API.someTrue */ +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 API.allTrue */ -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: [items], instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }; @@ -722,14 +763,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/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/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"); + }); }); 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; diff --git a/src/output-format.js b/src/output-format.js index e643363..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) => { - const localization = Localization.forLocale(options.locale ?? "en-US"); - return getErrors(plugin.output, instance, localization, { + /** @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 85041b5..7db0c9a 100644 --- a/src/output-plugin.js +++ b/src/output-plugin.js @@ -30,24 +30,17 @@ 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) { - const [keywordUri, schemaLocation] = keywordNode; - - if (keyword.simpleApplicator) { - for (const subSchemaOutput of context.subSchemaOutput ?? []) { - mergeOutput(schemaContext.output, subSchemaOutput); - } - } + afterKeyword(keywordNode, instance, context, valid, schemaContext) { + const [keywordUri, schemaLocation, value] = keywordNode; schemaContext.output[Instance.uri(instance)] ??= {}; schemaContext.output[Instance.uri(instance)][keywordUri] ??= {}; schemaContext.output[Instance.uri(instance)][keywordUri][schemaLocation] = context.subSchemaOutput - ? { valid, outputs: context.subSchemaOutput } - : { valid }; + ? { valid, value, outputs: context.subSchemaOutput } + : { valid, value }; } /** @type NonNullable["afterSchema"]> */ @@ -55,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 ??= []; @@ -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..50477e3 100644 --- a/src/output-plugin.test.ts +++ b/src/output-plugin.test.ts @@ -23,17 +23,15 @@ 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, + 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" } } } } @@ -41,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,17 +57,15 @@ 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, + 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" } } } } @@ -77,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`] } } } }); @@ -91,20 +87,16 @@ 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`]: { valid: false, + value: [/(?!)/u, `${schemaUri}#/additionalProperties`], outputs: [ { "#/foo": { "https://json-schema.org/validation": { - [`${schemaUri}#/additionalProperties`]: { valid: false } + [`${schemaUri}#/additionalProperties`]: { valid: false, value: false } } } } @@ -121,32 +113,23 @@ 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`]: { 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 } } } } @@ -165,20 +148,16 @@ 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`]: { 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" } } } } @@ -197,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`] + } } } }); @@ -217,27 +199,22 @@ 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, + 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 } } } } @@ -262,27 +239,24 @@ describe("JSON Schema Errors Output Format", () => { expect(outputPlugin.output).to.eql({ "#": { "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 } + [`${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 } } } } @@ -305,27 +279,22 @@ 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, + 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 } } } } @@ -353,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" } } } } @@ -390,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" } } } } @@ -429,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" } } } } @@ -466,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" } } } } @@ -502,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" } } } } @@ -529,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" } } } } @@ -558,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" } } } } @@ -592,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" } } } } @@ -627,17 +612,15 @@ 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, + 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"] } } } } @@ -660,25 +643,25 @@ 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, + 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"] } } } } @@ -700,17 +683,15 @@ 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, + 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"] } } } } @@ -736,28 +717,27 @@ 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" } } } } ] } }, - "https://json-schema.org/keyword/minLength": { - [`${schemaUri}#/then/minLength`]: { valid: false } - }, "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 } } } } @@ -781,28 +761,27 @@ 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" } } } } ] } }, - "https://json-schema.org/keyword/minLength": { - [`${schemaUri}#/then/minLength`]: { valid: true } - }, "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 } } } } @@ -827,33 +806,32 @@ 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" } } } } ] } }, - "https://json-schema.org/keyword/minimum": { - [`${schemaUri}#/else/minimum`]: { valid: false } - }, "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 } } } } @@ -876,33 +854,32 @@ 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" } } } } ] } }, - "https://json-schema.org/keyword/minimum": { - [`${schemaUri}#/else/minimum`]: { valid: true } - }, "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 } } } } @@ -923,32 +900,23 @@ 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`]: { 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" } } } } @@ -967,20 +935,16 @@ 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`]: { 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" } } } } @@ -1004,32 +968,26 @@ 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`]: { 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" } } } } @@ -1051,32 +1009,26 @@ 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`]: { 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" } } } } @@ -1100,32 +1052,26 @@ 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`]: { 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" } } } } @@ -1147,32 +1093,26 @@ 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`]: { 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" } } } } @@ -1196,32 +1136,26 @@ 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`]: { 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" } } } } @@ -1243,32 +1177,26 @@ 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`]: { 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" } } } } @@ -1289,32 +1217,23 @@ 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`]: { 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 } } } } @@ -1333,20 +1252,16 @@ 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`]: { 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 } } } } @@ -1365,20 +1280,16 @@ 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`]: { valid: false, + value: `${schemaUri}#/unevaluatedProperties`, outputs: [ { "#/foo": { "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedProperties`]: { valid: false } + [`${schemaUri}#/unevaluatedProperties`]: { valid: false, value: false } } } } @@ -1401,22 +1312,21 @@ 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`]: { 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 } } } } @@ -1426,22 +1336,18 @@ 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 } } } } ] } } - }, - "#/baz": { - "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedProperties`]: { valid: false } - } } }); }); @@ -1454,20 +1360,16 @@ 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`]: { 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" } } } } @@ -1486,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` } } } }); @@ -1500,20 +1402,16 @@ 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`]: { valid: false, + value: `${schemaUri}#/unevaluatedItems`, outputs: [ { "#/0": { "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedItems`]: { valid: false } + [`${schemaUri}#/unevaluatedItems`]: { valid: false, value: false } } } } @@ -1532,20 +1430,16 @@ 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`]: { 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" } } } } @@ -1565,22 +1459,21 @@ 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`]: { 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 } } } } @@ -1590,22 +1483,18 @@ 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 } } } } ] } } - }, - "#/2": { - "https://json-schema.org/validation": { - [`${schemaUri}#/unevaluatedItems`]: { valid: false } - } } }); }); @@ -1618,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` } } } }); @@ -1634,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\"" } } } }); @@ -1648,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\"" } } } }); @@ -1668,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"]]] } } } }); @@ -1687,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"]]] } } } }); @@ -1705,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"]]] } } } }); @@ -1721,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\""] } } } }); @@ -1735,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\""] } } } }); @@ -1751,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 } } } }); @@ -1765,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 } } } }); @@ -1781,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 } } } }); @@ -1795,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 } } } }); @@ -1811,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 } } } }); @@ -1825,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 } } } }); @@ -1841,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 } } } }); @@ -1855,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 } } } }); @@ -1871,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 } } } }); @@ -1885,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 } } } }); @@ -1901,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 } } } }); @@ -1915,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 } } } }); @@ -1931,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 } } } }); @@ -1945,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 } } } }); @@ -1961,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 } } } }); @@ -1975,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 } } } }); @@ -1991,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 } } } }); @@ -2005,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 } } } }); @@ -2021,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 } } } }); @@ -2035,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 } } } }); @@ -2051,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 } } } }); @@ -2065,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 } } } }); @@ -2081,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 } } } }); @@ -2095,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 } } } }); @@ -2111,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"] } } } }); @@ -2125,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"] } } } }); @@ -2139,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"] } } } }); @@ -2155,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" } } } }); @@ -2169,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"] } } } }); @@ -2183,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" } } } }); @@ -2199,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 } } } }); @@ -2213,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 } } } }); @@ -2232,20 +2121,19 @@ 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`]: { 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" } } } } @@ -2253,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"] } } } }); @@ -2273,47 +2161,23 @@ 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, + value: { foo: `${schemaUri}#/properties/foo` }, 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`]: { 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": { 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": {