diff --git a/src/error-handlers/type.js b/src/error-handlers/type.js index b3ff5cb..5907b4c 100644 --- a/src/error-handlers/type.js +++ b/src/error-handlers/type.js @@ -6,21 +6,50 @@ 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 type = async (normalizedErrors, instance, localization) => { /** @type ErrorObject[] */ const errors = []; if (normalizedErrors["https://json-schema.org/keyword/type"]) { + let allowedTypes = new Set(ALL_TYPES); + const failedTypeLocations = []; + for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/type"]) { - if (!normalizedErrors["https://json-schema.org/keyword/type"][schemaLocation]) { - const keyword = await getSchema(schemaLocation); - errors.push({ - message: localization.getTypeErrorMessage(Schema.value(keyword), Instance.typeOf(instance)), - instanceLocation: Instance.uri(instance), - schemaLocation: schemaLocation - }); + 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.getConflictingTypeMessage(), + instanceLocation: Instance.uri(instance), + schemaLocation: failedTypeLocations + }); + } else if (failedTypeLocations.length > 0) { + errors.push({ + message: localization.getTypeErrorMessage([...allowedTypes], Instance.typeOf(instance)), + instanceLocation: Instance.uri(instance), + schemaLocation: failedTypeLocations.length === 1 ? failedTypeLocations[0] : failedTypeLocations + }); } } diff --git a/src/keyword-error-message.test.js b/src/keyword-error-message.test.js index 4217ef1..5a5483b 100644 --- a/src/keyword-error-message.test.js +++ b/src/keyword-error-message.test.js @@ -1719,7 +1719,7 @@ describe("Error messages", async () => { ]); }); - test.skip("allOf conflicting type", async () => { + test("allOf conflicting type", async () => { registerSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", allOf: [ @@ -1749,13 +1749,130 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { - schemaLocation: "https://example.com/main#/allOf/0/type", + schemaLocation: [ + "https://example.com/main#/allOf/0/type", + "https://example.com/main#/allOf/1/type" + ], instanceLocation: "#", message: localization.getConflictingTypeMessage() } ]); }); + test("can be a number and integer at the same time", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + allOf: [ + { type: "number" }, + { type: "integer" } + ] + }, schemaUri); + + const instance = "foo"; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/allOf/0/type", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "https://example.com/main#/allOf/1/type", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(output, schemaUri, instance); + + expect(result.errors).to.eql([ + { + message: localization.getTypeErrorMessage("integer", "string"), + instanceLocation: "#", + schemaLocation: [ + "https://example.com/main#/allOf/0/type", + "https://example.com/main#/allOf/1/type" + ] + } + ]); + }); + + test("can be a number and integer at the same time - pass", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + allOf: [ + { type: "number" }, + { type: "integer" } + ], + maximum: 5 + }, schemaUri); + + const instance = 15; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/maximum", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(output, schemaUri, instance); + + expect(result.errors).to.eql([ + { + message: localization.getNumberErrorMessage({ maximum: 5 }), + instanceLocation: "#", + schemaLocation: "https://example.com/main#/maximum" + } + ]); + }); + + test("there should be one type message per schema", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + allOf: [ + { type: "number" }, + { type: "number" } + ] + }, schemaUri); + + const instance = "foo"; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/allOf/0/type", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "https://example.com/main#/allOf/1/type", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(output, schemaUri, instance); + + expect(result.errors).to.eql([ + { + message: localization.getTypeErrorMessage(["number"], "string"), + instanceLocation: "#", + schemaLocation: [ + "https://example.com/main#/allOf/0/type", + "https://example.com/main#/allOf/1/type" + ] + } + ]); + }); + test("normalized output for a failing 'contains' keyword", async () => { registerSchema({ $schema: "https://json-schema.org/draft/2020-12/schema", @@ -2261,4 +2378,117 @@ describe("Error messages", async () => { message: localization.getStringErrorMessage({ minLength: 3, maxLength: 5 }) }]); }); + test("can be a number and integer at the same time", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + allOf: [ + { type: "number" }, + { type: "integer" } + ] + }, schemaUri); + + const instance = "foo"; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/allOf/0/type", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "https://example.com/main#/allOf/1/type", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(output, schemaUri, instance); + + expect(result.errors).to.eql([ + { + message: localization.getTypeErrorMessage("integer", "string"), + instanceLocation: "#", + schemaLocation: [ + "https://example.com/main#/allOf/0/type", + "https://example.com/main#/allOf/1/type" + ] + } + ]); + }); + + test("can be a number and integer at the same time - pass", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + allOf: [ + { type: "number" }, + { type: "integer" } + ], + maximum: 5 + }, schemaUri); + + const instance = 15; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/maximum", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(output, schemaUri, instance); + + expect(result.errors).to.eql([ + { + message: localization.getNumberErrorMessage({ maximum: 5 }), + instanceLocation: "#", + schemaLocation: "https://example.com/main#/maximum" + } + ]); + }); + + test("there should be one type message per schema", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + allOf: [ + { type: "number" }, + { type: "number" } + ] + }, schemaUri); + + const instance = "foo"; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/allOf/0/type", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "https://example.com/main#/allOf/1/type", + instanceLocation: "#" + } + ] + }; + + const result = await betterJsonSchemaErrors(output, schemaUri, instance); + + expect(result.errors).to.eql([ + { + message: localization.getTypeErrorMessage(["number"], "string"), + instanceLocation: "#", + schemaLocation: [ + "https://example.com/main#/allOf/0/type", + "https://example.com/main#/allOf/1/type" + ] + } + ]); + }); });