diff --git a/src/error-handlers/constEnum.js b/src/error-handlers/constEnum.js deleted file mode 100644 index ab0a311..0000000 --- a/src/error-handlers/constEnum.js +++ /dev/null @@ -1,78 +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 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/type.js b/src/error-handlers/type.js deleted file mode 100644 index 99bcb9b..0000000 --- a/src/error-handlers/type.js +++ /dev/null @@ -1,61 +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" - */ - -const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "object", "integer"]); - -/** @type ErrorHandler */ -const typeErrorHandler = async (normalizedErrors, instance, localization) => { - /** @type ErrorObject[] */ - const errors = []; - - if (normalizedErrors["https://json-schema.org/keyword/type"]) { - /** @type {Set} */ - let allowedTypes = ALL_TYPES; - const failedTypeLocations = []; - - for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/type"]) { - const isValid = normalizedErrors["https://json-schema.org/keyword/type"][schemaLocation]; - if (!isValid) { - failedTypeLocations.push(schemaLocation); - - const keyword = await getSchema(schemaLocation); - /** @type {string|string[]} */ - const value = Schema.value(keyword); - const types = Array.isArray(value) ? value : [value]; - /** @type {Set} */ - const keywordTypes = new Set(types); - if (keywordTypes.has("number")) { - keywordTypes.add("integer"); - } - allowedTypes = allowedTypes.intersection(keywordTypes); - } - } - - if (allowedTypes.has("number")) { - allowedTypes.delete("integer"); - } - - if (allowedTypes.size === 0) { - errors.push({ - message: localization.getBooleanSchemaErrorMessage(), - instanceLocation: Instance.uri(instance), - schemaLocations: failedTypeLocations - }); - } else if (failedTypeLocations.length > 0) { - errors.push({ - message: localization.getTypeErrorMessage([...allowedTypes]), - instanceLocation: Instance.uri(instance), - schemaLocations: failedTypeLocations - }); - } - } - - return errors; -}; - -export default typeErrorHandler; diff --git a/src/error-handlers/typeConstEnum.js b/src/error-handlers/typeConstEnum.js new file mode 100644 index 0000000..0a0441b --- /dev/null +++ b/src/error-handlers/typeConstEnum.js @@ -0,0 +1,118 @@ +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" + */ + +const ALL_TYPES = new Set(["null", "boolean", "number", "string", "array", "object", "integer"]); + +/** @type {ErrorHandler} */ +const typeConstEnumErrorHandler = async (normalizedErrors, instance, localization) => { + let allowedTypes = new Set(ALL_TYPES); + /** @type {string[]} */ + const failedTypeLocations = []; + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/type"]) { + if (!normalizedErrors["https://json-schema.org/keyword/type"][schemaLocation]) { + failedTypeLocations.push(schemaLocation); + + const keyword = await getSchema(schemaLocation); + /** @type {string | string[]} */ + const value = Schema.value(keyword); + const types = Array.isArray(value) ? value : [value]; + /** @type {Set} */ + const keywordTypes = new Set(types); + if (keywordTypes.has("number")) { + keywordTypes.add("integer"); + } + allowedTypes = allowedTypes.intersection(keywordTypes); + } + } + if (allowedTypes.has("number")) { + allowedTypes.delete("integer"); + } + + /** @type {Set | undefined} */ + let allowedJson; + + /** @type {string[]} */ + const constEnumLocations = []; + /** @type {string[]} */ + const failedConstLocations = []; + /** @type {string[]} */ + const failedEnumLocations = []; + let typeFiltered = false; + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/const"]) { + constEnumLocations.push(schemaLocation); + if (!normalizedErrors["https://json-schema.org/keyword/const"][schemaLocation]) { + failedConstLocations.push(schemaLocation); + } + + const keyword = await getSchema(schemaLocation); + const keywordJson = new Set(); + if (allowedTypes.has(Schema.typeOf(keyword))) { + keywordJson.add(jsonStringify(Schema.value(keyword))); + } else { + typeFiltered = true; + } + + allowedJson = allowedJson?.intersection(keywordJson) ?? keywordJson; + } + + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/enum"]) { + constEnumLocations.push(schemaLocation); + if (!normalizedErrors["https://json-schema.org/keyword/enum"][schemaLocation]) { + failedEnumLocations.push(schemaLocation); + } + + const keyword = await getSchema(schemaLocation); + const keywordJson = new Set(); + for await (const enumValueNode of Schema.iter(keyword)) { + if (allowedTypes.has(Schema.typeOf(enumValueNode))) { + keywordJson.add(jsonStringify(Schema.value(enumValueNode))); + } else { + typeFiltered = true; + } + } + + allowedJson = allowedJson?.intersection(keywordJson) ?? keywordJson; + } + + const failedLocations = failedConstLocations.length > 0 + ? failedConstLocations + : failedEnumLocations; + + if (failedLocations.length === 0 && failedTypeLocations.length === 0) { + return []; + } else if (allowedTypes.size === 0 || allowedJson?.size === 0) { + return [{ + message: localization.getBooleanSchemaErrorMessage(), + instanceLocation: Instance.uri(instance), + schemaLocations: [...failedTypeLocations, ...constEnumLocations] + }]; + } else if (allowedJson?.size) { + /** @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: typeFiltered + ? [...failedTypeLocations, ...constEnumLocations] + : failedLocations + }]; + } else { + return [{ + message: localization.getTypeErrorMessage([...allowedTypes]), + instanceLocation: Instance.uri(instance), + schemaLocations: failedTypeLocations + }]; + } +}; + +export default typeConstEnumErrorHandler; diff --git a/src/index.js b/src/index.js index c617f46..65c90ae 100644 --- a/src/index.js +++ b/src/index.js @@ -55,7 +55,6 @@ 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 constEnumErrorHandler from "./error-handlers/constEnum.js"; import containsErrorHandler from "./error-handlers/contains.js"; import dependenciesErrorHandler from "./error-handlers/draft-04/dependencies.js"; import exclusiveMaximumErrorHandler from "./error-handlers/exclusiveMaximum.js"; @@ -76,7 +75,7 @@ import notErrorHandler from "./error-handlers/not.js"; import oneOfErrorHandler from "./error-handlers/oneOf.js"; import patternErrorHandler from "./error-handlers/pattern.js"; import requiredErrorHandler from "./error-handlers/required.js"; -import typeErrorHandler from "./error-handlers/type.js"; +import typeConstEnumErrorHandler from "./error-handlers/typeConstEnum.js"; import uniqueItemsErrorHandler from "./error-handlers/uniqueItems.js"; import unknownErrorHandler from "./error-handlers/unknown.js"; @@ -139,7 +138,6 @@ setNormalizationHandler("https://json-schema.org/keyword/unknown", unknownNormal addErrorHandler(anyOfErrorHandler); addErrorHandler(booleanSchemaErrorHandler); -addErrorHandler(constEnumErrorHandler); addErrorHandler(containsErrorHandler); addErrorHandler(dependenciesErrorHandler); addErrorHandler(exclusiveMaximumErrorHandler); @@ -160,7 +158,7 @@ addErrorHandler(notErrorHandler); addErrorHandler(oneOfErrorHandler); addErrorHandler(patternErrorHandler); addErrorHandler(requiredErrorHandler); -addErrorHandler(typeErrorHandler); +addErrorHandler(typeConstEnumErrorHandler); addErrorHandler(uniqueItemsErrorHandler); addErrorHandler(unknownErrorHandler); diff --git a/src/test-suite/tests/const.json b/src/test-suite/tests/const.json index 559ca3a..8d11e64 100644 --- a/src/test-suite/tests/const.json +++ b/src/test-suite/tests/const.json @@ -92,6 +92,49 @@ ] } ] + }, + { + "description": "const with type - filter by type", + "compatibility": "6", + "schema": { + "allOf": [ + { "type": "number" }, + { "const": "foo" } + ] + }, + "instance": "foo", + "errors": [ + { + "messageId": "boolean-schema-message", + "messageParams": {}, + "instanceLocation": "#", + "schemaLocations": [ + "#/allOf/0/type", + "#/allOf/1/const" + ] + } + ] + }, + { + "description": "const with matching type", + "compatibility": "6", + "schema": { + "allOf": [ + { "type": "string" }, + { "const": "foo" } + ] + }, + "instance": 42, + "errors": [ + { + "messageId": "const-message", + "messageParams": { + "expected": "\"foo\"" + }, + "instanceLocation": "#", + "schemaLocations": ["#/allOf/1/const"] + } + ] } ] } diff --git a/src/test-suite/tests/enum.json b/src/test-suite/tests/enum.json index 86be502..6f312e2 100644 --- a/src/test-suite/tests/enum.json +++ b/src/test-suite/tests/enum.json @@ -99,6 +99,71 @@ ] } ] + }, + { + "description": "enum with type - filter by type", + "schema": { + "allOf": [ + { "type": "string" }, + { "enum": ["foo", 42] } + ] + }, + "instance": 42, + "errors": [ + { + "messageId": "const-message", + "messageParams": { + "expected": "\"foo\"" + }, + "instanceLocation": "#", + "schemaLocations": [ + "#/allOf/0/type", + "#/allOf/1/enum" + ] + } + ] + }, + { + "description": "enum with type - no filtering needed", + "schema": { + "allOf": [ + { "type": "string" }, + { "enum": ["foo", "bar"] } + ] + }, + "instance": 42, + "errors": [ + { + "messageId": "enum-message", + "messageParams": { + "expected": { "or": ["\"foo\"", "\"bar\""] }, + "count": 2 + }, + "instanceLocation": "#", + "schemaLocations": ["#/allOf/1/enum"] + } + ] + }, + { + "description": "enum with type - all values filtered", + "schema": { + "allOf": [ + { "type": "boolean" }, + { "enum": ["foo", 42] } + ] + }, + "instance": "foo", + "errors": [ + { + "messageId": "boolean-schema-message", + "messageParams": {}, + "instanceLocation": "#", + "schemaLocations": [ + "#/allOf/0/type", + "#/allOf/1/enum" + ] + } + ] } ] }