diff --git a/README.md b/README.md index 4c9b340..cc77444 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,16 @@ console.log(errors); // ] ``` +Validators often only validate the `format` keyword if they're configured to. +Use the `isFormatAsserted` option to say whether the validator validated +formats. If it isn't given, messages that describe `format` say that it only +applies if formats are validated. The `JSE` output format works this out from +`@hyperjump/json-schema`'s configuration and each dialect's default. + +```TypeScript +const errors = await jsonSchemaErrors(output, schemaUri, instance, { isFormatAsserted: true }); +``` + ## API https://json-schema-errors.hyperjump.io diff --git a/src/error-handlers/anyOf.js b/src/error-handlers/anyOf.js index 10534d7..9f97a7a 100644 --- a/src/error-handlers/anyOf.js +++ b/src/error-handlers/anyOf.js @@ -8,7 +8,7 @@ import { allTrue, getErrors, getSuccesses, someTrue } from "../json-schema-error /** @type ErrorHandler */ const anyOfErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; @@ -53,13 +53,13 @@ const anyOfErrorHandler = { } // The alternative passed all the filters - alternatives.push(getErrors(alternative, instance, localization, ast)); + alternatives.push(getErrors(alternative, instance, localization, 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, ast)); + alternatives.push(getErrors(alternative, instance, localization, context)); } } @@ -78,7 +78,7 @@ const anyOfErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; @@ -91,7 +91,7 @@ const anyOfErrorHandler = { // 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, ast); + return getSuccesses(alternative, instance, localization, context); }); // An alternative that can't be described means we can't say how to make it fail @@ -112,7 +112,7 @@ const anyOfErrorHandler = { // 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, ast); + return getSuccesses(alternative, instance, localization, context); }); if (descriptions.some((description) => description.length === 0)) { diff --git a/src/error-handlers/contains.js b/src/error-handlers/contains.js index 1b62998..f40221e 100644 --- a/src/error-handlers/contains.js +++ b/src/error-handlers/contains.js @@ -13,7 +13,7 @@ const keywordUris = [ /** @type ErrorHandler */ const containsErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; @@ -27,20 +27,20 @@ const containsErrorHandler = { /** @type string[] */ const schemaLocations = [schemaLocation]; - const contains = /** @type ContainsAst */ (getCompiledKeywordValue(ast, schemaLocation)); + const contains = /** @type ContainsAst */ (getCompiledKeywordValue(context.ast, schemaLocation)); /** @type ContainsRange */ const range = {}; if (typeof contains !== "string") { if (contains.minContains !== 1) { range.minContains = contains.minContains; - const minContainsLocation = getSiblingKeywordLocation(ast, schemaLocation, "https://json-schema.org/keyword/minContains"); + const minContainsLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/minContains"); schemaLocations.push(minContainsLocation); } if (contains.maxContains !== Number.MAX_SAFE_INTEGER) { range.maxContains = contains.maxContains; - const maxContainsLocation = getSiblingKeywordLocation(ast, schemaLocation, "https://json-schema.org/keyword/maxContains"); + const maxContainsLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/maxContains"); schemaLocations.push(maxContainsLocation); // Too many items matched. Report on each matching item how it @@ -53,7 +53,7 @@ const containsErrorHandler = { }); if (matches.length > contains.maxContains) { const descriptions = matches.map(({ itemOutput }) => { - return getSuccesses(itemOutput, instance, localization, ast); + return getSuccesses(itemOutput, instance, localization, context); }); // If any match can't be described, the errors would be misleading @@ -84,7 +84,7 @@ 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, ast); + const description = describeEach(containsLocation, item, instance, localization, context); if (description.length > 0) { errors.push({ @@ -93,7 +93,7 @@ const containsErrorHandler = { instanceLocation: Instance.uri(instance), schemaLocations: schemaLocations }); - } else if (allowsAnyValue(containsLocation, ast)) { + } else if (allowsAnyValue(containsLocation, context.ast)) { // Any item matches, so there aren't enough items errors.push({ message: localization.getMinItemsErrorMessage(range.minContains ?? 1), @@ -113,7 +113,7 @@ const containsErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; @@ -121,7 +121,7 @@ const containsErrorHandler = { for (const schemaLocation in normalizedOutput[keywordUri]) { const itemOutputs = normalizedOutput[keywordUri][schemaLocation].outputs ?? []; - const contains = /** @type ContainsAst | string */ (getCompiledKeywordValue(ast, schemaLocation)); + const contains = /** @type ContainsAst | string */ (getCompiledKeywordValue(context.ast, schemaLocation)); const minContains = typeof contains === "string" ? 1 : contains.minContains; const maxContains = typeof contains === "string" || contains.maxContains === Number.MAX_SAFE_INTEGER ? Infinity @@ -132,7 +132,7 @@ const containsErrorHandler = { const notMatching = itemOutputs.filter((itemOutput) => !isPassing(itemOutput)); /** @type (itemOutput: NormalizedOutput, localization: Localization) => ErrorObject[] */ - const describe = (itemOutput, localization) => getSuccesses(itemOutput, instance, localization, ast); + const describe = (itemOutput, localization) => getSuccesses(itemOutput, instance, localization, context); if (localization.isNegated) { // 'contains' fails if too few items match or too many items match diff --git a/src/error-handlers/dependentSchemas.js b/src/error-handlers/dependentSchemas.js index 8ad9a09..3b52721 100644 --- a/src/error-handlers/dependentSchemas.js +++ b/src/error-handlers/dependentSchemas.js @@ -7,9 +7,8 @@ import { } from "../json-schema-errors.js"; /** - * @import { AST } from "@hyperjump/json-schema/experimental" * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" - * @import { ErrorHandler, ErrorObject, Localization, NormalizedOutput } from "../index.d.ts" + * @import { ErrorHandler, ErrorHandlerContext, ErrorObject, Localization, NormalizedOutput } from "../index.d.ts" */ /** @type ErrorHandler */ @@ -17,14 +16,14 @@ const dependentSchemasErrorHandler = { // Failures in dependent schemas are merged into the parent schema's results, // so they're reported by the handlers for the keywords that failed - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/dependentSchemas"]) { - const dependencies = /** @type [string, string][] */ (getCompiledKeywordValue(ast, schemaLocation)); + const dependencies = /** @type [string, string][] */ (getCompiledKeywordValue(context.ast, schemaLocation)); const outputs = normalizedOutput["https://json-schema.org/keyword/dependentSchemas"][schemaLocation].outputs ?? []; - successes.push(...describeSchemaDependencies(dependencies, outputs, instance, localization, ast)); + successes.push(...describeSchemaDependencies(dependencies, outputs, instance, localization, context)); } return successes; @@ -36,9 +35,9 @@ const dependentSchemasErrorHandler = { * dependency's property. The outputs are for the dependencies whose property is * present, in the order they appear in the schema. * - * @type (dependencies: [string, string][], outputs: NormalizedOutput[], instance: JsonNode, localization: Localization, ast: AST) => ErrorObject[] + * @type (dependencies: [string, string][], outputs: NormalizedOutput[], instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[] */ -export const describeSchemaDependencies = (dependencies, outputs, instance, localization, ast) => { +export const describeSchemaDependencies = (dependencies, outputs, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; @@ -48,7 +47,7 @@ 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, ast); + ?? evaluateRequirements(dependencyLocation, instance, context.ast); // The dependency only applies if the property is present successes.push(...describeConditional({ @@ -57,7 +56,7 @@ export const describeSchemaDependencies = (dependencies, outputs, instance, loca instanceLocation: Instance.uri(instance), schemaLocations: [dependencyLocation] }], - then: (localization) => getSuccesses(output, instance, localization, ast) + then: (localization) => getSuccesses(output, instance, localization, context) }, instance, dependencyLocation, localization)); } diff --git a/src/error-handlers/draft-04/dependencies.js b/src/error-handlers/draft-04/dependencies.js index cd2b317..2abfb50 100644 --- a/src/error-handlers/draft-04/dependencies.js +++ b/src/error-handlers/draft-04/dependencies.js @@ -7,7 +7,7 @@ import { describeSchemaDependencies } from "../dependentSchemas.js"; /** @type ErrorHandler */ const dependenciesErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, 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, ast); + const dependentSchemaErrors = getErrors(dependentSchemaOutput, instance, localization, context); errors.push(...dependentSchemaErrors); } } @@ -26,16 +26,16 @@ const dependenciesErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, 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(ast, schemaLocation)); + const dependencies = /** @type [string, string | string[]][] */ (getCompiledKeywordValue(context.ast, schemaLocation)); const schemaDependencies = /** @type [string, string][] */ (dependencies.filter(([, dependency]) => typeof dependency === "string")); const outputs = normalizedOutput["https://json-schema.org/keyword/draft-04/dependencies"][schemaLocation].outputs ?? []; - successes.push(...describeSchemaDependencies(schemaDependencies, outputs, instance, localization, ast)); + successes.push(...describeSchemaDependencies(schemaDependencies, outputs, instance, localization, context)); } return successes; diff --git a/src/error-handlers/eachProperty.js b/src/error-handlers/eachProperty.js index d5e2173..62a5ab3 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, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; @@ -37,43 +37,43 @@ 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(ast, schemaLocation)); + const patternProperties = /** @type [RegExp, string][] */ (getCompiledKeywordValue(context.ast, schemaLocation)); 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, ast)); + }, instance, schemaLocation, localization, context)); } } for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/additionalProperties"]) { - const [, subschemaLocation] = /** @type [RegExp, string] */ (getCompiledKeywordValue(ast, schemaLocation)); + const [, subschemaLocation] = /** @type [RegExp, string] */ (getCompiledKeywordValue(context.ast, schemaLocation)); const properties = Object.keys(/** @type Record */ ( - getSiblingValue(ast, schemaLocation, "https://json-schema.org/keyword/properties") ?? {} + getSiblingValue(context.ast, schemaLocation, "https://json-schema.org/keyword/properties") ?? {} )); const patterns = /** @type [RegExp, string][] */ ( - getSiblingValue(ast, schemaLocation, "https://json-schema.org/keyword/patternProperties") ?? [] + getSiblingValue(context.ast, schemaLocation, "https://json-schema.org/keyword/patternProperties") ?? [] ).map(([pattern]) => pattern.source); successes.push(...describeScope({ subschemaLocation, placeholder: property, each: (localization, count) => localization.getEachAdditionalPropertySuccessMessage(properties, patterns, count), none: (localization) => localization.getNoAdditionalPropertySuccessMessage(properties, patterns) - }, instance, schemaLocation, localization, ast)); + }, instance, schemaLocation, localization, 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(ast, schemaLocation)); + const subschemaLocation = /** @type string */ (getCompiledKeywordValue(context.ast, schemaLocation)); successes.push(...describeScope({ subschemaLocation, placeholder: propertyName, each: (localization, count) => localization.getEachPropertyNameSuccessMessage(count), none: (localization) => localization.getMaxPropertiesSuccessMessage(0) - }, instance, schemaLocation, localization, ast)); + }, instance, schemaLocation, localization, context)); } return successes; diff --git a/src/error-handlers/format.js b/src/error-handlers/format.js index 44926dc..643427b 100644 --- a/src/error-handlers/format.js +++ b/src/error-handlers/format.js @@ -6,6 +6,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. */ const keywordUris = [ + "https://json-schema.org/keyword/format", "https://json-schema.org/keyword/draft-2020-12/format", "https://json-schema.org/keyword/draft-2020-12/format-assertion", "https://json-schema.org/keyword/draft-2019-09/format", @@ -17,7 +18,7 @@ const keywordUris = [ /** @type ErrorHandler */ const formatErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; @@ -27,7 +28,7 @@ const formatErrorHandler = { continue; } - const format = /** @type string */ (getCompiledKeywordValue(ast, schemaLocation)); + const format = /** @type string */ (getCompiledKeywordValue(context.ast, schemaLocation)); errors.push({ message: localization.getFormatErrorMessage(format), @@ -40,14 +41,19 @@ const formatErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { - // Whether 'format' is an assertion depends on how the validator is - // configured, which we don't know, so the messages say that it only applies - // if formats are validated. That includes 'format-assertion' because some - // validators can be configured not to validate it either. + success: (normalizedOutput, instance, localization, 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, ast, (/** @type string */ format) => { - return localization.getFormatSuccessMessage(format); + return describeKeyword(normalizedOutput, keywordUri, instance, context.ast, (/** @type string */ format) => { + switch (context.isFormatAsserted(keywordUri, format)) { + case true: + return localization.getFormatSuccessMessage(format); + case false: + return undefined; + default: + return localization.getFormatIfValidatedSuccessMessage(format); + } }); }); } diff --git a/src/error-handlers/ifThenElse.js b/src/error-handlers/ifThenElse.js index 896c610..22a1cad 100644 --- a/src/error-handlers/ifThenElse.js +++ b/src/error-handlers/ifThenElse.js @@ -17,20 +17,20 @@ const ifThenElseErrorHandler = { // Failures in 'then' and 'else' are merged into the parent schema's results, // so they're reported by the handlers for the keywords that failed - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, 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, ast); - const elseOutput = getSiblingOutput(normalizedOutput, "https://json-schema.org/keyword/else", ifLocation, instance, ast); + 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); if (!ifOutput || (!thenOutput && !elseOutput)) { continue; } /** @type (output: NormalizedOutput) => (localization: Localization) => ErrorObject[] */ - const describe = (output) => (localization) => getSuccesses(output, instance, localization, ast); + const describe = (output) => (localization) => getSuccesses(output, instance, localization, context); // Both branches are described, even if we know which way 'if' went, // because changing the value could change whether 'if' passes diff --git a/src/error-handlers/items.js b/src/error-handlers/items.js index 5ff6184..341440d 100644 --- a/src/error-handlers/items.js +++ b/src/error-handlers/items.js @@ -14,7 +14,7 @@ import { describeScope, getCompiledKeywordValue, getPlaceholder, isPlaceholder } * @type ErrorHandler */ const itemsErrorHandler = { - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; @@ -24,7 +24,7 @@ const itemsErrorHandler = { return successes; } - for (const [schemaLocation, startIndex, itemsLocation] of getItemsKeywords(normalizedOutput, ast)) { + for (const [schemaLocation, startIndex, itemsLocation] of getItemsKeywords(normalizedOutput, context.ast)) { // 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 +32,7 @@ const itemsErrorHandler = { placeholder: getPlaceholder(instance, String(Math.max(startIndex, length))), each: (localization, count) => localization.getEachItemSuccessMessage(startIndex, count), none: (localization) => localization.getMaxItemsSuccessMessage(startIndex) - }, instance, schemaLocation, localization, ast)); + }, instance, schemaLocation, localization, context)); } return successes; diff --git a/src/error-handlers/maxItems.js b/src/error-handlers/maxItems.js index f329680..5b530a4 100644 --- a/src/error-handlers/maxItems.js +++ b/src/error-handlers/maxItems.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const maxItemsErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; let lowestMaxItems = Infinity; @@ -18,7 +18,7 @@ const maxItemsErrorHandler = { continue; } - const maxItems = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const maxItems = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (maxItems < lowestMaxItems) { lowestMaxItems = maxItems; @@ -37,8 +37,8 @@ const maxItemsErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxItems", instance, ast, (/** @type number */ maxItems) => { + success: (normalizedOutput, instance, localization, context) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxItems", instance, context.ast, (/** @type number */ maxItems) => { return localization.getMaxItemsSuccessMessage(maxItems); }); } diff --git a/src/error-handlers/maxLength.js b/src/error-handlers/maxLength.js index 02cddc4..463910e 100644 --- a/src/error-handlers/maxLength.js +++ b/src/error-handlers/maxLength.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const maxLengthErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; let lowestMaxLength = Infinity; @@ -18,7 +18,7 @@ const maxLengthErrorHandler = { continue; } - const maxLength = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const maxLength = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (maxLength < lowestMaxLength) { lowestMaxLength = maxLength; @@ -36,8 +36,8 @@ const maxLengthErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxLength", instance, ast, (/** @type number */ maxLength) => { + success: (normalizedOutput, instance, localization, context) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxLength", instance, context.ast, (/** @type number */ maxLength) => { return localization.getMaxLengthSuccessMessage(maxLength); }); } diff --git a/src/error-handlers/maxProperties.js b/src/error-handlers/maxProperties.js index 6f8335f..b9c48c7 100644 --- a/src/error-handlers/maxProperties.js +++ b/src/error-handlers/maxProperties.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const maxPropertiesErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; let lowestMaxProperties = Infinity; @@ -17,7 +17,7 @@ const maxPropertiesErrorHandler = { continue; } - const maxProperties = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const maxProperties = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (maxProperties < lowestMaxProperties) { lowestMaxProperties = maxProperties; @@ -35,8 +35,8 @@ const maxPropertiesErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxProperties", instance, ast, (/** @type number */ maxProperties) => { + success: (normalizedOutput, instance, localization, context) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maxProperties", instance, context.ast, (/** @type number */ maxProperties) => { return localization.getMaxPropertiesSuccessMessage(maxProperties); }); } diff --git a/src/error-handlers/maximum.js b/src/error-handlers/maximum.js index 2ac7c01..4a19ab8 100644 --- a/src/error-handlers/maximum.js +++ b/src/error-handlers/maximum.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue, getSiblingKeywordLocation } f /** @type ErrorHandler */ const maximumErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { let lowestMaximum = Infinity; let isExclusive = false; @@ -19,7 +19,7 @@ const maximumErrorHandler = { continue; } - const maximum = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const maximum = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (maximum < lowestMaximum) { lowestMaximum = maximum; schemaLocations = [schemaLocation]; @@ -31,7 +31,7 @@ const maximumErrorHandler = { continue; } - const exclusiveMaximum = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const exclusiveMaximum = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (exclusiveMaximum < lowestMaximum) { lowestMaximum = exclusiveMaximum; isExclusive = true; @@ -44,13 +44,13 @@ const maximumErrorHandler = { continue; } - const [maximum, exclusive] = /** @type [number, boolean] */ (getCompiledKeywordValue(ast, schemaLocation)); + const [maximum, exclusive] = /** @type [number, boolean] */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (maximum < lowestMaximum) { lowestMaximum = maximum; isExclusive = exclusive; schemaLocations = [schemaLocation]; if (exclusive) { - const exclusiveLocation = getSiblingKeywordLocation(ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMaximum"); + const exclusiveLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMaximum"); schemaLocations.push(exclusiveLocation); } } @@ -73,23 +73,23 @@ const maximumErrorHandler = { } }, - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; - successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/maximum", instance, ast, (/** @type number */ maximum) => { + 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/exclusiveMaximum", instance, ast, (/** @type number */ exclusiveMaximum) => { + successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/exclusiveMaximum", instance, context.ast, (/** @type number */ exclusiveMaximum) => { return 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(ast, schemaLocation)); + const [maximum, exclusive] = /** @type [number, boolean] */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (exclusive) { - const exclusiveLocation = getSiblingKeywordLocation(ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMaximum"); + const exclusiveLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMaximum"); successes.push({ message: localization.getExclusiveMaximumSuccessMessage(maximum), instanceLocation: Instance.uri(instance), diff --git a/src/error-handlers/minItems.js b/src/error-handlers/minItems.js index 9f646c2..3ba7193 100644 --- a/src/error-handlers/minItems.js +++ b/src/error-handlers/minItems.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const minItemsErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; let highestMinItem = 0; @@ -18,7 +18,7 @@ const minItemsErrorHandler = { continue; } - const minItems = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const minItems = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (minItems > highestMinItem) { highestMinItem = minItems; @@ -37,8 +37,8 @@ const minItemsErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minItems", instance, ast, (/** @type number */ minItems) => { + success: (normalizedOutput, instance, localization, context) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minItems", instance, context.ast, (/** @type number */ minItems) => { return localization.getMinItemsSuccessMessage(minItems); }); } diff --git a/src/error-handlers/minLength.js b/src/error-handlers/minLength.js index 88ee4f2..2d1c247 100644 --- a/src/error-handlers/minLength.js +++ b/src/error-handlers/minLength.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const minLengthErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; let highestMinLength = -Infinity; @@ -18,7 +18,7 @@ const minLengthErrorHandler = { continue; } - const minLength = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const minLength = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (minLength > highestMinLength) { highestMinLength = minLength; @@ -36,8 +36,8 @@ const minLengthErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minLength", instance, ast, (/** @type number */ minLength) => { + success: (normalizedOutput, instance, localization, context) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minLength", instance, context.ast, (/** @type number */ minLength) => { return localization.getMinLengthSuccessMessage(minLength); }); } diff --git a/src/error-handlers/minProperties.js b/src/error-handlers/minProperties.js index 9814ebe..6e1bc23 100644 --- a/src/error-handlers/minProperties.js +++ b/src/error-handlers/minProperties.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const minPropertiesErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; @@ -19,7 +19,7 @@ const minPropertiesErrorHandler = { continue; } - const minProperties = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const minProperties = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (minProperties > highestMinProperties) { highestMinProperties = minProperties; @@ -38,8 +38,8 @@ const minPropertiesErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minProperties", instance, ast, (/** @type number */ minProperties) => { + success: (normalizedOutput, instance, localization, context) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minProperties", instance, context.ast, (/** @type number */ minProperties) => { return localization.getMinPropertiesSuccessMessage(minProperties); }); } diff --git a/src/error-handlers/minimum.js b/src/error-handlers/minimum.js index b836f2b..8ec4cab 100644 --- a/src/error-handlers/minimum.js +++ b/src/error-handlers/minimum.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue, getSiblingKeywordLocation } f /** @type ErrorHandler */ const minimumErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { let highestMinimum = -Infinity; let isExclusive = false; /** @type string[] */ @@ -18,7 +18,7 @@ const minimumErrorHandler = { continue; } - const minimum = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const minimum = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (minimum > highestMinimum) { highestMinimum = minimum; @@ -31,7 +31,7 @@ const minimumErrorHandler = { continue; } - const exclusiveMinimum = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const exclusiveMinimum = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (exclusiveMinimum > highestMinimum) { highestMinimum = exclusiveMinimum; @@ -45,13 +45,13 @@ const minimumErrorHandler = { continue; } - const [minimum, exclusive] = /** @type [number, boolean] */ (getCompiledKeywordValue(ast, schemaLocation)); + const [minimum, exclusive] = /** @type [number, boolean] */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (minimum > highestMinimum) { highestMinimum = minimum; isExclusive = exclusive; schemaLocations = [schemaLocation]; if (exclusive) { - const exclusiveLocation = getSiblingKeywordLocation(ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMinimum"); + const exclusiveLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMinimum"); schemaLocations.push(exclusiveLocation); } } @@ -74,23 +74,23 @@ const minimumErrorHandler = { } }, - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; - successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/minimum", instance, ast, (/** @type number */ minimum) => { + 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/exclusiveMinimum", instance, ast, (/** @type number */ exclusiveMinimum) => { + successes.push(...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/exclusiveMinimum", instance, context.ast, (/** @type number */ exclusiveMinimum) => { return 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(ast, schemaLocation)); + const [minimum, exclusive] = /** @type [number, boolean] */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (exclusive) { - const exclusiveLocation = getSiblingKeywordLocation(ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMinimum"); + const exclusiveLocation = getSiblingKeywordLocation(context.ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMinimum"); successes.push({ message: localization.getExclusiveMinimumSuccessMessage(minimum), instanceLocation: Instance.uri(instance), diff --git a/src/error-handlers/multipleOf.js b/src/error-handlers/multipleOf.js index 561b0ed..53d4a85 100644 --- a/src/error-handlers/multipleOf.js +++ b/src/error-handlers/multipleOf.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const multipleOfErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; @@ -22,7 +22,7 @@ const multipleOfErrorHandler = { hasError = true; } - const multipleOf = /** @type number */ (getCompiledKeywordValue(ast, schemaLocation)); + const multipleOf = /** @type number */ (getCompiledKeywordValue(context.ast, schemaLocation)); combinedMultipleOf = combinedMultipleOf === null ? multipleOf : lcm(combinedMultipleOf, multipleOf); schemaLocations.push(schemaLocation); @@ -39,8 +39,8 @@ const multipleOfErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/multipleOf", instance, ast, (/** @type number */ multipleOf) => { + success: (normalizedOutput, instance, localization, context) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/multipleOf", instance, context.ast, (/** @type number */ multipleOf) => { return localization.getMultipleOfSuccessMessage(multipleOf); }); } diff --git a/src/error-handlers/not.js b/src/error-handlers/not.js index d8065db..f8da16d 100644 --- a/src/error-handlers/not.js +++ b/src/error-handlers/not.js @@ -7,7 +7,7 @@ import { allTrue, getSuccesses, isAllTrueGroup, limitOptions, someTrue } from ". /** @type ErrorHandler */ const notErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; @@ -21,7 +21,7 @@ const notErrorHandler = { // knows what needs to change. At least one of these needs to be true. const negatedLocalization = localization.negated(); const options = (not.outputs ?? []).flatMap((notOutput) => { - return getSuccesses(notOutput, instance, negatedLocalization, ast); + return getSuccesses(notOutput, instance, negatedLocalization, context); }); if (options.length === 0) { @@ -51,7 +51,7 @@ const notErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; @@ -61,13 +61,13 @@ const notErrorHandler = { for (const notOutput of not.outputs ?? []) { if (localization.isNegated) { // 'not' fails if its schema passes, which requires all of its keywords to pass - const requirements = getSuccesses(notOutput, instance, localization.negated(), ast); + const requirements = getSuccesses(notOutput, instance, localization.negated(), context); successes.push(...allTrue(requirements, instance, schemaLocation, localization)); } 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(), ast); + const options = getSuccesses(notOutput, instance, localization.negated(), context); successes.push(...someTrue(options.map((option) => [option]), instance, schemaLocation, localization)); } } diff --git a/src/error-handlers/oneOf.js b/src/error-handlers/oneOf.js index 0807ebf..5ce7cf0 100644 --- a/src/error-handlers/oneOf.js +++ b/src/error-handlers/oneOf.js @@ -3,14 +3,13 @@ import * as Pact from "@hyperjump/pact"; import { allTrue, allowsAnyValue, countTrue, getCompiledKeywordValue, getErrors, getSuccesses, isPassing, limitItems, limitOptions, someTrue } from "../json-schema-errors.js"; /** - * @import { AST } from "@hyperjump/json-schema/experimental" * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" - * @import { ErrorHandler, ErrorObject, InstanceOutput, Localization, NormalizedOutput } from "../index.d.ts" + * @import { ErrorHandler, ErrorHandlerContext, ErrorObject, InstanceOutput, Localization, NormalizedOutput } from "../index.d.ts" */ /** @type ErrorHandler */ const oneOfErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; @@ -21,12 +20,12 @@ const oneOfErrorHandler = { } const oneOf = oneOfOutput.outputs ?? []; - const alternativeLocations = /** @type string[] */ (getCompiledKeywordValue(ast, schemaLocation)); + const alternativeLocations = /** @type string[] */ (getCompiledKeywordValue(context.ast, schemaLocation)); const matches = alternativeLocations.flatMap((alternativeLocation, index) => { return isPassing(oneOf[index]) ? [{ alternativeLocation, output: oneOf[index] }] : []; }); if (matches.length > 1) { - errors.push(multipleMatchesError(matches, schemaLocation, instance, localization, ast)); + errors.push(multipleMatchesError(matches, schemaLocation, instance, localization, context)); continue; } @@ -63,7 +62,7 @@ const oneOfErrorHandler = { } // The alternative passed all the filters - const alternativeErrors = getErrors(alternative, instance, localization, ast); + const alternativeErrors = getErrors(alternative, instance, localization, context); if (alternativeErrors.length) { alternatives.push(alternativeErrors); } @@ -72,7 +71,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, ast); + const alternativeErrors = getErrors(alternative, instance, localization, context); if (alternativeErrors.length) { alternatives.push(alternativeErrors); } @@ -98,7 +97,7 @@ const oneOfErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; @@ -111,7 +110,7 @@ const oneOfErrorHandler = { // be described even if we know which one matches now. // Make all alternatives fail - const alternativeOptions = alternatives.map((alternative) => getSuccesses(alternative, instance, localization, ast)); + const alternativeOptions = alternatives.map((alternative) => getSuccesses(alternative, instance, localization, context)); if (alternativeOptions.every((alternativeOption) => alternativeOption.length > 0)) { const requirements = alternativeOptions.flatMap((alternativeOption) => { return someTrue(alternativeOption.map((option) => [option]), instance, schemaLocation, localization); @@ -120,7 +119,7 @@ const oneOfErrorHandler = { } // Make at least two alternatives match - const descriptions = alternatives.map((alternative) => getSuccesses(alternative, instance, localization.negated(), ast)); + const descriptions = alternatives.map((alternative) => getSuccesses(alternative, instance, localization.negated(), 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)); @@ -129,7 +128,7 @@ const oneOfErrorHandler = { // 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, ast)); + const descriptions = alternatives.map((alternative) => getSuccesses(alternative, instance, localization, context)); if (descriptions.every((description) => description.length > 0)) { successes.push(...countTrue(descriptions, { min: 1, max: 1 }, instance, schemaLocation, localization)); } @@ -144,12 +143,12 @@ 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, ast: AST) => ErrorObject + * @type (matches: { alternativeLocation: string, output: NormalizedOutput }[], schemaLocation: string, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject */ -const multipleMatchesError = (matches, schemaLocation, instance, localization, ast) => { +const multipleMatchesError = (matches, schemaLocation, instance, localization, context) => { const alternatives = matches.map(({ alternativeLocation, output }) => { - const description = getSuccesses(output, instance, localization, ast); - if (description.length === 0 && allowsAnyValue(alternativeLocation, ast)) { + const description = getSuccesses(output, instance, localization, context); + if (description.length === 0 && allowsAnyValue(alternativeLocation, context.ast)) { return [{ message: localization.getAnyValueMessage(), instanceLocation: Instance.uri(instance), diff --git a/src/error-handlers/pattern.js b/src/error-handlers/pattern.js index 33fedfb..3c69e9e 100644 --- a/src/error-handlers/pattern.js +++ b/src/error-handlers/pattern.js @@ -7,7 +7,7 @@ import { describeKeyword, getCompiledKeywordValue } from "../json-schema-errors. /** @type ErrorHandler */ const patternErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type ErrorObject[] */ const errors = []; @@ -16,7 +16,7 @@ const patternErrorHandler = { continue; } - const compiledPattern = /** @type RegExp */ (getCompiledKeywordValue(ast, schemaLocation)); + const compiledPattern = /** @type RegExp */ (getCompiledKeywordValue(context.ast, schemaLocation)); const pattern = compiledPattern.source; errors.push({ @@ -29,8 +29,8 @@ const patternErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/pattern", instance, ast, (/** @type RegExp */ pattern) => { + 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); }); } diff --git a/src/error-handlers/prefixItems.js b/src/error-handlers/prefixItems.js index 18cc334..59e34e0 100644 --- a/src/error-handlers/prefixItems.js +++ b/src/error-handlers/prefixItems.js @@ -20,7 +20,7 @@ import { * @type ErrorHandler */ const prefixItemsErrorHandler = { - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; @@ -35,7 +35,7 @@ const prefixItemsErrorHandler = { ...Object.keys(normalizedOutput["https://json-schema.org/keyword/draft-04/items"] ?? {}) ]; for (const schemaLocation of schemaLocations) { - const prefixItems = /** @type string | string[] */ (getCompiledKeywordValue(ast, schemaLocation)); + const prefixItems = /** @type string | string[] */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (typeof prefixItems === "string") { // A single schema for all items applies to items that can't be named continue; @@ -45,14 +45,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, ast); + const output = evaluateRequirements(prefixItems[index], item, context.ast); successes.push(...describeConditional({ condition: (localization) => [{ message: localization.getHasItemSuccessMessage(index), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }], - then: (localization) => getSuccesses(output, instance, localization, ast) + then: (localization) => getSuccesses(output, instance, localization, context) }, instance, schemaLocation, localization)); } } diff --git a/src/error-handlers/properties.js b/src/error-handlers/properties.js index bb3ad78..70b7b01 100644 --- a/src/error-handlers/properties.js +++ b/src/error-handlers/properties.js @@ -19,7 +19,7 @@ import { * @type ErrorHandler */ const propertiesErrorHandler = { - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; @@ -30,7 +30,7 @@ const propertiesErrorHandler = { } for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/properties"]) { - const properties = /** @type Record */ (getCompiledKeywordValue(ast, schemaLocation)); + const properties = /** @type Record */ (getCompiledKeywordValue(context.ast, schemaLocation)); const isObject = Instance.typeOf(instance) === "object"; for (const propertyName in properties) { @@ -40,14 +40,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, ast); + const output = evaluateRequirements(properties[propertyName], property, context.ast); successes.push(...describeConditional({ condition: (localization) => [{ message: localization.getHasPropertySuccessMessage([propertyName]), instanceLocation: Instance.uri(instance), schemaLocations: [schemaLocation] }], - then: (localization) => getSuccesses(output, instance, localization, ast) + then: (localization) => getSuccesses(output, instance, localization, context) }, instance, schemaLocation, localization)); } } diff --git a/src/error-handlers/required.js b/src/error-handlers/required.js index 57d5804..0f0b020 100644 --- a/src/error-handlers/required.js +++ b/src/error-handlers/required.js @@ -8,7 +8,7 @@ import { getCompiledKeywordValue } from "../json-schema-errors.js"; /** @type ErrorHandler */ const requiredErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, context) => { /** @type {Set} */ const allMissingRequired = new Set(); const allSchemaLocations = []; @@ -19,7 +19,7 @@ const requiredErrorHandler = { } allSchemaLocations.push(schemaLocation); - const required = /** @type string[] */ (getCompiledKeywordValue(ast, schemaLocation)); + const required = /** @type string[] */ (getCompiledKeywordValue(context.ast, schemaLocation)); addMissingProperties(required, instance, allMissingRequired); } @@ -30,7 +30,7 @@ const requiredErrorHandler = { } allSchemaLocations.push(schemaLocation); - const dependencies = /** @type {[string, string[]][]} */ (getCompiledKeywordValue(ast, schemaLocation)); + const dependencies = /** @type {[string, string[]][]} */ (getCompiledKeywordValue(context.ast, schemaLocation)); for (const [propertyName, requiredProperties] of dependencies) { if (!Instance.has(propertyName, instance)) { @@ -45,7 +45,7 @@ const requiredErrorHandler = { continue; } - const dependencies = /** @type {[string, unknown][]} */ (getCompiledKeywordValue(ast, schemaLocation)); + const dependencies = /** @type {[string, unknown][]} */ (getCompiledKeywordValue(context.ast, schemaLocation)); let hasArrayFormDependencies = false; for (const [propertyName, dependency] of dependencies) { @@ -74,7 +74,7 @@ const requiredErrorHandler = { }]; }, - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { /** @type ErrorObject[] */ const successes = []; @@ -82,7 +82,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(ast, schemaLocation)); + const required = /** @type string[] */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (required.length) { requiredSchemaLocations.push(schemaLocation); addAll(required, allRequired); @@ -98,7 +98,7 @@ const requiredErrorHandler = { } for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/dependentRequired"]) { - const dependencies = /** @type {[string, string[]][]} */ (getCompiledKeywordValue(ast, schemaLocation)); + const dependencies = /** @type {[string, string[]][]} */ (getCompiledKeywordValue(context.ast, schemaLocation)); for (const [propertyName, requiredProperties] of dependencies) { if (requiredProperties.length > 0) { successes.push({ @@ -111,7 +111,7 @@ const requiredErrorHandler = { } for (const schemaLocation in normalizedOutput["https://json-schema.org/keyword/draft-04/dependencies"]) { - const dependencies = /** @type {[string, unknown][]} */ (getCompiledKeywordValue(ast, schemaLocation)); + const dependencies = /** @type {[string, unknown][]} */ (getCompiledKeywordValue(context.ast, schemaLocation)); for (const [propertyName, dependency] of dependencies) { if (Array.isArray(dependency) && dependency.length > 0) { successes.push({ diff --git a/src/error-handlers/typeConstEnum.js b/src/error-handlers/typeConstEnum.js index 7abf40e..7ba8c34 100644 --- a/src/error-handlers/typeConstEnum.js +++ b/src/error-handlers/typeConstEnum.js @@ -9,7 +9,7 @@ const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "obje /** @type {ErrorHandler} */ const typeConstEnumErrorHandler = { - error: (normalizedErrors, instance, localization, ast) => { + error: (normalizedErrors, instance, localization, 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(ast, schemaLocation)); + const value = /** @type {string | string[]} */ (getCompiledKeywordValue(context.ast, schemaLocation)); 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(ast, schemaLocation)); + const constValueJson = /** @type string */ (getCompiledKeywordValue(context.ast, schemaLocation)); if (allowedTypes.has(jsonTypeOf(constValueJson))) { keywordJson.add(constValueJson); } else { @@ -68,7 +68,7 @@ const typeConstEnumErrorHandler = { } const keywordJson = new Set(); - const enumValuesJson = /** @type string[] */ (getCompiledKeywordValue(ast, schemaLocation)); + const enumValuesJson = /** @type string[] */ (getCompiledKeywordValue(context.ast, schemaLocation)); for (const enumValueJson of enumValuesJson) { if (allowedTypes.has(jsonTypeOf(enumValueJson))) { keywordJson.add(enumValueJson); @@ -112,15 +112,15 @@ const typeConstEnumErrorHandler = { } }, - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { return [ - ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/type", instance, ast, (/** @type string | string[] */ type) => { + ...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/const", instance, ast, (/** @type string */ constJson) => { + ...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/enum", instance, ast, (/** @type string[] */ enumJson) => { + ...describeKeyword(normalizedOutput, "https://json-schema.org/keyword/enum", instance, context.ast, (/** @type string[] */ enumJson) => { return localization.getEnumSuccessMessage(enumJson.map((json) => JSON.parse(json))); }) ]; diff --git a/src/error-handlers/uniqueItems.js b/src/error-handlers/uniqueItems.js index 8b90796..02c9313 100644 --- a/src/error-handlers/uniqueItems.js +++ b/src/error-handlers/uniqueItems.js @@ -42,9 +42,9 @@ const uniqueItemsErrorHandler = { return errors; }, - success: (normalizedOutput, instance, localization, ast) => { + success: (normalizedOutput, instance, localization, context) => { // 'uniqueItems: false' allows anything, so there's nothing to say - return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/uniqueItems", instance, ast, (/** @type boolean */ uniqueItems) => { + return describeKeyword(normalizedOutput, "https://json-schema.org/keyword/uniqueItems", instance, context.ast, (/** @type boolean */ uniqueItems) => { return uniqueItems ? localization.getUniqueItemsSuccessMessage() : undefined; }); } diff --git a/src/format.test.js b/src/format.test.js new file mode 100644 index 0000000..e35676d --- /dev/null +++ b/src/format.test.js @@ -0,0 +1,120 @@ +import { afterEach, describe, expect, test } from "vitest"; +import { registerSchema, setShouldValidateFormat, unregisterSchema, validate } from "@hyperjump/json-schema/draft-2020-12"; +import { BASIC } from "@hyperjump/json-schema/experimental"; +import "@hyperjump/json-schema/draft-07"; +import "@hyperjump/json-schema"; +import "@hyperjump/json-schema/formats"; +import { JSE, jsonSchemaErrors } from "./index.js"; + +/** + * @import { ErrorObject } from "./index.js" + */ + +/** + * Whether 'format' asserts depends on whether formats are validated, so these + * can't be covered by the test suite, which always validates formats. + */ +describe("format descriptions", () => { + const schemaUri = "https://example.com/main"; + const formatAssertionDialectUri = "https://example.com/format-assertion"; + + registerSchema({ + $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/format-assertion": true + }, + $dynamicAnchor: "meta", + allOf: [ + { $ref: "https://json-schema.org/draft/2020-12/meta/core" }, + { $ref: "https://json-schema.org/draft/2020-12/meta/applicator" }, + { $ref: "https://json-schema.org/draft/2020-12/meta/format-assertion" } + ] + }, formatAssertionDialectUri); + + afterEach(() => { + unregisterSchema(schemaUri); + setShouldValidateFormat(undefined); + }); + + const dialectUri = "https://json-schema.org/draft/2020-12/schema"; + + /** @type (errors: ErrorObject[]) => string[] */ + const messages = (errors) => [errors[0], ...(errors[0].alternatives ?? []).flat()].map((error) => error.message); + + /** @type (dialectUri: string, format?: string) => Promise */ + const describeNotFormat = async (dialectUri, format = "email") => { + registerSchema({ not: { format } }, schemaUri, dialectUri); + const output = await validate(schemaUri, "a@example.com", JSE); + if (output.valid) { + throw Error("Expected validation to fail"); + } + return messages(output.errors); + }; + + const described = [ + "Expected the following to be true", + "The value is a string that doesn't match the 'email' format" + ]; + const notDescribed = ["A value is not allowed here"]; + + test("2020-12 format isn't validated by default", async () => { + expect(await describeNotFormat(dialectUri)).to.eql(notDescribed); + }); + + test("2020-12 format is validated if turned on", async () => { + setShouldValidateFormat(true); + expect(await describeNotFormat(dialectUri)).to.eql(described); + }); + + test("draft-07 format is validated by default", async () => { + expect(await describeNotFormat("http://json-schema.org/draft-07/schema")).to.eql(described); + }); + + test("draft-07 format isn't validated if turned off", async () => { + setShouldValidateFormat(false); + expect(await describeNotFormat("http://json-schema.org/draft-07/schema")).to.eql(notDescribed); + }); + + test("2020-12 format-assertion is validated even if turned off", async () => { + setShouldValidateFormat(false); + expect(await describeNotFormat(formatAssertionDialectUri)).to.eql(described); + }); + + test("v1 format is validated by default", async () => { + expect(await describeNotFormat("https://json-schema.org/v1")).to.eql(described); + }); + + test("v1 format isn't validated if turned off", async () => { + setShouldValidateFormat(false); + expect(await describeNotFormat("https://json-schema.org/v1")).to.eql(notDescribed); + }); + + test("unsupported formats aren't validated", async () => { + setShouldValidateFormat(true); + expect(await describeNotFormat(dialectUri, "unsupported")).to.eql(notDescribed); + }); + + test("jsonSchemaErrors describes format as only applying if formats are validated if that's not known", async () => { + setShouldValidateFormat(true); + registerSchema({ not: { format: "email" } }, schemaUri, dialectUri); + const output = await validate(schemaUri, "a@example.com", BASIC); + const errors = await jsonSchemaErrors(output, schemaUri, "a@example.com"); + expect(messages(errors)).to.eql([ + "Expected the following to be true", + "The value is a string that doesn't match the 'email' format (if formats are validated)" + ]); + }); + + test("jsonSchemaErrors describes format if the validator validates formats", async () => { + setShouldValidateFormat(true); + registerSchema({ not: { format: "email" } }, schemaUri, dialectUri); + const output = await validate(schemaUri, "a@example.com", BASIC); + const errors = await jsonSchemaErrors(output, schemaUri, "a@example.com", { isFormatAsserted: true }); + expect(messages(errors)).to.eql([ + "Expected the following to be true", + "The value is a string that doesn't match the 'email' format" + ]); + }); +}); diff --git a/src/hyperjump-json-schema.test.js b/src/hyperjump-json-schema.test.js index 8749e74..5a89947 100644 --- a/src/hyperjump-json-schema.test.js +++ b/src/hyperjump-json-schema.test.js @@ -83,7 +83,7 @@ const runTests = (dialectUri, dialect) => { const instance = testCase.instance; const output = await validate(schemaUri, instance, BASIC); - const errors = await jsonSchemaErrors(output, schemaUri, instance); + const errors = await jsonSchemaErrors(output, schemaUri, instance, { isFormatAsserted: true }); expect(errors).to.eql(buildErrors(testCase.errors, schemaUri)); expectNestedLocations(errors); diff --git a/src/index.d.ts b/src/index.d.ts index e1ed76e..53cbc20 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -57,6 +57,14 @@ export type JsonSchemaErrorsOptions = { * @example "en-US" */ locale?: string; + + /** + * Whether the validator treated the `format` keyword as an assertion rather + * than an annotation. Validators often only validate `format` if they're + * configured to. If this isn't given, messages that describe `format` say + * that it only applies if formats are validated. + */ + isFormatAsserted?: boolean; }; /** @@ -189,22 +197,36 @@ 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, ast: AST) => ErrorObject[]; - success?: (normalizedOutput: InstanceOutput, instance: JsonNode, localization: Localization, ast: AST) => ErrorObject[]; + error?: (normalizedErrors: InstanceOutput, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[]; + success?: (normalizedOutput: InstanceOutput, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[]; +}; + +/** + * What error handlers need to know about the schema and how it was validated. + */ +export type ErrorHandlerContext = { + ast: AST; + + /** + * Whether the validator treated a `format` keyword as an assertion rather + * than an annotation. It can depend on the dialect, given by the keyword + * URI, and on the format. `undefined` if it's not known. + */ + isFormatAsserted: (keywordUri: string, format: string) => 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, ast: AST) => ErrorObject[]; +export const getErrors: (normalizedErrors: NormalizedOutput, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[]; /** * Converts the normalized output of a passing subschema to human readable * messages describing how the instance satisfied the subschema. It's used to * build errors in applicator error handlers that fail when a subschema passes. */ -export const getSuccesses: (normalizedOutput: NormalizedOutput, instance: JsonNode, localization: Localization, ast: AST) => ErrorObject[]; +export const getSuccesses: (normalizedOutput: NormalizedOutput, instance: JsonNode, localization: Localization, context: ErrorHandlerContext) => ErrorObject[]; export type { Localization }; diff --git a/src/index.js b/src/index.js index cb497e5..47e4c6f 100644 --- a/src/index.js +++ b/src/index.js @@ -108,6 +108,7 @@ setNormalizationHandler("https://json-schema.org/keyword/draft-2020-12/dynamicRe 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); diff --git a/src/json-schema-errors.js b/src/json-schema-errors.js index 94a7d63..90ab9de 100644 --- a/src/json-schema-errors.js +++ b/src/json-schema-errors.js @@ -8,7 +8,7 @@ import { Localization } from "./localization.js"; /** * @import * as API from "./index.d.ts" * @import { Browser } from "@hyperjump/browser"; - * @import { AST, SchemaDocument, CompiledSchema, Node } from "@hyperjump/json-schema/experimental"; + * @import { AST, SchemaDocument, Node } from "@hyperjump/json-schema/experimental"; * @import { JsonNode } from "@hyperjump/json-schema/instance/experimental" */ @@ -24,7 +24,10 @@ export const jsonSchemaErrors = async (errorOutput, schemaUri, instance, options plugins: [...ast.plugins] }); const localization = Localization.forLocale(options.locale ?? "en-US"); - return getErrors(normalizedErrors, rootInstance, localization, ast); + return getErrors(normalizedErrors, rootInstance, localization, { + ast, + isFormatAsserted: () => options.isFormatAsserted + }); }; /** @type Record */ @@ -276,11 +279,11 @@ export const evaluateRequirements = (schemaLocation, instance, ast) => { * 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, ast: AST) => API.ErrorObject[] + * @type (subschemaLocation: string, placeholder: JsonNode, parent: JsonNode, localization: Localization, context: API.ErrorHandlerContext) => API.ErrorObject[] */ -export const describeEach = (subschemaLocation, placeholder, parent, localization, ast) => { - const output = evaluateRequirements(subschemaLocation, placeholder, ast); - return getSuccesses(output, parent, localization, ast) +export const describeEach = (subschemaLocation, placeholder, parent, localization, context) => { + const output = evaluateRequirements(subschemaLocation, placeholder, context.ast); + return getSuccesses(output, parent, localization, context) .map((success) => relocate(success, Instance.uri(placeholder), Instance.uri(parent))); }; @@ -300,10 +303,10 @@ 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, ast: AST) => API.ErrorObject[] + * @type (scope: Scope, instance: JsonNode, schemaLocation: string, localization: Localization, context: API.ErrorHandlerContext) => API.ErrorObject[] */ -export const describeScope = ({ subschemaLocation, placeholder, each, none }, instance, schemaLocation, localization, ast) => { - if (ast[subschemaLocation] === false) { +export const describeScope = ({ subschemaLocation, placeholder, each, none }, instance, schemaLocation, localization, context) => { + if (context.ast[subschemaLocation] === false) { return [{ message: none(localization), instanceLocation: Instance.uri(instance), @@ -311,7 +314,7 @@ export const describeScope = ({ subschemaLocation, placeholder, each, none }, in }]; } - const description = describeEach(subschemaLocation, placeholder, instance, localization, ast); + const description = describeEach(subschemaLocation, placeholder, instance, localization, context); if (description.length === 0) { return []; } @@ -428,14 +431,14 @@ export const removeErrorHandler = (errorHandlerUri) => { }; /** @type API.getErrors */ -export const getErrors = (normalizedErrors, rootInstance, localization, ast) => { +export const getErrors = (normalizedErrors, rootInstance, localization, context) => { /** @type API.ErrorObject[] */ const errors = []; for (const instanceLocation in normalizedErrors) { const instance = /** @type JsonNode */ (Instance.get(instanceLocation, rootInstance)); for (const errorHandlerUri in errorHandlers) { - const errorObjects = errorHandlers[errorHandlerUri].error?.(normalizedErrors[instanceLocation], instance, localization, ast) ?? []; + const errorObjects = errorHandlers[errorHandlerUri].error?.(normalizedErrors[instanceLocation], instance, localization, context) ?? []; errors.push(...errorObjects); } } @@ -501,7 +504,7 @@ const withoutConditionalResults = (normalizedOutput) => { const resultKey = (instanceLocation, keywordUri, keywordLocation) => JSON.stringify([instanceLocation, keywordUri, keywordLocation]); /** @type API.getSuccesses */ -export const getSuccesses = (normalizedOutput, rootInstance, localization, ast) => { +export const getSuccesses = (normalizedOutput, rootInstance, localization, 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) { @@ -527,7 +530,7 @@ export const getSuccesses = (normalizedOutput, rootInstance, localization, ast) const instance = getInstance(instanceLocation, rootInstance) ?? toPlaceholder(instanceLocation, rootInstance); for (const errorHandlerUri in errorHandlers) { - const successObjects = errorHandlers[errorHandlerUri].success?.(describedOutput[instanceLocation], instance, localization, ast) ?? []; + const successObjects = errorHandlers[errorHandlerUri].success?.(describedOutput[instanceLocation], instance, localization, context) ?? []; successes.push(...successObjects); } } diff --git a/src/localization.js b/src/localization.js index 0045659..de28e86 100644 --- a/src/localization.js +++ b/src/localization.js @@ -351,6 +351,11 @@ export class Localization { return this.#formatSuccessMessage("format", { format }); } + /** @type (format: string) => string */ + getFormatIfValidatedSuccessMessage(format) { + return this.#formatSuccessMessage("formatIfValidated", { format }); + } + /** @type (maxItems: number) => string */ getMaxItemsSuccessMessage(maxItems) { return this.#formatSuccessMessage("maxItems", { maxItems }); diff --git a/src/output-format.js b/src/output-format.js index e78ed32..e643363 100644 --- a/src/output-format.js +++ b/src/output-format.js @@ -1,4 +1,5 @@ -import { setOutputFormat } from "@hyperjump/json-schema/experimental"; +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 { Localization } from "./localization.js"; import { JsonSchemaErrorsOutputPlugin } from "./output-plugin.js"; @@ -16,9 +17,50 @@ const jseOutputFormatHandler = (options) => { plugin, getErrors: (instance, context) => { const localization = Localization.forLocale(options.locale ?? "en-US"); - return getErrors(plugin.output, instance, localization, context.ast); + return getErrors(plugin.output, instance, localization, { + ast: context.ast, + isFormatAsserted + }); } }; }; +/** + * Whether @hyperjump/json-schema validates 'format' depends on the dialect and + * on whether format validation is turned on or off. If it's not set, each + * dialect has its own default. + * + * @type (keywordUri: string, format: string) => boolean | undefined + */ +const isFormatAsserted = (keywordUri, format) => { + const shouldValidateFormat = getShouldValidateFormat(); + + switch (keywordUri) { + case "https://json-schema.org/keyword/draft-2020-12/format-assertion": + // Always validated. Validation throws if a format isn't supported. + return true; + case "https://json-schema.org/keyword/format": + case "https://json-schema.org/keyword/draft-2019-09/format-assertion": + case "https://json-schema.org/keyword/draft-07/format": + case "https://json-schema.org/keyword/draft-06/format": + case "https://json-schema.org/keyword/draft-04/format": + if (shouldValidateFormat === false) { + return false; + } + break; + case "https://json-schema.org/keyword/draft-2020-12/format": + case "https://json-schema.org/keyword/draft-2019-09/format": + if (shouldValidateFormat !== true) { + return false; + } + break; + default: + return undefined; + } + + // Formats that aren't supported always pass + const { formats } = /** @type {{ formats: Record }} */ (/** @type unknown */ (getKeyword(keywordUri))); + return Object.hasOwn(formats, format); +}; + setOutputFormat(JSE, jseOutputFormatHandler); diff --git a/src/translations/en-US.js b/src/translations/en-US.js index 7931b81..846e1a5 100644 --- a/src/translations/en-US.js +++ b/src/translations/en-US.js @@ -18,8 +18,10 @@ enum-message = Expected one of {$expected} enum-success-message = The value is one of {$expected} enum-negated-message = The value is not one of {$expected} format-message = Expected a value matching the '{$format}' format -format-success-message = The value is either not a string or matches the '{$format}' format (if formats are validated) -format-negated-message = The value is a string that doesn't match the '{$format}' format (if formats are validated) +format-success-message = The value is either not a string or matches the '{$format}' format +format-negated-message = The value is a string that doesn't match the '{$format}' format +formatIfValidated-success-message = The value is either not a string or matches the '{$format}' format (if formats are validated) +formatIfValidated-negated-message = The value is a string that doesn't match the '{$format}' format (if formats are validated) unknown-message = Validation failed for '{$keyword}' type-success-message = The value is {$count -> [one] {$type ->