From 41801eec085fc973f71495577e5473f6e6574f4a Mon Sep 17 00:00:00 2001 From: Arpit Kuriyal Date: Tue, 12 Aug 2025 18:16:48 +0530 Subject: [PATCH 1/3] added string-handler --- src/error-handlers/maxLength.js | 30 ------------- src/error-handlers/minLength.js | 30 ------------- src/error-handlers/pattern.js | 29 ------------- src/error-handlers/string-handler.js | 64 ++++++++++++++++++++++++++++ src/index.js | 8 +--- src/keyword-error-message.test.js | 59 +++++++++++++++++++++---- src/localization.js | 45 ++++++++++++------- src/normalized-output.test.js | 8 ++-- src/translations/en-US.ftl | 10 +++-- 9 files changed, 157 insertions(+), 126 deletions(-) delete mode 100644 src/error-handlers/maxLength.js delete mode 100644 src/error-handlers/minLength.js delete mode 100644 src/error-handlers/pattern.js create mode 100644 src/error-handlers/string-handler.js diff --git a/src/error-handlers/maxLength.js b/src/error-handlers/maxLength.js deleted file mode 100644 index dc178f0..0000000 --- a/src/error-handlers/maxLength.js +++ /dev/null @@ -1,30 +0,0 @@ -import { getSchema } from "@hyperjump/json-schema/experimental"; -import * as Schema from "@hyperjump/browser"; -import * as Instance from "@hyperjump/json-schema/instance/experimental"; - -/** - * @import { ErrorHandler, ErrorObject } from "../index.d.ts" - */ - -/** @type ErrorHandler */ -const maxLength = async (normalizedErrors, instance, localization) => { - /** @type ErrorObject[] */ - const errors = []; - - if (normalizedErrors["https://json-schema.org/keyword/maxLength"]) { - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/maxLength"]) { - if (!normalizedErrors["https://json-schema.org/keyword/maxLength"][schemaLocation]) { - const keyword = await getSchema(schemaLocation); - errors.push({ - message: localization.getMaxLengthErrorMessage(Schema.value(keyword)), - instanceLocation: Instance.uri(instance), - schemaLocation: schemaLocation - }); - } - } - } - - return errors; -}; - -export default maxLength; diff --git a/src/error-handlers/minLength.js b/src/error-handlers/minLength.js deleted file mode 100644 index 83ebc51..0000000 --- a/src/error-handlers/minLength.js +++ /dev/null @@ -1,30 +0,0 @@ -import { getSchema } from "@hyperjump/json-schema/experimental"; -import * as Schema from "@hyperjump/browser"; -import * as Instance from "@hyperjump/json-schema/instance/experimental"; - -/** - * @import { ErrorHandler, ErrorObject } from "../index.d.ts" - */ - -/** @type ErrorHandler */ -const minLength = async (normalizedErrors, instance, localization) => { - /** @type ErrorObject[] */ - const errors = []; - - if (normalizedErrors["https://json-schema.org/keyword/minLength"]) { - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/minLength"]) { - if (!normalizedErrors["https://json-schema.org/keyword/minLength"][schemaLocation]) { - const keyword = await getSchema(schemaLocation); - errors.push({ - message: localization.getMinLengthErrorMessage(Schema.value(keyword)), - instanceLocation: Instance.uri(instance), - schemaLocation: schemaLocation - }); - } - } - } - - return errors; -}; - -export default minLength; diff --git a/src/error-handlers/pattern.js b/src/error-handlers/pattern.js deleted file mode 100644 index e2a8513..0000000 --- a/src/error-handlers/pattern.js +++ /dev/null @@ -1,29 +0,0 @@ -import { getSchema } from "@hyperjump/json-schema/experimental"; -import * as Schema from "@hyperjump/browser"; -import * as Instance from "@hyperjump/json-schema/instance/experimental"; - -/** - * @import { ErrorHandler, ErrorObject } from "../index.d.ts" - */ - -/** @type ErrorHandler */ -const pattern = async (normalizedErrors, instance, localization) => { - /** @type ErrorObject[] */ - const errors = []; - - if (normalizedErrors["https://json-schema.org/keyword/pattern"]) { - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/pattern"]) { - if (!normalizedErrors["https://json-schema.org/keyword/pattern"][schemaLocation]) { - const keyword = await getSchema(schemaLocation); - errors.push({ - message: localization.getPatternErrorMessage(Schema.value(keyword)), - instanceLocation: Instance.uri(instance), - schemaLocation: schemaLocation - }); - } - } - } - - return errors; -}; -export default pattern; diff --git a/src/error-handlers/string-handler.js b/src/error-handlers/string-handler.js new file mode 100644 index 0000000..4e83b15 --- /dev/null +++ b/src/error-handlers/string-handler.js @@ -0,0 +1,64 @@ +import { getSchema } from "@hyperjump/json-schema/experimental"; +import * as Schema from "@hyperjump/browser"; +import * as Instance from "@hyperjump/json-schema/instance/experimental"; + +/** + * @import { StringConstraints } from "../localization.js" + * @import { ErrorHandler } from "../index.d.ts" + */ + +/** @type ErrorHandler */ +const stringHandler = async (normalizedErrors, instance, localization) => { + /** @type StringConstraints */ + const constraints = {}; + + /** @type string[] */ + const failedSchemaLocations = []; + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/minLength"]) { + if (!normalizedErrors["https://json-schema.org/keyword/minLength"][schemaLocation]) { + failedSchemaLocations.push(schemaLocation); + } + + const keyword = await getSchema(schemaLocation); + /** @type number */ + const minLength = Schema.value(keyword); + constraints.minLength = Math.max(constraints.minLength ?? Number.MIN_VALUE, minLength); + } + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/maxLength"]) { + if (!normalizedErrors["https://json-schema.org/keyword/maxLength"][schemaLocation]) { + failedSchemaLocations.push(schemaLocation); + } + + const keyword = await getSchema(schemaLocation); + /** @type number */ + const maxLength = Schema.value(keyword); + constraints.maxLength = Math.min(constraints.maxLength ?? Number.MAX_VALUE, maxLength); + } + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/pattern"]) { + if (!normalizedErrors["https://json-schema.org/keyword/pattern"][schemaLocation]) { + failedSchemaLocations.push(schemaLocation); + } + + const keyword = await getSchema(schemaLocation); + /** @type string */ + const pattern = Schema.value(keyword); + constraints.pattern = pattern; + } + + if (failedSchemaLocations.length > 0) { + return [ + { + message: localization.getStringErrorMessage(constraints), + instanceLocation: Instance.uri(instance), + schemaLocation: failedSchemaLocations.length > 1 ? failedSchemaLocations : failedSchemaLocations[0] + } + ]; + } + + return []; +}; + +export default stringHandler; diff --git a/src/index.js b/src/index.js index e10be68..2f0103b 100644 --- a/src/index.js +++ b/src/index.js @@ -55,15 +55,13 @@ import maxItemsErrorHandler from "./error-handlers/maxItems.js"; import minItemsErrorHandler from "./error-handlers/minItems.js"; import maxPropertiesErrorHandler from "./error-handlers/maxProperties.js"; import minPropertiesErrorHandler from "./error-handlers/minProperties.js"; -import minLengthErrorHandler from "./error-handlers/minLength.js"; import multipleOfErrorHandler from "./error-handlers/multipleOf.js"; import notErrorHandler from "./error-handlers/not.js"; import numberRangeHandler from "./error-handlers/number-range-handler.js"; -import patternErrorHandler from "./error-handlers/pattern.js"; import requiredErrorHandler from "./error-handlers/required.js"; import typeErrorHandler from "./error-handlers/type.js"; import uniqueItemsErrorHandler from "./error-handlers/uniqueItems.js"; -import maxLengthErrorHandler from "./error-handlers/maxLength.js"; +import stringHandler from "./error-handlers/string-handler.js"; /** * @import { betterJsonSchemaErrors } from "./index.d.ts" @@ -127,15 +125,13 @@ addErrorHandler(maxItemsErrorHandler); addErrorHandler(minItemsErrorHandler); addErrorHandler(maxPropertiesErrorHandler); addErrorHandler(minPropertiesErrorHandler); -addErrorHandler(minLengthErrorHandler); -addErrorHandler(maxLengthErrorHandler); addErrorHandler(multipleOfErrorHandler); addErrorHandler(notErrorHandler); addErrorHandler(numberRangeHandler); -addErrorHandler(patternErrorHandler); addErrorHandler(requiredErrorHandler); addErrorHandler(typeErrorHandler); addErrorHandler(uniqueItemsErrorHandler); +addErrorHandler(stringHandler); export { setNormalizationHandler } from "./normalized-output.js"; export { addErrorHandler } from "./error-handling.js"; diff --git a/src/keyword-error-message.test.js b/src/keyword-error-message.test.js index 10d390a..6035aaa 100644 --- a/src/keyword-error-message.test.js +++ b/src/keyword-error-message.test.js @@ -39,7 +39,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", - message: localization.getMinLengthErrorMessage(3) + message: localization.getStringErrorMessage({ minLength: 3 }) }]); }); @@ -66,7 +66,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/maxLength", instanceLocation: "#", - message: localization.getMaxLengthErrorMessage(3) + message: localization.getStringErrorMessage({ maxLength: 3 }) }]); }); @@ -614,7 +614,7 @@ describe("Error messages", async () => { { schemaLocation: "https://example.com/main#/pattern", instanceLocation: "#", - message: localization.getPatternErrorMessage("^[a-z]+$") + message: localization.getStringErrorMessage({ pattern: "^[a-z]+$" }) } ]); }); @@ -822,7 +822,7 @@ describe("Error messages", async () => { { schemaLocation: `https://example.com/main#/anyOf/0/minLength`, instanceLocation: "#", - message: localization.getMinLengthErrorMessage(5) + message: localization.getStringErrorMessage({ minLength: 5 }) } ]); }); @@ -883,7 +883,7 @@ describe("Error messages", async () => { { schemaLocation: "https://example.com/main#/anyOf/1/properties/ID/pattern", instanceLocation: "#/ID", - message: localization.getPatternErrorMessage("^[0-9\\-]+$") + message: localization.getStringErrorMessage({ pattern: "^[0-9\\-]+$" }) } ]); }); @@ -1229,7 +1229,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#/Foo", - message: localization.getPatternErrorMessage("^[a-z]*$"), + message: localization.getStringErrorMessage({ pattern: "^[a-z]*$" }), schemaLocation: "https://example.com/main#/propertyNames/pattern" } ]); @@ -1256,7 +1256,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#/Foo", - message: localization.getPatternErrorMessage("^[a-z]*$"), + message: localization.getStringErrorMessage({ pattern: "^[a-z]*$" }), schemaLocation: "https://example.com/main#/propertyNames/pattern" } ]); @@ -1283,7 +1283,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#*/Foo", - message: localization.getPatternErrorMessage("^[a-z]*$"), + message: localization.getStringErrorMessage({ pattern: "^[a-z]*$" }), schemaLocation: "https://example.com/main#/propertyNames/pattern" } ]); @@ -1371,4 +1371,47 @@ describe("Error messages", async () => { } ]); }); + + test("minLength/maxLength and pattern test", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + allOf: [ + { minLength: 3 }, + { maxLength: 5 }, + { pattern: "^[a-z]+$" } + ] + }, schemaUri); + + const instance = "AAAAAAA"; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/allOf/0/minLength", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "https://example.com/main#/allOf/1/maxLength", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "https://example.com/main#/allOf/2/pattern", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(output, schemaUri, instance); + expect(result.errors).to.eql([{ + schemaLocation: [ + "https://example.com/main#/allOf/0/minLength", + "https://example.com/main#/allOf/1/maxLength", + "https://example.com/main#/allOf/2/pattern" + ], + instanceLocation: "#", + message: localization.getStringErrorMessage({ minLength: 3, maxLength: 5, pattern: "^[a-z]+$" }) + }]); + }); }); diff --git a/src/localization.js b/src/localization.js index 7093587..cf9094f 100644 --- a/src/localization.js +++ b/src/localization.js @@ -14,6 +14,14 @@ import { FluentBundle, FluentResource } from "@fluent/bundle"; * }} NumberConstraints */ +/** + * @typedef {{ + * minLength?: number; + * maxLength?: number; + * pattern?: string; + * }} StringConstraints + */ + export class Localization { /** * @param {string} locale @@ -65,16 +73,6 @@ export class Localization { }); } - /** @type (limit: number) => string */ - getMinLengthErrorMessage(limit) { - return this._formatMessage("min-length-error", { limit }); - } - - /** @type (limit: number) => string */ - getMaxLengthErrorMessage(limit) { - return this._formatMessage("max-length-error", { limit }); - } - /** @type (constraints: NumberConstraints) => string */ getNumberErrorMessage(constraints) { /** @type string[] */ @@ -101,6 +99,28 @@ export class Localization { }); } + /** @type (constraints: StringConstraints) => string */ + getStringErrorMessage(constraints) { + /** @type string[] */ + const messages = []; + + if (constraints.minLength) { + messages.push(this._formatMessage("string-error-minLength", constraints)); + } + + if (constraints.maxLength) { + messages.push(this._formatMessage("string-error-maxLength", constraints)); + } + + if (constraints.pattern) { + messages.push(this._formatMessage("string-error-pattern", constraints)); + } + + return this._formatMessage("string-error", { + constraints: new Intl.ListFormat(this.locale, { type: "conjunction" }).format(messages) + }); + } + /** @type (instanceLocation: string, missingProperties: string[]) => string */ getRequiredErrorMessage(instanceLocation, missingProperties) { return this._formatMessage("required-error", { @@ -149,11 +169,6 @@ export class Localization { return this._formatMessage("format-error", { format }); } - /** @type (pattern: string) => string */ - getPatternErrorMessage(pattern) { - return this._formatMessage("pattern-error", { pattern }); - } - /** @type () => string */ getContainsErrorMessage() { return this._formatMessage("contains-error"); diff --git a/src/normalized-output.test.js b/src/normalized-output.test.js index e15d9b5..7ca583b 100644 --- a/src/normalized-output.test.js +++ b/src/normalized-output.test.js @@ -41,7 +41,7 @@ describe("Error Output Normalization", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", - message: localization.getMinLengthErrorMessage(3) + message: localization.getStringErrorMessage({ minLength: 3 }) } ]); }); @@ -69,7 +69,7 @@ describe("Error Output Normalization", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", - message: localization.getMinLengthErrorMessage(3) + message: localization.getStringErrorMessage({ minLength: 3 }) }]); }); @@ -96,7 +96,7 @@ describe("Error Output Normalization", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", - message: localization.getMinLengthErrorMessage(3) + message: localization.getStringErrorMessage({ minLength: 3 }) }]); }); @@ -370,7 +370,7 @@ describe("Error Output Normalization", async () => { { schemaLocation: "https://example.com/main#/$defs/lengthDefinition/minLength", instanceLocation: "#/foo", - message: localization.getMinLengthErrorMessage(3) + message: localization.getStringErrorMessage({ minLength: 3 }) } ]); }); diff --git a/src/translations/en-US.ftl b/src/translations/en-US.ftl index cc4c136..9759d51 100644 --- a/src/translations/en-US.ftl +++ b/src/translations/en-US.ftl @@ -1,11 +1,14 @@ type-error = The instance should be of type {$expected} but found {$actual}. -min-length-error = The instance should be atleast {$limit} characters. -max-length-error = The instance should be atmost {$limit} characters long. + +string-error = Expected a string {$constraints}. +string-error-minLength = atleast {$minLength} characters long +string-error-maxLength = atmost {$maxLength} characters long +string-error-pattern = to match the pattern {$pattern} number-error = Expected a number {$constraints}. number-error-minimum = greater than {$minimum} number-error-exclusive-minimum = greater than or equal to {$minimum} -number-error-maximum = greater than {$maximum} +number-error-maximum = less than {$maximum} number-error-exclusive-maximum = less than or equal to {$maximum} required-error = "{$instanceLocation}" is missing required property(s): {$missingProperties}. @@ -17,7 +20,6 @@ max-items-error = The instance should contain a maximum of {$limit} items in the min-items-error = The instance should contain a minimum of {$limit} items in the array. unique-items-error = The instance should have unique items in the array. format-error = The instance should match the format: {$format}. -pattern-error = The instance should match the pattern: {$pattern}. contains-error = A required value is missing from the list. not-error = The instance is not allowed to be used in this schema. additional-properties-error = The property "{$propertyName}" is not allowed. From 3e4e441d9d147a8ec5b3c978cb2a65e7fad33c71 Mon Sep 17 00:00:00 2001 From: Arpit Kuriyal Date: Tue, 12 Aug 2025 22:23:14 +0530 Subject: [PATCH 2/3] refactor contains errorHandler --- src/error-handlers/contains.js | 24 ++++- src/keyword-error-message.test.js | 130 ++++++++++++++++++++++++- src/localization.js | 17 +++- src/normalization-handlers/contains.js | 1 - src/translations/en-US.ftl | 15 ++- 5 files changed, 177 insertions(+), 10 deletions(-) diff --git a/src/error-handlers/contains.js b/src/error-handlers/contains.js index fb77047..3449f0d 100644 --- a/src/error-handlers/contains.js +++ b/src/error-handlers/contains.js @@ -1,7 +1,11 @@ import * as Instance from "@hyperjump/json-schema/instance/experimental"; +import { getSchema } from "@hyperjump/json-schema/experimental"; +import * as Schema from "@hyperjump/browser"; +import * as JsonPointer from "@hyperjump/json-pointer"; import { getErrors } from "../error-handling.js"; /** + * @import { ContainsConstraints } from "../localization.js" * @import { ErrorHandler, ErrorObject, NormalizedOutput } from "../index.d.ts" */ @@ -11,8 +15,26 @@ const contains = async (normalizedErrors, instance, localization) => { const errors = []; if (normalizedErrors["https://json-schema.org/keyword/contains"]) { for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/contains"]) { + const position = schemaLocation.lastIndexOf("/"); + const parentLocation = schemaLocation.slice(0, position); + + /** @type ContainsConstraints */ + const containsConstraints = {}; + const minContainsLocation = JsonPointer.append("minContains", parentLocation); + const minContainsNode = await getSchema(minContainsLocation); + /** @type number */ + containsConstraints.minContains = Schema.value(minContainsNode) ?? 1; + + const maxContainsLocation = JsonPointer.append("maxContains", parentLocation); + const maxContainsNode = await getSchema(maxContainsLocation); + /** @type number */ + const maxContains = Schema.value(maxContainsNode); + if (maxContains !== undefined) { + containsConstraints.maxContains = maxContains; + } + errors.push({ - message: localization.getContainsErrorMessage(), + message: localization.getContainsErrorMessage(containsConstraints), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); diff --git a/src/keyword-error-message.test.js b/src/keyword-error-message.test.js index 6035aaa..2634814 100644 --- a/src/keyword-error-message.test.js +++ b/src/keyword-error-message.test.js @@ -1032,13 +1032,139 @@ describe("Error messages", async () => { }); test("normalized output for a failing 'contains' keyword", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + contains: { + type: "number", + multipleOf: 2 + } + }, schemaUri); + const instance = ["", 3, 5]; + const output = { + valid: false, + errors: [ + { + valid: false, + keywordLocation: "/contains", + instanceLocation: "#", + absoluteKeywordLocation: "https://example.com/main#/contains", + errors: [ + { + valid: false, + instanceLocation: "#/0", + absoluteKeywordLocation: "https://example.com/main#/contains/type" + }, + { + valid: false, + instanceLocation: "#/1", + absoluteKeywordLocation: "https://example.com/main#/contains/multipleOf" + }, + { + valid: false, + instanceLocation: "#/2", + absoluteKeywordLocation: "https://example.com/main#/contains/multipleOf" + } + ] + } + ] + }; + const result = await betterJsonSchemaErrors(output, schemaUri, instance); + expect(result.errors).to.eql([ + { + instanceLocation: "#", + message: localization.getContainsErrorMessage({ minContains: 1 }), + schemaLocation: "https://example.com/main#/contains" + }, + { + instanceLocation: "#/0", + message: localization.getTypeErrorMessage("number", "string"), + schemaLocation: "https://example.com/main#/contains/type" + }, + { + instanceLocation: "#/1", + message: localization.getMultipleOfErrorMessage(2), + schemaLocation: "https://example.com/main#/contains/multipleOf" + }, + { + instanceLocation: "#/2", + message: localization.getMultipleOfErrorMessage(2), + schemaLocation: "https://example.com/main#/contains/multipleOf" + } + ]); + }); + + test("normalized output for a failing 'contains' keyword with only minContains", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + contains: { + type: "number", + multipleOf: 2 + }, + minContains: 2 + }, schemaUri); + const instance = ["", 3, 5]; + const output = { + valid: false, + errors: [ + { + valid: false, + keywordLocation: "/contains", + instanceLocation: "#", + absoluteKeywordLocation: "https://example.com/main#/contains", + errors: [ + { + valid: false, + instanceLocation: "#/0", + absoluteKeywordLocation: "https://example.com/main#/contains/type" + }, + { + valid: false, + instanceLocation: "#/1", + absoluteKeywordLocation: "https://example.com/main#/contains/multipleOf" + }, + { + valid: false, + instanceLocation: "#/2", + absoluteKeywordLocation: "https://example.com/main#/contains/multipleOf" + } + ] + } + ] + }; + const result = await betterJsonSchemaErrors(output, schemaUri, instance); + expect(result.errors).to.eql([ + { + instanceLocation: "#", + message: localization.getContainsErrorMessage({ minContains: 2 }), + schemaLocation: "https://example.com/main#/contains" + }, + { + instanceLocation: "#/0", + message: localization.getTypeErrorMessage("number", "string"), + schemaLocation: "https://example.com/main#/contains/type" + }, + { + instanceLocation: "#/1", + message: localization.getMultipleOfErrorMessage(2), + schemaLocation: "https://example.com/main#/contains/multipleOf" + }, + { + instanceLocation: "#/2", + message: localization.getMultipleOfErrorMessage(2), + schemaLocation: "https://example.com/main#/contains/multipleOf" + } + ]); + }); + + test("`contains` with `minContains` and `maxContains` keyword", async () => { registerSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", contains: { type: "number", multipleOf: 2 }, - minContains: 1 + minContains: 2, + maxContains: 4 }, schemaUri); const instance = ["", 3, 5]; const output = { @@ -1073,7 +1199,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#", - message: localization.getContainsErrorMessage(), + message: localization.getContainsErrorMessage({ minContains: 2, maxContains: 4 }), schemaLocation: "https://example.com/main#/contains" }, { diff --git a/src/localization.js b/src/localization.js index cf9094f..ccf1247 100644 --- a/src/localization.js +++ b/src/localization.js @@ -22,6 +22,13 @@ import { FluentBundle, FluentResource } from "@fluent/bundle"; * }} StringConstraints */ +/** + * @typedef {{ + * maxContains?: number; + * minContains: number; + * }} ContainsConstraints + */ + export class Localization { /** * @param {string} locale @@ -169,9 +176,13 @@ export class Localization { return this._formatMessage("format-error", { format }); } - /** @type () => string */ - getContainsErrorMessage() { - return this._formatMessage("contains-error"); + /** @type (constraints: ContainsConstraints) => string */ + getContainsErrorMessage(constraints) { + if (constraints.maxContains) { + return this._formatMessage("contains-error-min-max", constraints); + } else { + return this._formatMessage("contains-error-min", constraints); + } } /** @type () => string */ diff --git a/src/normalization-handlers/contains.js b/src/normalization-handlers/contains.js index 28c2d4b..eda1052 100644 --- a/src/normalization-handlers/contains.js +++ b/src/normalization-handlers/contains.js @@ -1,6 +1,5 @@ import { evaluateSchema } from "../normalized-output.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; - /** * @import { KeywordHandler, NormalizedOutput } from "../index.d.ts" * @import { EvaluatedItemsContext } from "./unevaluatedItems.js" diff --git a/src/translations/en-US.ftl b/src/translations/en-US.ftl index 9759d51..a138d98 100644 --- a/src/translations/en-US.ftl +++ b/src/translations/en-US.ftl @@ -1,8 +1,8 @@ type-error = The instance should be of type {$expected} but found {$actual}. string-error = Expected a string {$constraints}. -string-error-minLength = atleast {$minLength} characters long -string-error-maxLength = atmost {$maxLength} characters long +string-error-minLength = at least {$minLength} characters long +string-error-maxLength = at most {$maxLength} characters long string-error-pattern = to match the pattern {$pattern} number-error = Expected a number {$constraints}. @@ -20,7 +20,16 @@ max-items-error = The instance should contain a maximum of {$limit} items in the min-items-error = The instance should contain a minimum of {$limit} items in the array. unique-items-error = The instance should have unique items in the array. format-error = The instance should match the format: {$format}. -contains-error = A required value is missing from the list. + +contains-error-min = The array must contain at least {$minContains -> + [one] item that passes + *[other] items that pass +} the 'contains' schema. +contains-error-min-max = The array must contain at least {$minContains} and at most {$maxContains -> + [one] item that passes + *[other] items that pass +} the 'contains' schema. + not-error = The instance is not allowed to be used in this schema. additional-properties-error = The property "{$propertyName}" is not allowed. dependent-required-error = Property "{$property}" requires property(s): {$missingDependents}. From 33eb965141857fb6ea15643dc65c70b370052b01 Mon Sep 17 00:00:00 2001 From: Arpit Kuriyal Date: Tue, 12 Aug 2025 22:34:46 +0530 Subject: [PATCH 3/3] remove pattern from the stringHandler --- src/error-handlers/pattern.js | 29 ++++++++++++++++++++++++++ src/error-handlers/string-handler.js | 11 ---------- src/index.js | 6 ++++-- src/keyword-error-message.test.js | 22 +++++++------------ src/localization.js | 10 ++++----- src/normalization-handlers/contains.js | 1 + src/translations/en-US.ftl | 2 +- 7 files changed, 48 insertions(+), 33 deletions(-) create mode 100644 src/error-handlers/pattern.js diff --git a/src/error-handlers/pattern.js b/src/error-handlers/pattern.js new file mode 100644 index 0000000..e2a8513 --- /dev/null +++ b/src/error-handlers/pattern.js @@ -0,0 +1,29 @@ +import { getSchema } from "@hyperjump/json-schema/experimental"; +import * as Schema from "@hyperjump/browser"; +import * as Instance from "@hyperjump/json-schema/instance/experimental"; + +/** + * @import { ErrorHandler, ErrorObject } from "../index.d.ts" + */ + +/** @type ErrorHandler */ +const pattern = async (normalizedErrors, instance, localization) => { + /** @type ErrorObject[] */ + const errors = []; + + if (normalizedErrors["https://json-schema.org/keyword/pattern"]) { + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/pattern"]) { + if (!normalizedErrors["https://json-schema.org/keyword/pattern"][schemaLocation]) { + const keyword = await getSchema(schemaLocation); + errors.push({ + message: localization.getPatternErrorMessage(Schema.value(keyword)), + instanceLocation: Instance.uri(instance), + schemaLocation: schemaLocation + }); + } + } + } + + return errors; +}; +export default pattern; diff --git a/src/error-handlers/string-handler.js b/src/error-handlers/string-handler.js index 4e83b15..8428bd8 100644 --- a/src/error-handlers/string-handler.js +++ b/src/error-handlers/string-handler.js @@ -37,17 +37,6 @@ const stringHandler = async (normalizedErrors, instance, localization) => { constraints.maxLength = Math.min(constraints.maxLength ?? Number.MAX_VALUE, maxLength); } - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/pattern"]) { - if (!normalizedErrors["https://json-schema.org/keyword/pattern"][schemaLocation]) { - failedSchemaLocations.push(schemaLocation); - } - - const keyword = await getSchema(schemaLocation); - /** @type string */ - const pattern = Schema.value(keyword); - constraints.pattern = pattern; - } - if (failedSchemaLocations.length > 0) { return [ { diff --git a/src/index.js b/src/index.js index 2f0103b..2b216df 100644 --- a/src/index.js +++ b/src/index.js @@ -61,7 +61,8 @@ import numberRangeHandler from "./error-handlers/number-range-handler.js"; import requiredErrorHandler from "./error-handlers/required.js"; import typeErrorHandler from "./error-handlers/type.js"; import uniqueItemsErrorHandler from "./error-handlers/uniqueItems.js"; -import stringHandler from "./error-handlers/string-handler.js"; +import stringErrorHandler from "./error-handlers/string-handler.js"; +import patternErrorHandler from "./error-handlers/pattern.js"; /** * @import { betterJsonSchemaErrors } from "./index.d.ts" @@ -131,7 +132,8 @@ addErrorHandler(numberRangeHandler); addErrorHandler(requiredErrorHandler); addErrorHandler(typeErrorHandler); addErrorHandler(uniqueItemsErrorHandler); -addErrorHandler(stringHandler); +addErrorHandler(stringErrorHandler); +addErrorHandler(patternErrorHandler); export { setNormalizationHandler } from "./normalized-output.js"; export { addErrorHandler } from "./error-handling.js"; diff --git a/src/keyword-error-message.test.js b/src/keyword-error-message.test.js index 2634814..7c2240c 100644 --- a/src/keyword-error-message.test.js +++ b/src/keyword-error-message.test.js @@ -614,7 +614,7 @@ describe("Error messages", async () => { { schemaLocation: "https://example.com/main#/pattern", instanceLocation: "#", - message: localization.getStringErrorMessage({ pattern: "^[a-z]+$" }) + message: localization.getPatternErrorMessage("^[a-z]+$") } ]); }); @@ -883,7 +883,7 @@ describe("Error messages", async () => { { schemaLocation: "https://example.com/main#/anyOf/1/properties/ID/pattern", instanceLocation: "#/ID", - message: localization.getStringErrorMessage({ pattern: "^[0-9\\-]+$" }) + message: localization.getPatternErrorMessage("^[0-9\\-]+$") } ]); }); @@ -1355,7 +1355,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#/Foo", - message: localization.getStringErrorMessage({ pattern: "^[a-z]*$" }), + message: localization.getPatternErrorMessage("^[a-z]*$"), schemaLocation: "https://example.com/main#/propertyNames/pattern" } ]); @@ -1382,7 +1382,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#/Foo", - message: localization.getStringErrorMessage({ pattern: "^[a-z]*$" }), + message: localization.getPatternErrorMessage("^[a-z]*$"), schemaLocation: "https://example.com/main#/propertyNames/pattern" } ]); @@ -1409,7 +1409,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#*/Foo", - message: localization.getStringErrorMessage({ pattern: "^[a-z]*$" }), + message: localization.getPatternErrorMessage("^[a-z]*$"), schemaLocation: "https://example.com/main#/propertyNames/pattern" } ]); @@ -1503,8 +1503,7 @@ describe("Error messages", async () => { $schema: "https://json-schema.org/draft/2020-12/schema", allOf: [ { minLength: 3 }, - { maxLength: 5 }, - { pattern: "^[a-z]+$" } + { maxLength: 5 } ] }, schemaUri); @@ -1521,10 +1520,6 @@ describe("Error messages", async () => { { absoluteKeywordLocation: "https://example.com/main#/allOf/1/maxLength", instanceLocation: "#" - }, - { - absoluteKeywordLocation: "https://example.com/main#/allOf/2/pattern", - instanceLocation: "#" } ] }; @@ -1533,11 +1528,10 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: [ "https://example.com/main#/allOf/0/minLength", - "https://example.com/main#/allOf/1/maxLength", - "https://example.com/main#/allOf/2/pattern" + "https://example.com/main#/allOf/1/maxLength" ], instanceLocation: "#", - message: localization.getStringErrorMessage({ minLength: 3, maxLength: 5, pattern: "^[a-z]+$" }) + message: localization.getStringErrorMessage({ minLength: 3, maxLength: 5 }) }]); }); }); diff --git a/src/localization.js b/src/localization.js index ccf1247..4355b98 100644 --- a/src/localization.js +++ b/src/localization.js @@ -18,7 +18,6 @@ import { FluentBundle, FluentResource } from "@fluent/bundle"; * @typedef {{ * minLength?: number; * maxLength?: number; - * pattern?: string; * }} StringConstraints */ @@ -119,10 +118,6 @@ export class Localization { messages.push(this._formatMessage("string-error-maxLength", constraints)); } - if (constraints.pattern) { - messages.push(this._formatMessage("string-error-pattern", constraints)); - } - return this._formatMessage("string-error", { constraints: new Intl.ListFormat(this.locale, { type: "conjunction" }).format(messages) }); @@ -176,6 +171,11 @@ export class Localization { return this._formatMessage("format-error", { format }); } + /** @type (pattern: string) => string */ + getPatternErrorMessage(pattern) { + return this._formatMessage("pattern-error", { pattern }); + } + /** @type (constraints: ContainsConstraints) => string */ getContainsErrorMessage(constraints) { if (constraints.maxContains) { diff --git a/src/normalization-handlers/contains.js b/src/normalization-handlers/contains.js index eda1052..28c2d4b 100644 --- a/src/normalization-handlers/contains.js +++ b/src/normalization-handlers/contains.js @@ -1,5 +1,6 @@ import { evaluateSchema } from "../normalized-output.js"; import * as Instance from "@hyperjump/json-schema/instance/experimental"; + /** * @import { KeywordHandler, NormalizedOutput } from "../index.d.ts" * @import { EvaluatedItemsContext } from "./unevaluatedItems.js" diff --git a/src/translations/en-US.ftl b/src/translations/en-US.ftl index a138d98..5fc886f 100644 --- a/src/translations/en-US.ftl +++ b/src/translations/en-US.ftl @@ -3,7 +3,6 @@ type-error = The instance should be of type {$expected} but found {$actual}. string-error = Expected a string {$constraints}. string-error-minLength = at least {$minLength} characters long string-error-maxLength = at most {$maxLength} characters long -string-error-pattern = to match the pattern {$pattern} number-error = Expected a number {$constraints}. number-error-minimum = greater than {$minimum} @@ -20,6 +19,7 @@ max-items-error = The instance should contain a maximum of {$limit} items in the min-items-error = The instance should contain a minimum of {$limit} items in the array. unique-items-error = The instance should have unique items in the array. format-error = The instance should match the format: {$format}. +pattern-error = The instance should match the pattern: {$pattern}. contains-error-min = The array must contain at least {$minContains -> [one] item that passes