diff --git a/src/error-handlers/const.js b/src/error-handlers/const.js deleted file mode 100644 index 64c23c3..0000000 --- a/src/error-handlers/const.js +++ /dev/null @@ -1,32 +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, Json } from "../index.d.ts" - */ - -/** @type ErrorHandler */ -const constErrorHandler = async (normalizedErrors, instance, localization) => { - /** @type ErrorObject[] */ - const errors = []; - - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/const"]) { - if (normalizedErrors["https://json-schema.org/keyword/const"][schemaLocation]) { - continue; - } - - const keyword = await getSchema(schemaLocation); - const expected = /** @type Json */ (Schema.value(keyword)); - - errors.push({ - message: localization.getConstErrorMessage(expected), - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); - } - - return errors; -}; - -export default constErrorHandler; diff --git a/src/error-handlers/constEnum.js b/src/error-handlers/constEnum.js new file mode 100644 index 0000000..ab0a311 --- /dev/null +++ b/src/error-handlers/constEnum.js @@ -0,0 +1,78 @@ +import { getSchema } from "@hyperjump/json-schema/experimental"; +import * as Schema from "@hyperjump/browser"; +import * as Instance from "@hyperjump/json-schema/instance/experimental"; +import jsonStringify from "json-stringify-deterministic"; + +/** + * @import { ErrorHandler, Json } from "../index.d.ts" + */ + +/** + * @typedef {{ + * allowedValues: Json[]; + * schemaLocation: string; + * }} Constraint + */ + +/** @type {ErrorHandler} */ +const constEnumErrorHandler = async (normalizedErrors, instance, localization) => { + /** @type Set | undefined */ + let allowedJson; + + /** @type string[]> */ + const constSchemaLocations = []; + + /** @type string[]> */ + const enumSchemaLocations = []; + + /** @type string[]> */ + const allSchemaLocations = []; + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/const"]) { + if (!normalizedErrors["https://json-schema.org/keyword/const"][schemaLocation]) { + constSchemaLocations.push(schemaLocation); + } + allSchemaLocations.push(schemaLocation); + + const keyword = await getSchema(schemaLocation); + const keywordJson = new Set([jsonStringify(/** @type Json */ (Schema.value(keyword)))]); + + allowedJson = allowedJson?.intersection(keywordJson) ?? keywordJson; + } + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/enum"]) { + if (!normalizedErrors["https://json-schema.org/keyword/enum"][schemaLocation]) { + enumSchemaLocations.push(schemaLocation); + } + allSchemaLocations.push(schemaLocation); + + const keyword = await getSchema(schemaLocation); + const keywordJson = new Set(/** @type Json[] */ (Schema.value(keyword)).map((value) => jsonStringify(value))); + + allowedJson = allowedJson?.intersection(keywordJson) ?? keywordJson; + } + + if (constSchemaLocations.length === 0 && enumSchemaLocations.length === 0) { + return []; + } + + if (allowedJson?.size === 0) { + return [{ + message: localization.getBooleanSchemaErrorMessage(), + instanceLocation: Instance.uri(instance), + schemaLocations: allSchemaLocations + }]; + } else { + /** @type Json[] */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const allowedValues = [...allowedJson ?? []].map((json) => JSON.parse(json)); + + return [{ + message: localization.getEnumErrorMessage(allowedValues), + instanceLocation: Instance.uri(instance), + schemaLocations: constSchemaLocations.length ? constSchemaLocations : enumSchemaLocations + }]; + } +}; + +export default constEnumErrorHandler; diff --git a/src/error-handlers/enum.js b/src/error-handlers/enum.js deleted file mode 100644 index 2d1c1d0..0000000 --- a/src/error-handlers/enum.js +++ /dev/null @@ -1,32 +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, Json } from "../index.d.ts" - */ - -/** @type ErrorHandler */ -const enumErrorHandler = async (normalizedErrors, instance, localization) => { - /** @type ErrorObject[] */ - const errors = []; - - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/enum"]) { - if (normalizedErrors["https://json-schema.org/keyword/enum"][schemaLocation]) { - continue; - } - - const keyword = await getSchema(schemaLocation); - const expected = /** @type Json[] */ (Schema.value(keyword)); - - errors.push({ - message: localization.getEnumErrorMessage(expected), - instanceLocation: Instance.uri(instance), - schemaLocations: [schemaLocation] - }); - } - - return errors; -}; - -export default enumErrorHandler; diff --git a/src/index.js b/src/index.js index ce48b7f..c617f46 100644 --- a/src/index.js +++ b/src/index.js @@ -55,10 +55,9 @@ import unknownNormalizationHandler from "./normalization-handlers/unknown.js"; // Error Handlers import anyOfErrorHandler from "./error-handlers/anyOf.js"; import booleanSchemaErrorHandler from "./error-handlers/boolean-schema.js"; -import constErrorHandler from "./error-handlers/const.js"; +import constEnumErrorHandler from "./error-handlers/constEnum.js"; import containsErrorHandler from "./error-handlers/contains.js"; import dependenciesErrorHandler from "./error-handlers/draft-04/dependencies.js"; -import enumErrorHandler from "./error-handlers/enum.js"; import exclusiveMaximumErrorHandler from "./error-handlers/exclusiveMaximum.js"; import exclusiveMinimumErrorHandler from "./error-handlers/exclusiveMinimum.js"; import formatErrorHandler from "./error-handlers/format.js"; @@ -140,10 +139,9 @@ setNormalizationHandler("https://json-schema.org/keyword/unknown", unknownNormal addErrorHandler(anyOfErrorHandler); addErrorHandler(booleanSchemaErrorHandler); -addErrorHandler(constErrorHandler); +addErrorHandler(constEnumErrorHandler); addErrorHandler(containsErrorHandler); addErrorHandler(dependenciesErrorHandler); -addErrorHandler(enumErrorHandler); addErrorHandler(exclusiveMaximumErrorHandler); addErrorHandler(exclusiveMinimumErrorHandler); addErrorHandler(formatErrorHandler); diff --git a/src/localization.js b/src/localization.js index 17ccef4..ddd16d5 100644 --- a/src/localization.js +++ b/src/localization.js @@ -59,19 +59,18 @@ export class Localization { }); } - /** @type (expected: Json) => string */ - getConstErrorMessage(expected) { - return this.#formatMessage("const-message", { - expected: JSON.stringify(expected, null, " ") - }); - } - /** @type (expected: Json[]) => string */ getEnumErrorMessage(expected) { - const expectedJson = expected.map((value) => JSON.stringify(value)); - return this.#formatMessage("enum-message", { - expected: this.disjunction.format(expectedJson) - }); + if (expected.length === 1) { + return this.#formatMessage("const-message", { + expected: JSON.stringify(expected[0], null, " ") + }); + } else { + const expectedJson = expected.map((value) => JSON.stringify(value)); + return this.#formatMessage("enum-message", { + expected: this.disjunction.format(expectedJson) + }); + } } /** @type (format: string) => string */ diff --git a/src/test-suite/tests/const.json b/src/test-suite/tests/const.json index c59ac52..559ca3a 100644 --- a/src/test-suite/tests/const.json +++ b/src/test-suite/tests/const.json @@ -29,6 +29,69 @@ }, "instance": 42, "errors": [] + }, + { + "description": "const with enum", + "compatibility": "6", + "schema": { + "allOf": [ + { "enum": ["a", "b", "c"] }, + { "enum": ["a", "b"] }, + { "const": "a" } + ] + }, + "instance": "x", + "errors": [ + { + "messageId": "const-message", + "messageParams": { "expected": "\"a\"" }, + "instanceLocation": "#", + "schemaLocations": ["#/allOf/2/const"] + } + ] + }, + { + "description": "const with enum - deterministic key order", + "compatibility": "6", + "schema": { + "allOf": [ + { "enum": [{ "a": 1, "b": 2 }, { "c": 3 }] }, + { "const": { "b": 2, "a": 1 } } + ] + }, + "instance": "x", + "errors": [ + { + "messageId": "const-message", + "messageParams": { + "expected": "{\n \"a\": 1,\n \"b\": 2\n}" + }, + "instanceLocation": "#", + "schemaLocations": ["#/allOf/1/const"] + } + ] + }, + { + "description": "contradictory const", + "compatibility": "6", + "schema": { + "allOf": [ + { "const": "a" }, + { "const": "b" } + ] + }, + "instance": "a", + "errors": [ + { + "messageId": "boolean-schema-message", + "messageParams": {}, + "instanceLocation": "#", + "schemaLocations": [ + "#/allOf/0/const", + "#/allOf/1/const" + ] + } + ] } ] } diff --git a/src/test-suite/tests/enum.json b/src/test-suite/tests/enum.json index 613e004..86be502 100644 --- a/src/test-suite/tests/enum.json +++ b/src/test-suite/tests/enum.json @@ -28,6 +28,77 @@ }, "instance": "foo", "errors": [] + }, + { + "description": "multiple enums with matches", + "schema": { + "allOf": [ + { "enum": ["a", "b", "c"] }, + { "enum": ["a", "b"] } + ] + }, + "instance": "x", + "errors": [ + { + "messageId": "enum-message", + "messageParams": { + "expected": { "or": ["\"a\"", "\"b\""] }, + "count": 2 + }, + "instanceLocation": "#", + "schemaLocations": [ + "#/allOf/0/enum", + "#/allOf/1/enum" + ] + } + ] + }, + { + "description": "multiple enums with a match", + "compatibility": "6", + "schema": { + "allOf": [ + { "enum": ["a", "b", "c"] }, + { "enum": ["a", "b", "d"] }, + { "enum": ["a", "b", "e"] } + ] + }, + "instance": "c", + "errors": [ + { + "messageId": "enum-message", + "messageParams": { + "expected": { "or": ["\"a\"", "\"b\""] } + }, + "instanceLocation": "#", + "schemaLocations": [ + "#/allOf/1/enum", + "#/allOf/2/enum" + ] + } + ] + }, + { + "description": "contradictory enum", + "compatibility": "6", + "schema": { + "allOf": [ + { "enum": ["a", "b", "c"] }, + { "enum": ["d", "e", "f"] } + ] + }, + "instance": "a", + "errors": [ + { + "messageId": "boolean-schema-message", + "messageParams": {}, + "instanceLocation": "#", + "schemaLocations": [ + "#/allOf/0/enum", + "#/allOf/1/enum" + ] + } + ] } ] }