From 25d8fac8c3fb4eb5714b54808cc2ddb93548156a Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 8 Apr 2025 18:50:05 -0700 Subject: [PATCH 1/3] Refactor keywords to return boolean instead of Output --- src/index.js | 212 ++++++++++++++++++++++---------------------------- src/output.js | 27 ------- 2 files changed, 92 insertions(+), 147 deletions(-) delete mode 100644 src/output.js diff --git a/src/index.js b/src/index.js index ac30e57..90c1bc3 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,6 @@ import { jsonPointerStep, jsonValue } from "./jsonast-util.js"; -import { Output } from "./output.js"; /** * @import { @@ -22,6 +21,12 @@ import { Output } from "./output.js"; */ +/** + * @typedef {{ + * valid: boolean; + * }} Output + */ + /** @type (schema: Json, instance: Json) => Output */ export const validate = (schema, instance) => { // Determine schema identifier @@ -39,33 +44,30 @@ export const validate = (schema, instance) => { } } - const output = validateSchema(schemaNode, toJsonNode(instance)); + const valid = validateSchema(schemaNode, toJsonNode(instance)); schemaRegistry.delete(uri); - return output; + return { valid }; }; -/** @type (schemaNode: JsonNode, instanceNode: JsonNode) => Output */ +/** @type (schemaNode: JsonNode, instanceNode: JsonNode) => boolean */ const validateSchema = (schemaNode, instanceNode) => { if (schemaNode.type === "json") { switch (schemaNode.jsonType) { case "boolean": - return new Output(schemaNode.value, schemaNode, instanceNode); + return schemaNode.value; case "object": let isValid = true; for (const propertyNode of schemaNode.children) { const [keywordNode, keywordValueNode] = propertyNode.children; const keywordHandler = keywordHandlers.get(keywordNode.value); - if (keywordHandler) { - const keywordOutput = keywordHandler(keywordValueNode, instanceNode, schemaNode); - if (!keywordOutput.valid) { - isValid = false; - } + if (keywordHandler && !keywordHandler(keywordValueNode, instanceNode, schemaNode)) { + isValid = false; } } - return new Output(isValid, schemaNode, instanceNode); + return isValid; } } @@ -85,7 +87,7 @@ export const registerSchema = (schema, uri) => { * keywordNode: JsonNode, * instanceNode: JsonNode, * schemaNode: JsonObjectNode - * ) => Output} KeywordHandler + * ) => boolean} KeywordHandler */ /** @type Map */ @@ -106,13 +108,12 @@ keywordHandlers.set("$ref", (refNode, instanceNode) => { const pointer = decodeURI(parseIriReference(refNode.value).fragment ?? ""); const referencedSchemaNode = jsonPointerGet(pointer, schemaNode, uri); - const keywordOutput = validateSchema(referencedSchemaNode, instanceNode); - return new Output(keywordOutput.valid, refNode, instanceNode, keywordOutput.errors); + return validateSchema(referencedSchemaNode, instanceNode); }); keywordHandlers.set("additionalProperties", (additionalPropertiesNode, instanceNode, schemaNode) => { if (instanceNode.jsonType !== "object") { - return new Output(true, additionalPropertiesNode, instanceNode); + return true; } const propertyPatterns = []; @@ -139,14 +140,13 @@ keywordHandlers.set("additionalProperties", (additionalPropertiesNode, instanceN for (const propertyNode of instanceNode.children) { const [propertyNameNode, instancePropertyNode] = propertyNode.children; if (!isDefinedProperty.test(propertyNameNode.value)) { - const schemaOutput = validateSchema(additionalPropertiesNode, instancePropertyNode); - if (!schemaOutput.valid) { + if (!validateSchema(additionalPropertiesNode, instancePropertyNode)) { isValid = false; } } } - return new Output(isValid, additionalPropertiesNode, instanceNode); + return isValid; }); /** @type (string: string) => string */ @@ -159,12 +159,12 @@ keywordHandlers.set("allOf", (allOfNode, instanceNode) => { let isValid = true; for (const schemaNode of allOfNode.children) { - if (!validateSchema(schemaNode, instanceNode).valid) { + if (!validateSchema(schemaNode, instanceNode)) { isValid = false; } } - return new Output(isValid, allOfNode, instanceNode); + return isValid; }); keywordHandlers.set("anyOf", (anyOfNode, instanceNode) => { @@ -172,12 +172,11 @@ keywordHandlers.set("anyOf", (anyOfNode, instanceNode) => { let isValid = false; for (const schemaNode of anyOfNode.children) { - const schemaOutput = validateSchema(schemaNode, instanceNode); - if (schemaOutput.valid) { + if (validateSchema(schemaNode, instanceNode)) { isValid = true; } } - return new Output(isValid, anyOfNode, instanceNode); + return isValid; }); keywordHandlers.set("oneOf", (oneOfNode, instanceNode) => { @@ -185,23 +184,21 @@ keywordHandlers.set("oneOf", (oneOfNode, instanceNode) => { let matches = 0; for (const schemaNode of oneOfNode.children) { - const schemaOutput = validateSchema(schemaNode, instanceNode); - if (schemaOutput.valid) { + if (validateSchema(schemaNode, instanceNode)) { matches++; } } - return new Output(matches === 1, oneOfNode, instanceNode); + return matches === 1; }); keywordHandlers.set("not", (notNode, instanceNode) => { - const schemaOutput = validateSchema(notNode, instanceNode); - return new Output(!schemaOutput.valid, notNode, instanceNode); + return !validateSchema(notNode, instanceNode); }); keywordHandlers.set("contains", (containsNode, instanceNode, schemaNode) => { if (instanceNode.jsonType !== "array") { - return new Output(true, containsNode, instanceNode); + return true; } let minContains = 1; @@ -222,19 +219,17 @@ keywordHandlers.set("contains", (containsNode, instanceNode, schemaNode) => { let matches = 0; for (const itemNode of instanceNode.children) { - const schemaOutput = validateSchema(containsNode, itemNode); - if (schemaOutput.valid) { + if (validateSchema(containsNode, itemNode)) { matches++; } } - const isValid = matches >= minContains && matches <= maxContains; - return new Output(isValid, containsNode, instanceNode); + return matches >= minContains && matches <= maxContains; }); keywordHandlers.set("dependentSchemas", (dependentSchemasNode, instanceNode) => { if (instanceNode.jsonType !== "object") { - return new Output(true, dependentSchemasNode, instanceNode); + return true; } assertNodeType(dependentSchemasNode, "object"); @@ -242,44 +237,39 @@ keywordHandlers.set("dependentSchemas", (dependentSchemasNode, instanceNode) => let isValid = true; for (const propertyNode of dependentSchemasNode.children) { const [keyNode, schemaNode] = propertyNode.children; - if (jsonObjectHas(keyNode.value, instanceNode)) { - const schemaOutput = validateSchema(schemaNode, instanceNode); - if (!schemaOutput.valid) { - isValid = false; - } + if (jsonObjectHas(keyNode.value, instanceNode) && !validateSchema(schemaNode, instanceNode)) { + isValid = false; } } - return new Output(isValid, dependentSchemasNode, instanceNode); + return isValid; }); keywordHandlers.set("then", (thenNode, instanceNode, schemaNode) => { if (jsonObjectHas("if", schemaNode)) { const ifNode = jsonPointerStep("if", schemaNode); - const schemaOutput = validateSchema(ifNode, instanceNode); - if (schemaOutput.valid) { + if (validateSchema(ifNode, instanceNode)) { return validateSchema(thenNode, instanceNode); } } - return new Output(true, thenNode, instanceNode); + return true; }); keywordHandlers.set("else", (elseNode, instanceNode, schemaNode) => { if (jsonObjectHas("if", schemaNode)) { const ifNode = jsonPointerStep("if", schemaNode); - const schemaOutput = validateSchema(ifNode, instanceNode); - if (!schemaOutput.valid) { + if (!validateSchema(ifNode, instanceNode)) { return validateSchema(elseNode, instanceNode); } } - return new Output(true, elseNode, instanceNode); + return true; }); keywordHandlers.set("items", (itemsNode, instanceNode, schemaNode) => { if (instanceNode.jsonType !== "array") { - return new Output(true, itemsNode, instanceNode); + return true; } let numberOfPrefixItems = 0; @@ -292,18 +282,17 @@ keywordHandlers.set("items", (itemsNode, instanceNode, schemaNode) => { let isValid = true; for (const itemNode of instanceNode.children.slice(numberOfPrefixItems)) { - const schemaOutput = validateSchema(itemsNode, itemNode); - if (!schemaOutput.valid) { + if (!validateSchema(itemsNode, itemNode)) { isValid = false; } } - return new Output(isValid, itemsNode, instanceNode); + return isValid; }); keywordHandlers.set("patternProperties", (patternPropertiesNode, instanceNode) => { if (instanceNode.jsonType !== "object") { - return new Output(true, patternPropertiesNode, instanceNode); + return true; } assertNodeType(patternPropertiesNode, "object"); @@ -315,41 +304,35 @@ keywordHandlers.set("patternProperties", (patternPropertiesNode, instanceNode) = for (const propertyNode of instanceNode.children) { const [propertyNameNode, propertyValueNode] = propertyNode.children; const propertyName = propertyNameNode.value; - if (pattern.test(propertyName)) { - const schemaOutput = validateSchema(patternSchemaNode, propertyValueNode); - if (!schemaOutput.valid) { - isValid = false; - } + if (pattern.test(propertyName) && !validateSchema(patternSchemaNode, propertyValueNode)) { + isValid = false; } } } - return new Output(isValid, patternPropertiesNode, instanceNode); + return isValid; }); keywordHandlers.set("prefixItems", (prefixItemsNode, instanceNode) => { if (instanceNode.jsonType !== "array") { - return new Output(true, prefixItemsNode, instanceNode); + return true; } assertNodeType(prefixItemsNode, "array"); let isValid = true; for (let index = 0; index < instanceNode.children.length; index++) { - if (prefixItemsNode.children[index]) { - const schemaOutput = validateSchema(prefixItemsNode.children[index], instanceNode.children[index]); - if (!schemaOutput.valid) { - isValid = false; - } + if (prefixItemsNode.children[index] && !validateSchema(prefixItemsNode.children[index], instanceNode.children[index])) { + isValid = false; } } - return new Output(isValid, prefixItemsNode, instanceNode); + return isValid; }); keywordHandlers.set("properties", (propertiesNode, instanceNode) => { if (instanceNode.jsonType !== "object") { - return new Output(true, propertiesNode, instanceNode); + return true; } assertNodeType(propertiesNode, "object"); @@ -359,19 +342,18 @@ keywordHandlers.set("properties", (propertiesNode, instanceNode) => { const [propertyNameNode, instancePropertyNode] = jsonPropertyNode.children; if (jsonObjectHas(propertyNameNode.value, propertiesNode)) { const schemaPropertyNode = jsonPointerStep(propertyNameNode.value, propertiesNode); - const schemaOutput = validateSchema(schemaPropertyNode, instancePropertyNode); - if (!schemaOutput.valid) { + if (!validateSchema(schemaPropertyNode, instancePropertyNode)) { isValid = false; } } } - return new Output(isValid, propertiesNode, instanceNode); + return isValid; }); keywordHandlers.set("propertyNames", (propertyNamesNode, instanceNode) => { if (instanceNode.jsonType !== "object") { - return new Output(true, propertyNamesNode, instanceNode); + return true; } let isValid = true; @@ -383,23 +365,21 @@ keywordHandlers.set("propertyNames", (propertyNamesNode, instanceNode) => { value: propertyNode.children[0].value, location: JsonPointer.append(propertyNode.children[0].value, instanceNode.location) }; - const schemaOutput = validateSchema(propertyNamesNode, keyNode); - if (!schemaOutput.valid) { + if (!validateSchema(propertyNamesNode, keyNode)) { isValid = false; } } - return new Output(isValid, propertyNamesNode, instanceNode); + return isValid; }); keywordHandlers.set("const", (constNode, instanceNode) => { - const isValid = jsonStringify(jsonValue(instanceNode)) === jsonStringify(jsonValue(constNode)); - return new Output(isValid, constNode, instanceNode); + return jsonStringify(jsonValue(instanceNode)) === jsonStringify(jsonValue(constNode)); }); keywordHandlers.set("dependentRequired", (dependentRequiredNode, instanceNode) => { if (instanceNode.jsonType !== "object") { - return new Output(true, dependentRequiredNode, instanceNode); + return true; } assertNodeType(dependentRequiredNode, "object"); @@ -420,7 +400,7 @@ keywordHandlers.set("dependentRequired", (dependentRequiredNode, instanceNode) = } } - return new Output(isValid, dependentRequiredNode, instanceNode); + return isValid; }); keywordHandlers.set("enum", (enumNode, instanceNode) => { @@ -429,132 +409,126 @@ keywordHandlers.set("enum", (enumNode, instanceNode) => { const instanceValue = jsonStringify(jsonValue(instanceNode)); for (const enumItemNode of enumNode.children) { if (jsonStringify(jsonValue(enumItemNode)) === instanceValue) { - return new Output(true, enumNode, instanceNode); + return true; } } - return new Output(false, enumNode, instanceNode); + return false; }); keywordHandlers.set("exclusiveMaximum", (exclusiveMaximumNode, instanceNode) => { if (instanceNode.jsonType !== "number") { - return new Output(true, exclusiveMaximumNode, instanceNode); + return true; } assertNodeType(exclusiveMaximumNode, "number"); const isValid = instanceNode.value < exclusiveMaximumNode.value; - return new Output(isValid, exclusiveMaximumNode, instanceNode); + return isValid; }); keywordHandlers.set("exclusiveMinimum", (exclusiveMinimumNode, instanceNode) => { if (instanceNode.jsonType !== "number") { - return new Output(true, exclusiveMinimumNode, instanceNode); + return true; } assertNodeType(exclusiveMinimumNode, "number"); - const isValid = instanceNode.value > exclusiveMinimumNode.value; - return new Output(isValid, exclusiveMinimumNode, instanceNode); + return instanceNode.value > exclusiveMinimumNode.value; }); keywordHandlers.set("maxItems", (maxItemsNode, instanceNode) => { if (instanceNode.jsonType !== "array") { - return new Output(true, maxItemsNode, instanceNode); + return true; } assertNodeType(maxItemsNode, "number"); const isValid = instanceNode.children.length <= maxItemsNode.value; - return new Output(isValid, maxItemsNode, instanceNode); + return isValid; }); keywordHandlers.set("minItems", (minItemsNode, instanceNode) => { if (instanceNode.jsonType !== "array") { - return new Output(true, minItemsNode, instanceNode); + return true; } assertNodeType(minItemsNode, "number"); - const isValid = instanceNode.children.length >= minItemsNode.value; - return new Output(isValid, minItemsNode, instanceNode); + return instanceNode.children.length >= minItemsNode.value; }); keywordHandlers.set("maxLength", (maxLengthNode, instanceNode) => { if (instanceNode.jsonType !== "string") { - return new Output(true, maxLengthNode, instanceNode); + return true; } assertNodeType(maxLengthNode, "number"); - const isValid = [...instanceNode.value].length <= maxLengthNode.value; - return new Output(isValid, maxLengthNode, instanceNode); + return [...instanceNode.value].length <= maxLengthNode.value; }); keywordHandlers.set("minLength", (minLengthNode, instanceNode) => { if (instanceNode.jsonType !== "string") { - return new Output(true, minLengthNode, instanceNode); + return true; } assertNodeType(minLengthNode, "number"); - const isValid = [...instanceNode.value].length >= minLengthNode.value; - return new Output(isValid, minLengthNode, instanceNode); + return [...instanceNode.value].length >= minLengthNode.value; }); keywordHandlers.set("maxProperties", (maxPropertiesNode, instanceNode) => { if (instanceNode.jsonType !== "object") { - return new Output(true, maxPropertiesNode, instanceNode); + return true; } assertNodeType(maxPropertiesNode, "number"); const isValid = instanceNode.children.length <= maxPropertiesNode.value; - return new Output(isValid, maxPropertiesNode, instanceNode); + return isValid; }); keywordHandlers.set("minProperties", (minPropertiesNode, instanceNode) => { if (instanceNode.jsonType !== "object") { - return new Output(true, minPropertiesNode, instanceNode); + return true; } assertNodeType(minPropertiesNode, "number"); const isValid = instanceNode.children.length >= minPropertiesNode.value; - return new Output(isValid, minPropertiesNode, instanceNode); + return isValid; }); keywordHandlers.set("maximum", (maximumNode, instanceNode) => { if (instanceNode.jsonType !== "number") { - return new Output(true, maximumNode, instanceNode); + return true; } assertNodeType(maximumNode, "number"); const isValid = instanceNode.value <= maximumNode.value; - return new Output(isValid, maximumNode, instanceNode); + return isValid; }); keywordHandlers.set("minimum", (minimumNode, instanceNode) => { if (instanceNode.jsonType !== "number") { - return new Output(true, minimumNode, instanceNode); + return true; } assertNodeType(minimumNode, "number"); - const isValid = instanceNode.value >= minimumNode.value; - return new Output(isValid, minimumNode, instanceNode); + return instanceNode.value >= minimumNode.value; }); keywordHandlers.set("multipleOf", (multipleOfNode, instanceNode) => { if (instanceNode.jsonType !== "number") { - return new Output(true, multipleOfNode, instanceNode); + return true; } assertNodeType(multipleOfNode, "number"); const remainder = instanceNode.value % multipleOfNode.value; - const isValid = numberEqual(0, remainder) || numberEqual(multipleOfNode.value, remainder); - return new Output(isValid, multipleOfNode, instanceNode); + return numberEqual(0, remainder) || numberEqual(multipleOfNode.value, remainder); }); /** @type (a: number, b: number) => boolean */ @@ -562,34 +536,33 @@ const numberEqual = (a, b) => Math.abs(a - b) < 1.19209290e-7; keywordHandlers.set("pattern", (patternNode, instanceNode) => { if (instanceNode.jsonType !== "string") { - return new Output(true, patternNode, instanceNode); + return true; } assertNodeType(patternNode, "string"); - const isValid = new RegExp(patternNode.value, "u").test(instanceNode.value); - return new Output(isValid, patternNode, instanceNode); + return new RegExp(patternNode.value, "u").test(instanceNode.value); }); keywordHandlers.set("required", (requiredNode, instanceNode) => { if (instanceNode.jsonType !== "object") { - return new Output(true, requiredNode, instanceNode); + return true; } assertNodeType(requiredNode, "array"); for (const requiredPropertyNode of requiredNode.children) { assertNodeType(requiredPropertyNode, "string"); if (!jsonObjectHas(requiredPropertyNode.value, instanceNode)) { - return new Output(false, requiredNode, instanceNode); + return false; } } - return new Output(true, requiredNode, instanceNode); + return true; }); keywordHandlers.set("type", (typeNode, instanceNode) => { if (typeNode.type === "json") { if (typeNode.jsonType === "string") { - return new Output(isTypeOf(instanceNode, typeNode.value), typeNode, instanceNode); + return isTypeOf(instanceNode, typeNode.value); } if (typeNode.jsonType === "array") { @@ -599,11 +572,11 @@ keywordHandlers.set("type", (typeNode, instanceNode) => { } if (isTypeOf(instanceNode, itemNode.value)) { - return new Output(true, typeNode, instanceNode); + return true; } } - return new Output(false, typeNode, instanceNode); + return false; } } @@ -617,18 +590,17 @@ const isTypeOf = (instance, type) => type === "integer" keywordHandlers.set("uniqueItems", (uniqueItemsNode, instanceNode) => { if (instanceNode.jsonType !== "array") { - return new Output(true, uniqueItemsNode, instanceNode); + return true; } assertNodeType(uniqueItemsNode, "boolean"); if (uniqueItemsNode.value === false) { - return new Output(true, uniqueItemsNode, instanceNode); + return true; } const normalizedItems = instanceNode.children.map((itemNode) => jsonStringify(jsonValue(itemNode))); - const isValid = new Set(normalizedItems).size === normalizedItems.length; - return new Output(isValid, uniqueItemsNode, instanceNode); + return new Set(normalizedItems).size === normalizedItems.length; }); keywordHandlers.set("$id", (idNode, instanceNode, schemaNode) => { @@ -636,7 +608,7 @@ keywordHandlers.set("$id", (idNode, instanceNode, schemaNode) => { throw Error(`Embedded schemas are not supported. Found at ${schemaNode.location}`); } - return new Output(true, idNode, instanceNode); + return true; }); keywordHandlers.set("$anchor", (anchorNode) => { diff --git a/src/output.js b/src/output.js deleted file mode 100644 index 558247e..0000000 --- a/src/output.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @import { JsonNode } from "./jsonast.d.ts" - */ - - -export class Output { - valid; - instanceLocation; - absoluteKeywordLocation; - errors; - - /** - * @param {boolean} valid - * @param {JsonNode} keywordNode - * @param {JsonNode} instanceNode - * @param {Output[]} [errors] - */ - constructor(valid, keywordNode, instanceNode, errors) { - this.valid = valid; - this.absoluteKeywordLocation = keywordNode.location; - this.instanceLocation = instanceNode.location; - - if (errors) { - this.errors = errors; - } - } -} From 018aedc82a621eeed8635dce7aa1b26c7b1e7a6b Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 8 Apr 2025 19:10:26 -0700 Subject: [PATCH 2/3] Minor cleanup and refactoring --- src/index.js | 87 +++++++++++++++------------------------------ src/jsonast-util.js | 14 ++++---- 2 files changed, 37 insertions(+), 64 deletions(-) diff --git a/src/index.js b/src/index.js index 90c1bc3..0b5064b 100644 --- a/src/index.js +++ b/src/index.js @@ -12,12 +12,7 @@ import { } from "./jsonast-util.js"; /** - * @import { - * Json, - * JsonNode, - * JsonObjectNode, - * JsonStringNode - * } from "./jsonast.d.ts" + * @import { Json, JsonNode, JsonObjectNode, JsonStringNode } from "./jsonast.d.ts" */ @@ -139,10 +134,8 @@ keywordHandlers.set("additionalProperties", (additionalPropertiesNode, instanceN let isValid = true; for (const propertyNode of instanceNode.children) { const [propertyNameNode, instancePropertyNode] = propertyNode.children; - if (!isDefinedProperty.test(propertyNameNode.value)) { - if (!validateSchema(additionalPropertiesNode, instancePropertyNode)) { - isValid = false; - } + if (!isDefinedProperty.test(propertyNameNode.value) && !validateSchema(additionalPropertiesNode, instancePropertyNode)) { + isValid = false; } } @@ -384,35 +377,25 @@ keywordHandlers.set("dependentRequired", (dependentRequiredNode, instanceNode) = assertNodeType(dependentRequiredNode, "object"); - let isValid = true; - for (const propertyNode of dependentRequiredNode.children) { + return dependentRequiredNode.children.every((propertyNode) => { const [keyNode, requiredPropertiesNode] = propertyNode.children; - if (jsonObjectHas(keyNode.value, instanceNode)) { - assertNodeType(requiredPropertiesNode, "array"); - const isConditionValid = requiredPropertiesNode.children.every((requiredPropertyNode) => { - assertNodeType(requiredPropertyNode, "string"); - return jsonObjectHas(requiredPropertyNode.value, instanceNode); - }); - - if (!isConditionValid) { - isValid = false; - } + if (!jsonObjectHas(keyNode.value, instanceNode)) { + return true; } - } - return isValid; + assertNodeType(requiredPropertiesNode, "array"); + return requiredPropertiesNode.children.every((requiredPropertyNode) => { + assertNodeType(requiredPropertyNode, "string"); + return jsonObjectHas(requiredPropertyNode.value, instanceNode); + }); + }); }); keywordHandlers.set("enum", (enumNode, instanceNode) => { assertNodeType(enumNode, "array"); const instanceValue = jsonStringify(jsonValue(instanceNode)); - for (const enumItemNode of enumNode.children) { - if (jsonStringify(jsonValue(enumItemNode)) === instanceValue) { - return true; - } - } - return false; + return enumNode.children.some((enumItemNode) => jsonStringify(jsonValue(enumItemNode)) === instanceValue); }); keywordHandlers.set("exclusiveMaximum", (exclusiveMaximumNode, instanceNode) => { @@ -422,8 +405,7 @@ keywordHandlers.set("exclusiveMaximum", (exclusiveMaximumNode, instanceNode) => assertNodeType(exclusiveMaximumNode, "number"); - const isValid = instanceNode.value < exclusiveMaximumNode.value; - return isValid; + return instanceNode.value < exclusiveMaximumNode.value; }); keywordHandlers.set("exclusiveMinimum", (exclusiveMinimumNode, instanceNode) => { @@ -443,8 +425,7 @@ keywordHandlers.set("maxItems", (maxItemsNode, instanceNode) => { assertNodeType(maxItemsNode, "number"); - const isValid = instanceNode.children.length <= maxItemsNode.value; - return isValid; + return instanceNode.children.length <= maxItemsNode.value; }); keywordHandlers.set("minItems", (minItemsNode, instanceNode) => { @@ -484,8 +465,7 @@ keywordHandlers.set("maxProperties", (maxPropertiesNode, instanceNode) => { assertNodeType(maxPropertiesNode, "number"); - const isValid = instanceNode.children.length <= maxPropertiesNode.value; - return isValid; + return instanceNode.children.length <= maxPropertiesNode.value; }); keywordHandlers.set("minProperties", (minPropertiesNode, instanceNode) => { @@ -495,8 +475,7 @@ keywordHandlers.set("minProperties", (minPropertiesNode, instanceNode) => { assertNodeType(minPropertiesNode, "number"); - const isValid = instanceNode.children.length >= minPropertiesNode.value; - return isValid; + return instanceNode.children.length >= minPropertiesNode.value; }); keywordHandlers.set("maximum", (maximumNode, instanceNode) => { @@ -506,8 +485,7 @@ keywordHandlers.set("maximum", (maximumNode, instanceNode) => { assertNodeType(maximumNode, "number"); - const isValid = instanceNode.value <= maximumNode.value; - return isValid; + return instanceNode.value <= maximumNode.value; }); keywordHandlers.set("minimum", (minimumNode, instanceNode) => { @@ -550,6 +528,7 @@ keywordHandlers.set("required", (requiredNode, instanceNode) => { } assertNodeType(requiredNode, "array"); + for (const requiredPropertyNode of requiredNode.children) { assertNodeType(requiredPropertyNode, "string"); if (!jsonObjectHas(requiredPropertyNode.value, instanceNode)) { @@ -560,27 +539,19 @@ keywordHandlers.set("required", (requiredNode, instanceNode) => { }); keywordHandlers.set("type", (typeNode, instanceNode) => { - if (typeNode.type === "json") { - if (typeNode.jsonType === "string") { + switch (typeNode.jsonType) { + case "string": return isTypeOf(instanceNode, typeNode.value); - } - - if (typeNode.jsonType === "array") { - for (const itemNode of typeNode.children) { - if (itemNode.type !== "json" || itemNode.jsonType != "string") { - throw Error("Invalid Schema"); - } - if (isTypeOf(instanceNode, itemNode.value)) { - return true; - } - } + case "array": + return typeNode.children.some((itemNode) => { + assertNodeType(itemNode, "string"); + return isTypeOf(instanceNode, itemNode.value); + }); - return false; - } + default: + throw Error("Invalid Schema"); } - - throw Error("Invalid Schema"); }); /** @type (instanceNode: JsonNode, type: string) => boolean */ @@ -603,7 +574,7 @@ keywordHandlers.set("uniqueItems", (uniqueItemsNode, instanceNode) => { return new Set(normalizedItems).size === normalizedItems.length; }); -keywordHandlers.set("$id", (idNode, instanceNode, schemaNode) => { +keywordHandlers.set("$id", (idNode, _instanceNode, schemaNode) => { if (!idNode.location.endsWith("#/$id")) { throw Error(`Embedded schemas are not supported. Found at ${schemaNode.location}`); } diff --git a/src/jsonast-util.js b/src/jsonast-util.js index 9fb40de..7245900 100644 --- a/src/jsonast-util.js +++ b/src/jsonast-util.js @@ -19,16 +19,18 @@ import * as JsonPointer from "@hyperjump/json-pointer"; /** @type (json: Json, uri?: string, pointer?: string) => JsonNode */ export const toJsonNode = (json, uri = "", pointer = "") => { + const location = `${uri}#${encodeURI(pointer)}`; + switch (typeof json) { case "boolean": - return { type: "json", jsonType: "boolean", value: json, location: `${uri}#${pointer}` }; + return { type: "json", jsonType: "boolean", value: json, location: location }; case "number": - return { type: "json", jsonType: "number", value: json, location: `${uri}#${pointer}` }; + return { type: "json", jsonType: "number", value: json, location: location }; case "string": - return { type: "json", jsonType: "string", value: json, location: `${uri}#${pointer}` }; + return { type: "json", jsonType: "string", value: json, location: location }; case "object": if (json === null) { - return { type: "json", jsonType: "null", value: json, location: `${uri}#${pointer}` }; + return { type: "json", jsonType: "null", value: json, location: location }; } else if (Array.isArray(json)) { return { type: "json", @@ -40,7 +42,7 @@ export const toJsonNode = (json, uri = "", pointer = "") => { }; } else { /** @type JsonObjectNode */ - const objectNode = { type: "json", jsonType: "object", children: [], location: `${uri}#${pointer}` }; + const objectNode = { type: "json", jsonType: "object", children: [], location: location }; for (const property in json) { /** @type JsonPropertyNode */ @@ -87,7 +89,7 @@ export const jsonPointerStep = (segment, node, uri = "#") => { }; /** @type (pointer: string, tree: JsonNode, uri?: string) => JsonNode */ -export const jsonPointerGet = (pointer, tree, uri) => { +export const jsonPointerGet = (pointer, tree, uri = "") => { let currentPointer = ""; let node = tree; for (const segment of pointerSegments(pointer)) { From d15b71a5bad6aab61a4f27ff8192957dcd6ddd01 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Tue, 8 Apr 2025 19:12:13 -0700 Subject: [PATCH 3/3] Implement the Basic output format --- src/index.js | 108 +++-- src/output.test.js | 1109 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1178 insertions(+), 39 deletions(-) create mode 100644 src/output.test.js diff --git a/src/index.js b/src/index.js index 0b5064b..4cf93a8 100644 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,17 @@ import { /** * @typedef {{ - * valid: boolean; + * instanceLocation: string; + * absoluteKeywordLocation: string; + * keywordLocation?: string; + * error?: string; + * }} OutputUnit + * + * @typedef {{ + * valid: true; + * } | { + * valid: false; + * errors?: OutputUnit[]; * }} Output */ @@ -39,26 +49,44 @@ export const validate = (schema, instance) => { } } - const valid = validateSchema(schemaNode, toJsonNode(instance)); + /** @type OutputUnit[] */ + const errors = []; + const valid = validateSchema(schemaNode, toJsonNode(instance), errors); schemaRegistry.delete(uri); - return { valid }; + return valid ? { valid } : { valid, errors }; }; -/** @type (schemaNode: JsonNode, instanceNode: JsonNode) => boolean */ -const validateSchema = (schemaNode, instanceNode) => { +/** @type (schemaNode: JsonNode, instanceNode: JsonNode, errors: OutputUnit[]) => boolean */ +const validateSchema = (schemaNode, instanceNode, errors) => { if (schemaNode.type === "json") { switch (schemaNode.jsonType) { case "boolean": + if (!schemaNode.value) { + errors.push({ + absoluteKeywordLocation: schemaNode.location, + instanceLocation: instanceNode.location + }); + } return schemaNode.value; + case "object": let isValid = true; for (const propertyNode of schemaNode.children) { const [keywordNode, keywordValueNode] = propertyNode.children; const keywordHandler = keywordHandlers.get(keywordNode.value); - if (keywordHandler && !keywordHandler(keywordValueNode, instanceNode, schemaNode)) { - isValid = false; + if (keywordHandler) { + /** @type OutputUnit[] */ + const keywordErrors = []; + if (!keywordHandler(keywordValueNode, instanceNode, schemaNode, keywordErrors)) { + isValid = false; + errors.push({ + absoluteKeywordLocation: keywordValueNode.location, + instanceLocation: instanceNode.location + }); + errors.push(...keywordErrors); + } } } @@ -81,14 +109,15 @@ export const registerSchema = (schema, uri) => { * @typedef {( * keywordNode: JsonNode, * instanceNode: JsonNode, - * schemaNode: JsonObjectNode + * schemaNode: JsonObjectNode, + * errors: OutputUnit[], * ) => boolean} KeywordHandler */ /** @type Map */ const keywordHandlers = new Map(); -keywordHandlers.set("$ref", (refNode, instanceNode) => { +keywordHandlers.set("$ref", (refNode, instanceNode, _schemaNode, errors) => { assertNodeType(refNode, "string"); const uri = refNode.location.startsWith("#") @@ -103,10 +132,10 @@ keywordHandlers.set("$ref", (refNode, instanceNode) => { const pointer = decodeURI(parseIriReference(refNode.value).fragment ?? ""); const referencedSchemaNode = jsonPointerGet(pointer, schemaNode, uri); - return validateSchema(referencedSchemaNode, instanceNode); + return validateSchema(referencedSchemaNode, instanceNode, errors); }); -keywordHandlers.set("additionalProperties", (additionalPropertiesNode, instanceNode, schemaNode) => { +keywordHandlers.set("additionalProperties", (additionalPropertiesNode, instanceNode, schemaNode, errors) => { if (instanceNode.jsonType !== "object") { return true; } @@ -134,7 +163,7 @@ keywordHandlers.set("additionalProperties", (additionalPropertiesNode, instanceN let isValid = true; for (const propertyNode of instanceNode.children) { const [propertyNameNode, instancePropertyNode] = propertyNode.children; - if (!isDefinedProperty.test(propertyNameNode.value) && !validateSchema(additionalPropertiesNode, instancePropertyNode)) { + if (!isDefinedProperty.test(propertyNameNode.value) && !validateSchema(additionalPropertiesNode, instancePropertyNode, errors)) { isValid = false; } } @@ -147,12 +176,12 @@ const regexEscape = (string) => string .replace(/[|\\{}()[\]^$+*?.]/g, "\\$&") .replace(/-/g, "\\x2d"); -keywordHandlers.set("allOf", (allOfNode, instanceNode) => { +keywordHandlers.set("allOf", (allOfNode, instanceNode, _schemaNode, errors) => { assertNodeType(allOfNode, "array"); let isValid = true; for (const schemaNode of allOfNode.children) { - if (!validateSchema(schemaNode, instanceNode)) { + if (!validateSchema(schemaNode, instanceNode, errors)) { isValid = false; } } @@ -160,24 +189,25 @@ keywordHandlers.set("allOf", (allOfNode, instanceNode) => { return isValid; }); -keywordHandlers.set("anyOf", (anyOfNode, instanceNode) => { +keywordHandlers.set("anyOf", (anyOfNode, instanceNode, _schemaNode, errors) => { assertNodeType(anyOfNode, "array"); let isValid = false; for (const schemaNode of anyOfNode.children) { - if (validateSchema(schemaNode, instanceNode)) { + if (validateSchema(schemaNode, instanceNode, errors)) { isValid = true; } } + return isValid; }); -keywordHandlers.set("oneOf", (oneOfNode, instanceNode) => { +keywordHandlers.set("oneOf", (oneOfNode, instanceNode, _schemaNode, errors) => { assertNodeType(oneOfNode, "array"); let matches = 0; for (const schemaNode of oneOfNode.children) { - if (validateSchema(schemaNode, instanceNode)) { + if (validateSchema(schemaNode, instanceNode, errors)) { matches++; } } @@ -186,10 +216,10 @@ keywordHandlers.set("oneOf", (oneOfNode, instanceNode) => { }); keywordHandlers.set("not", (notNode, instanceNode) => { - return !validateSchema(notNode, instanceNode); + return !validateSchema(notNode, instanceNode, []); }); -keywordHandlers.set("contains", (containsNode, instanceNode, schemaNode) => { +keywordHandlers.set("contains", (containsNode, instanceNode, schemaNode, errors) => { if (instanceNode.jsonType !== "array") { return true; } @@ -212,7 +242,7 @@ keywordHandlers.set("contains", (containsNode, instanceNode, schemaNode) => { let matches = 0; for (const itemNode of instanceNode.children) { - if (validateSchema(containsNode, itemNode)) { + if (validateSchema(containsNode, itemNode, errors)) { matches++; } } @@ -220,7 +250,7 @@ keywordHandlers.set("contains", (containsNode, instanceNode, schemaNode) => { return matches >= minContains && matches <= maxContains; }); -keywordHandlers.set("dependentSchemas", (dependentSchemasNode, instanceNode) => { +keywordHandlers.set("dependentSchemas", (dependentSchemasNode, instanceNode, _schemaNode, errors) => { if (instanceNode.jsonType !== "object") { return true; } @@ -230,7 +260,7 @@ keywordHandlers.set("dependentSchemas", (dependentSchemasNode, instanceNode) => let isValid = true; for (const propertyNode of dependentSchemasNode.children) { const [keyNode, schemaNode] = propertyNode.children; - if (jsonObjectHas(keyNode.value, instanceNode) && !validateSchema(schemaNode, instanceNode)) { + if (jsonObjectHas(keyNode.value, instanceNode) && !validateSchema(schemaNode, instanceNode, errors)) { isValid = false; } } @@ -238,29 +268,29 @@ keywordHandlers.set("dependentSchemas", (dependentSchemasNode, instanceNode) => return isValid; }); -keywordHandlers.set("then", (thenNode, instanceNode, schemaNode) => { +keywordHandlers.set("then", (thenNode, instanceNode, schemaNode, errors) => { if (jsonObjectHas("if", schemaNode)) { const ifNode = jsonPointerStep("if", schemaNode); - if (validateSchema(ifNode, instanceNode)) { - return validateSchema(thenNode, instanceNode); + if (validateSchema(ifNode, instanceNode, [])) { + return validateSchema(thenNode, instanceNode, errors); } } return true; }); -keywordHandlers.set("else", (elseNode, instanceNode, schemaNode) => { +keywordHandlers.set("else", (elseNode, instanceNode, schemaNode, errors) => { if (jsonObjectHas("if", schemaNode)) { const ifNode = jsonPointerStep("if", schemaNode); - if (!validateSchema(ifNode, instanceNode)) { - return validateSchema(elseNode, instanceNode); + if (!validateSchema(ifNode, instanceNode, [])) { + return validateSchema(elseNode, instanceNode, errors); } } return true; }); -keywordHandlers.set("items", (itemsNode, instanceNode, schemaNode) => { +keywordHandlers.set("items", (itemsNode, instanceNode, schemaNode, errors) => { if (instanceNode.jsonType !== "array") { return true; } @@ -275,7 +305,7 @@ keywordHandlers.set("items", (itemsNode, instanceNode, schemaNode) => { let isValid = true; for (const itemNode of instanceNode.children.slice(numberOfPrefixItems)) { - if (!validateSchema(itemsNode, itemNode)) { + if (!validateSchema(itemsNode, itemNode, errors)) { isValid = false; } } @@ -283,7 +313,7 @@ keywordHandlers.set("items", (itemsNode, instanceNode, schemaNode) => { return isValid; }); -keywordHandlers.set("patternProperties", (patternPropertiesNode, instanceNode) => { +keywordHandlers.set("patternProperties", (patternPropertiesNode, instanceNode, _schemaNode, errors) => { if (instanceNode.jsonType !== "object") { return true; } @@ -297,7 +327,7 @@ keywordHandlers.set("patternProperties", (patternPropertiesNode, instanceNode) = for (const propertyNode of instanceNode.children) { const [propertyNameNode, propertyValueNode] = propertyNode.children; const propertyName = propertyNameNode.value; - if (pattern.test(propertyName) && !validateSchema(patternSchemaNode, propertyValueNode)) { + if (pattern.test(propertyName) && !validateSchema(patternSchemaNode, propertyValueNode, errors)) { isValid = false; } } @@ -306,7 +336,7 @@ keywordHandlers.set("patternProperties", (patternPropertiesNode, instanceNode) = return isValid; }); -keywordHandlers.set("prefixItems", (prefixItemsNode, instanceNode) => { +keywordHandlers.set("prefixItems", (prefixItemsNode, instanceNode, _schemaNode, errors) => { if (instanceNode.jsonType !== "array") { return true; } @@ -315,7 +345,7 @@ keywordHandlers.set("prefixItems", (prefixItemsNode, instanceNode) => { let isValid = true; for (let index = 0; index < instanceNode.children.length; index++) { - if (prefixItemsNode.children[index] && !validateSchema(prefixItemsNode.children[index], instanceNode.children[index])) { + if (prefixItemsNode.children[index] && !validateSchema(prefixItemsNode.children[index], instanceNode.children[index], errors)) { isValid = false; } } @@ -323,7 +353,7 @@ keywordHandlers.set("prefixItems", (prefixItemsNode, instanceNode) => { return isValid; }); -keywordHandlers.set("properties", (propertiesNode, instanceNode) => { +keywordHandlers.set("properties", (propertiesNode, instanceNode, _schemaNode, errors) => { if (instanceNode.jsonType !== "object") { return true; } @@ -335,7 +365,7 @@ keywordHandlers.set("properties", (propertiesNode, instanceNode) => { const [propertyNameNode, instancePropertyNode] = jsonPropertyNode.children; if (jsonObjectHas(propertyNameNode.value, propertiesNode)) { const schemaPropertyNode = jsonPointerStep(propertyNameNode.value, propertiesNode); - if (!validateSchema(schemaPropertyNode, instancePropertyNode)) { + if (!validateSchema(schemaPropertyNode, instancePropertyNode, errors)) { isValid = false; } } @@ -344,7 +374,7 @@ keywordHandlers.set("properties", (propertiesNode, instanceNode) => { return isValid; }); -keywordHandlers.set("propertyNames", (propertyNamesNode, instanceNode) => { +keywordHandlers.set("propertyNames", (propertyNamesNode, instanceNode, _schemaNode, errors) => { if (instanceNode.jsonType !== "object") { return true; } @@ -358,7 +388,7 @@ keywordHandlers.set("propertyNames", (propertyNamesNode, instanceNode) => { value: propertyNode.children[0].value, location: JsonPointer.append(propertyNode.children[0].value, instanceNode.location) }; - if (!validateSchema(propertyNamesNode, keyNode)) { + if (!validateSchema(propertyNamesNode, keyNode, errors)) { isValid = false; } } diff --git a/src/output.test.js b/src/output.test.js new file mode 100644 index 0000000..9e92aa0 --- /dev/null +++ b/src/output.test.js @@ -0,0 +1,1109 @@ +import { describe, expect, test } from "vitest"; +import { validate } from "./index.js"; + + +describe("Basic Output Format", () => { + describe("$ref", () => { + test("invalid", () => { + const output = validate({ + $ref: "#/$defs/string", + $defs: { + string: { type: "string" } + } + }, 42); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/$ref", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/$defs/string/type", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + $ref: "#/$defs/string", + $defs: { + string: { type: "string" } + } + }, "foo"); + + expect(output).to.eql({ valid: true }); + }); + }); + + describe("additionalProperties", () => { + test("invalid", () => { + const output = validate({ additionalProperties: false }, { foo: 42 }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/additionalProperties", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/additionalProperties", + instanceLocation: "#/foo" + } + ] + }); + }); + + test("invalid - multiple errors", () => { + const output = validate({ additionalProperties: false }, { foo: 42, bar: 24 }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/additionalProperties", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/additionalProperties", + instanceLocation: "#/foo" + }, + { + absoluteKeywordLocation: "#/additionalProperties", + instanceLocation: "#/bar" + } + ] + }); + }); + + test("invalid - schema", () => { + const output = validate({ + additionalProperties: { type: "string" } + }, { foo: 42 }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/additionalProperties", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/additionalProperties/type", + instanceLocation: "#/foo" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ additionalProperties: true }, {}); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("allOf", () => { + test("invalid", () => { + const output = validate({ + allOf: [ + { type: "number" }, + { maximum: 5 } + ] + }, 42); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/allOf", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/allOf/1/maximum", + instanceLocation: "#" + } + ] + }); + }); + + test("invalid - multiple errors", () => { + const output = validate({ + type: "number", + allOf: [ + { maximum: 2 }, + { maximum: 5 } + ] + }, 42); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/allOf", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/allOf/0/maximum", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/allOf/1/maximum", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + allOf: [ + { type: "number" }, + { maximum: 5 } + ] + }, 3); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("anyOf", () => { + test("invalid", () => { + const output = validate({ + anyOf: [ + { type: "string" }, + { type: "number" } + ] + }, true); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/anyOf", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/anyOf/0/type", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/anyOf/1/type", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + anyOf: [ + { type: "string" }, + { type: "number" } + ] + }, "foo"); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("oneOf", () => { + test("invalid", () => { + const output = validate({ + oneOf: [ + { type: "string" }, + { type: "number" } + ] + }, true); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/oneOf", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/oneOf/0/type", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/oneOf/1/type", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + oneOf: [ + { type: "string" }, + { type: "number" } + ] + }, "foo"); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("not", () => { + test("invalid", () => { + const output = validate({ + not: { type: "number" } + }, 42); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/not", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + not: { type: "number" } + }, "foo"); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("contains", () => { + test("invalid", () => { + const output = validate({ + contains: { type: "string" } + }, [1, 2]); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/contains", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/contains/type", + instanceLocation: "#/0" + }, + { + absoluteKeywordLocation: "#/contains/type", + instanceLocation: "#/1" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + contains: { type: "string" } + }, [1, "foo"]); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("dependentSchemas", () => { + test("invalid", () => { + const output = validate({ + dependentSchemas: { + foo: { required: ["a"] } + } + }, { foo: 42 }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/dependentSchemas", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/dependentSchemas/foo/required", + instanceLocation: "#" + } + ] + }); + }); + + test("invalid - multiple conditions fail", () => { + const output = validate({ + dependentSchemas: { + foo: { required: ["a"] }, + bar: { required: ["b"] } + } + }, { foo: 42, bar: 24 }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/dependentSchemas", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/dependentSchemas/foo/required", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/dependentSchemas/bar/required", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + dependentSchemas: { + foo: { required: ["a"] } + } + }, { foo: 42, a: true }); + + expect(output).to.eql({ valid: true }); + }); + }); + + describe("then", () => { + test("invalid", () => { + const output = validate({ + if: { type: "string" }, + then: { minLength: 1 } + }, ""); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/then", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/then/minLength", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + if: { type: "string" }, + then: { minLength: 1 } + }, "foo"); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("else", () => { + test("invalid", () => { + const output = validate({ + type: ["string", "number"], + if: { type: "string" }, + else: { minimum: 42 } + }, 5); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/else", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/else/minimum", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + type: ["string", "number"], + if: { type: "string" }, + else: { minimum: 5 } + }, 42); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("items", () => { + test("invalid", () => { + const output = validate({ + items: { type: "string" } + }, [42, 24]); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/items", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/items/type", + instanceLocation: "#/0" + }, + { + absoluteKeywordLocation: "#/items/type", + instanceLocation: "#/1" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + items: { type: "string" } + }, ["foo"]); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("patternProperties", () => { + test("invalid", () => { + const output = validate({ + patternProperties: { + "^f": { type: "string" }, + "^b": { type: "number" } + } + }, { foo: 42, bar: true }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/patternProperties", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/patternProperties/%5Ef/type", + instanceLocation: "#/foo" + }, + { + absoluteKeywordLocation: "#/patternProperties/%5Eb/type", + instanceLocation: "#/bar" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + patternProperties: { + "^f": { type: "string" }, + "^b": { type: "number" } + } + }, { foo: "a", bar: 42 }); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("prefixItems", () => { + test("invalid", () => { + const output = validate({ + prefixItems: [ + { type: "string" }, + { type: "number" } + ] + }, [42, "foo"]); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/prefixItems", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/prefixItems/0/type", + instanceLocation: "#/0" + }, + { + absoluteKeywordLocation: "#/prefixItems/1/type", + instanceLocation: "#/1" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + prefixItems: [ + { type: "string" }, + { type: "number" } + ] + }, ["foo", 42]); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("properties", () => { + test("invalid", () => { + const output = validate({ + properties: { + foo: { type: "string" }, + bar: { type: "number" } + } + }, { foo: 42, bar: true }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/properties", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/properties/foo/type", + instanceLocation: "#/foo" + }, + { + absoluteKeywordLocation: "#/properties/bar/type", + instanceLocation: "#/bar" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + properties: { + foo: { type: "string" }, + bar: { type: "number" } + } + }, { foo: "a", bar: 42 }); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("propertyNames", () => { + test("invalid", () => { + const output = validate({ + propertyNames: { pattern: "^a" } + }, { banana: true, pear: false }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/propertyNames", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/propertyNames/pattern", + instanceLocation: "#/banana" + }, + { + absoluteKeywordLocation: "#/propertyNames/pattern", + instanceLocation: "#/pear" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + propertyNames: { pattern: "^a" } + }, { apple: true }); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("const", () => { + test("invalid", () => { + const output = validate({ const: "foo" }, 42); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/const", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ const: "foo" }, "foo"); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("dependentRequired", () => { + test("invalid", () => { + const output = validate({ + dependentRequired: { + foo: ["a"] + } + }, { foo: 42 }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/dependentRequired", + instanceLocation: "#" + } + ] + }); + }); + + test("invalid - multiple conditions fail", () => { + const output = validate({ + dependentRequired: { + foo: ["a"], + bar: ["b"] + } + }, { foo: 42, bar: 24 }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/dependentRequired", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ + dependentRequired: { + foo: ["a"] + } + }, { foo: 42, a: true }); + + expect(output).to.eql({ valid: true }); + }); + }); + + describe("enum", () => { + test("invalid", () => { + const output = validate({ enum: ["foo"] }, 42); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/enum", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ enum: ["foo"] }, "foo"); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("exclusiveMaximum", () => { + test("invalid", () => { + const output = validate({ exclusiveMaximum: 5 }, 42); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/exclusiveMaximum", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ exclusiveMaximum: 42 }, 5); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("exclusiveMinimum", () => { + test("invalid", () => { + const output = validate({ exclusiveMinimum: 42 }, 5); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/exclusiveMinimum", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ exclusiveMinimum: 5 }, 42); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("maxItems", () => { + test("invalid", () => { + const output = validate({ maxItems: 1 }, [1, 2]); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/maxItems", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ maxItems: 1 }, []); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("minItems", () => { + test("invalid", () => { + const output = validate({ minItems: 1 }, []); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/minItems", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ minItems: 1 }, [1, 2]); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("maxLength", () => { + test("invalid", () => { + const output = validate({ maxLength: 2 }, "foo"); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/maxLength", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ maxLength: 2 }, "a"); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("minLength", () => { + test("invalid", () => { + const output = validate({ minLength: 2 }, "a"); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/minLength", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ minLength: 1 }, "foo"); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("maxProperties", () => { + test("invalid", () => { + const output = validate({ maxProperties: 1 }, { a: 1, b: 2 }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/maxProperties", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ maxProperties: 1 }, {}); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("minProperties", () => { + test("invalid", () => { + const output = validate({ minProperties: 1 }, {}); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/minProperties", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ minProperties: 1 }, { a: 1, b: 2 }); + + expect(output).to.eql({ valid: true }); + }); + }); + + describe("maximum", () => { + test("invalid", () => { + const output = validate({ maximum: 5 }, 42); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/maximum", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ maximum: 42 }, 5); + + expect(output).to.eql({ valid: true }); + }); + }); + + describe("minimum", () => { + test("invalid", () => { + const output = validate({ minimum: 42 }, 5); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/minimum", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ minimum: 5 }, 42); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("multipleOf", () => { + test("invalid", () => { + const output = validate({ multipleOf: 2 }, 3); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/multipleOf", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ multipleOf: 2 }, 4); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("pattern", () => { + test("invalid", () => { + const output = validate({ pattern: "^a" }, "banana"); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/pattern", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ pattern: "^a" }, "apple"); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("required", () => { + test("invalid", () => { + const output = validate({ required: ["a"] }, {}); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/required", + instanceLocation: "#" + } + ] + }); + }); + + test("invalid - multiple missing", () => { + const output = validate({ required: ["a", "b"] }, {}); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/required", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ required: ["a"] }, { a: 1 }); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("type", () => { + test("invalid", () => { + const output = validate({ type: "string" }, 42); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/type", + instanceLocation: "#" + } + ] + }); + }); + + test("invalid - multiple types", () => { + const output = validate({ type: ["string", "null"] }, 42); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/type", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ type: "string" }, "foo"); + expect(output).to.eql({ valid: true }); + }); + }); + + describe("uniqueItems", () => { + test("invalid", () => { + const output = validate({ uniqueItems: true }, [1, 1]); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/uniqueItems", + instanceLocation: "#" + } + ] + }); + }); + + test("valid", () => { + const output = validate({ uniqueItems: true }, [1, 2]); + expect(output).to.eql({ valid: true }); + }); + }); + + test("Multiple errors in schema", () => { + const output = validate({ + properties: { + foo: { type: "string" }, + bar: { type: "boolean" } + }, + required: ["foo", "bar"] + }, { foo: 42 }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/properties", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/properties/foo/type", + instanceLocation: "#/foo" + }, + { + absoluteKeywordLocation: "#/required", + instanceLocation: "#" + } + ] + }); + }); + + test("Deeply nested", () => { + const output = validate({ + properties: { + foo: { + properties: { + bar: { type: "boolean" } + } + } + } + }, { foo: { bar: 42 } }); + + expect(output).to.eql({ + valid: false, + errors: [ + { + absoluteKeywordLocation: "#/properties", + instanceLocation: "#" + }, + { + absoluteKeywordLocation: "#/properties/foo/properties", + instanceLocation: "#/foo" + }, + { + absoluteKeywordLocation: "#/properties/foo/properties/bar/type", + instanceLocation: "#/foo/bar" + } + ] + }); + }); +});