From 0b474751b5fb994d402caf3d0515d95680684b60 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Jan 2020 11:06:03 -0500 Subject: [PATCH 01/17] Add simplified isObject test --- ext/bg/js/json-schema.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 5d596a8b..2b7c9f27 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -131,19 +131,19 @@ class JsonSchemaProxyHandler { case 'object': { const properties = schema.properties; - if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) { + if (JsonSchemaProxyHandler.isObject(properties)) { if (Object.prototype.hasOwnProperty.call(properties, property)) { return properties[property]; } } const additionalProperties = schema.additionalProperties; - return (additionalProperties !== null && typeof additionalProperties === 'object' && !Array.isArray(additionalProperties)) ? additionalProperties : null; + return JsonSchemaProxyHandler.isObject(additionalProperties) ? additionalProperties : null; } case 'array': { const items = schema.items; - return (items !== null && typeof items === 'object' && !Array.isArray(items)) ? items : null; + return JsonSchemaProxyHandler.isObject(items) ? items : null; } default: return null; @@ -399,6 +399,10 @@ class JsonSchemaProxyHandler { return value; } + + static isObject(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); + } } class JsonSchema { From 6595715f7cdf1c4d0de743443b88eba05d7d6ae1 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Jan 2020 10:57:52 -0500 Subject: [PATCH 02/17] Add support for allOf, anyOf, oneOf, and not --- ext/bg/js/json-schema.js | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 2b7c9f27..8129b6d2 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -151,6 +151,70 @@ class JsonSchemaProxyHandler { } static validate(value, schema) { + let result = JsonSchemaProxyHandler.validateSingleSchema(value, schema); + if (result !== null) { return result; } + + result = JsonSchemaProxyHandler.validateAllOf(value, schema); + if (result !== null) { return result; } + + result = JsonSchemaProxyHandler.validateAnyOf(value, schema); + if (result !== null) { return result; } + + result = JsonSchemaProxyHandler.validateOneOf(value, schema); + if (result !== null) { return result; } + + result = JsonSchemaProxyHandler.validateNoneOf(value, schema); + if (result !== null) { return result; } + + return null; + } + + static validateAllOf(value, schema) { + const subSchemas = schema.allOf; + if (!Array.isArray(subSchemas)) { return null; } + + for (let i = 0; i < subSchemas.length; ++i) { + const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]); + if (result !== null) { return `allOf[${i}] schema didn't match: ${result}`; } + } + return null; + } + + static validateAnyOf(value, schema) { + const subSchemas = schema.anyOf; + if (!Array.isArray(subSchemas)) { return null; } + + for (let i = 0; i < subSchemas.length; ++i) { + const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]); + if (result === null) { return null; } + } + return '0 anyOf schemas matched'; + } + + static validateOneOf(value, schema) { + const subSchemas = schema.oneOf; + if (!Array.isArray(subSchemas)) { return null; } + + let count = 0; + for (let i = 0; i < subSchemas.length; ++i) { + const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]); + if (result === null) { ++count; } + } + return count === 1 ? null : `${count} oneOf schemas matched`; + } + + static validateNoneOf(value, schema) { + const subSchemas = schema.not; + if (!Array.isArray(subSchemas)) { return null; } + + for (let i = 0; i < subSchemas.length; ++i) { + const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]); + if (result === null) { return `not[${i}] schema matched`; } + } + return null; + } + + static validateSingleSchema(value, schema) { const type = JsonSchemaProxyHandler.getValueType(value); const schemaType = schema.type; if (!JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType)) { From 203216986e82d8b6c654979791d1ff5172ba83ca Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Jan 2020 11:13:13 -0500 Subject: [PATCH 03/17] Add support for conditionals --- ext/bg/js/json-schema.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 8129b6d2..ab4a4817 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -154,6 +154,9 @@ class JsonSchemaProxyHandler { let result = JsonSchemaProxyHandler.validateSingleSchema(value, schema); if (result !== null) { return result; } + result = JsonSchemaProxyHandler.validateConditional(value, schema); + if (result !== null) { return result; } + result = JsonSchemaProxyHandler.validateAllOf(value, schema); if (result !== null) { return result; } @@ -169,6 +172,25 @@ class JsonSchemaProxyHandler { return null; } + static validateConditional(value, schema) { + const ifCondition = schema.if; + if (!JsonSchemaProxyHandler.isObject(ifCondition)) { return null; } + + const thenSchema = schema.then; + if (JsonSchemaProxyHandler.isObject(thenSchema)) { + const result = JsonSchemaProxyHandler.validate(value, thenSchema); + if (result !== null) { return `then conditional didn't match: ${result}`; } + } + + const elseSchema = schema.else; + if (JsonSchemaProxyHandler.isObject(elseSchema)) { + const result = JsonSchemaProxyHandler.validate(value, thenSchema); + if (result !== null) { return `else conditional didn't match: ${result}`; } + } + + return null; + } + static validateAllOf(value, schema) { const subSchemas = schema.allOf; if (!Array.isArray(subSchemas)) { return null; } From a844698f153773d5255724ba48a00851418af009 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Jan 2020 15:45:31 -0500 Subject: [PATCH 04/17] Return unconstrained schema when additionalProperties is true/undefined --- ext/bg/js/json-schema.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index ab4a4817..7a7f2489 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -138,7 +138,13 @@ class JsonSchemaProxyHandler { } const additionalProperties = schema.additionalProperties; - return JsonSchemaProxyHandler.isObject(additionalProperties) ? additionalProperties : null; + if (additionalProperties === false) { + return null; + } if (JsonSchemaProxyHandler.isObject(additionalProperties)) { + return additionalProperties; + } else { + return JsonSchemaProxyHandler._unconstrainedSchema; + } } case 'array': { @@ -491,6 +497,8 @@ class JsonSchemaProxyHandler { } } +JsonSchemaProxyHandler._unconstrainedSchema = {}; + class JsonSchema { static createProxy(target, schema) { return new Proxy(target, new JsonSchemaProxyHandler(schema)); From 980a1ddf745032a790a043e8040cdb7ec5b110ad Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Jan 2020 12:34:27 -0500 Subject: [PATCH 05/17] Improve support for array schemas --- ext/bg/js/json-schema.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 7a7f2489..f32176fd 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -149,7 +149,23 @@ class JsonSchemaProxyHandler { case 'array': { const items = schema.items; - return JsonSchemaProxyHandler.isObject(items) ? items : null; + if (JsonSchemaProxyHandler.isObject(items)) { + return items; + } + if (Array.isArray(items)) { + if (property >= 0 && property < items.length) { + return items[property]; + } + } + + const additionalItems = schema.additionalItems; + if (additionalItems === false) { + return null; + } else if (JsonSchemaProxyHandler.isObject(additionalItems)) { + return additionalItems; + } else { + return JsonSchemaProxyHandler._unconstrainedSchema; + } } default: return null; @@ -322,6 +338,18 @@ class JsonSchemaProxyHandler { return 'Array length too long'; } + for (let i = 0, ii = value.length; i < ii; ++i) { + const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i, value); + if (propertySchema === null) { + return `No schema found for array[${i}]`; + } + + const error = JsonSchemaProxyHandler.validate(value[i], propertySchema); + if (error !== null) { + return error; + } + } + return null; } From 31dbeab67ce79a9c46053ce124ad3924f71b67cc Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Jan 2020 14:59:37 -0500 Subject: [PATCH 06/17] Add validate on JsonSchema --- ext/bg/js/json-schema.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index f32176fd..61bd3d43 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -532,6 +532,10 @@ class JsonSchema { return new Proxy(target, new JsonSchemaProxyHandler(schema)); } + static validate(value, schema) { + return JsonSchemaProxyHandler.validate(value, schema); + } + static getValidValueOrDefault(schema, value) { return JsonSchemaProxyHandler.getValidValueOrDefault(schema, value); } From 52b623b5cdb1963aa1fb65228f9377e147708959 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Jan 2020 15:57:39 -0500 Subject: [PATCH 07/17] Improve getPropertySchema's type detection --- ext/bg/js/json-schema.js | 42 +++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 61bd3d43..9b651f46 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -64,7 +64,7 @@ class JsonSchemaProxyHandler { } } - const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property); + const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property, target); if (propertySchema === null) { return; } @@ -86,7 +86,7 @@ class JsonSchemaProxyHandler { } } - const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property); + const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property, target); if (propertySchema === null) { throw new Error(`Property ${property} not supported`); } @@ -122,11 +122,8 @@ class JsonSchemaProxyHandler { throw new Error('construct not supported'); } - static getPropertySchema(schema, property) { - const type = schema.type; - if (Array.isArray(type)) { - throw new Error(`Ambiguous property type for ${property}`); - } + static getPropertySchema(schema, property, value) { + const type = JsonSchemaProxyHandler.getSchemaOrValueType(schema, value); switch (type) { case 'object': { @@ -172,6 +169,29 @@ class JsonSchemaProxyHandler { } } + static getSchemaOrValueType(schema, value) { + const type = schema.type; + + if (Array.isArray(type)) { + if (typeof value !== 'undefined') { + const valueType = JsonSchemaProxyHandler.getValueType(value); + if (type.indexOf(valueType) >= 0) { + return valueType; + } + } + throw new Error(`Ambiguous property type for ${property}`); + } + + if (typeof type === 'undefined') { + if (typeof value !== 'undefined') { + return JsonSchemaProxyHandler.getValueType(value); + } + throw new Error(`No property type for ${property}`); + } + + return type; + } + static validate(value, schema) { let result = JsonSchemaProxyHandler.validateSingleSchema(value, schema); if (result !== null) { return result; } @@ -376,7 +396,7 @@ class JsonSchemaProxyHandler { } for (const property of properties) { - const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property); + const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value); if (propertySchema === null) { return `No schema found for ${property}`; } @@ -492,14 +512,14 @@ class JsonSchemaProxyHandler { for (const property of required) { properties.delete(property); - const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property); + const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value); if (propertySchema === null) { continue; } value[property] = JsonSchemaProxyHandler.getValidValueOrDefault(propertySchema, value[property]); } } for (const property of properties) { - const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property); + const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value); if (propertySchema === null) { Reflect.deleteProperty(value, property); } else { @@ -512,7 +532,7 @@ class JsonSchemaProxyHandler { static populateArrayDefaults(value, schema) { for (let i = 0, ii = value.length; i < ii; ++i) { - const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i); + const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i, value); if (propertySchema === null) { continue; } value[i] = JsonSchemaProxyHandler.getValidValueOrDefault(propertySchema, value[i]); } From 0171d86b28e3a4373a2deabb4a4a8cf738ca2743 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 1 Feb 2020 22:20:47 -0500 Subject: [PATCH 08/17] Fix maxLength check --- ext/bg/js/json-schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 9b651f46..29873b52 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -339,7 +339,7 @@ class JsonSchemaProxyHandler { return 'String length too short'; } - const maxLength = schema.minLength; + const maxLength = schema.maxLength; if (typeof maxLength === 'number' && value.length > maxLength) { return 'String length too long'; } From 36e641e00168b09e81aad7e234399d06ce12e619 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 1 Feb 2020 22:57:27 -0500 Subject: [PATCH 09/17] getSchemaOrValueType return null --- ext/bg/js/json-schema.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 29873b52..c20cb502 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -179,14 +179,14 @@ class JsonSchemaProxyHandler { return valueType; } } - throw new Error(`Ambiguous property type for ${property}`); + return null; } if (typeof type === 'undefined') { if (typeof value !== 'undefined') { return JsonSchemaProxyHandler.getValueType(value); } - throw new Error(`No property type for ${property}`); + return null; } return type; From 964db7410863a5b840b101884c3c522389dd4e80 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 2 Feb 2020 00:13:46 -0500 Subject: [PATCH 10/17] Update schema validation to throw errors --- ext/bg/js/json-schema.js | 158 ++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 84 deletions(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index c20cb502..65fbce41 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -93,10 +93,7 @@ class JsonSchemaProxyHandler { value = JsonSchema.isolate(value); - const error = JsonSchemaProxyHandler.validate(value, propertySchema); - if (error !== null) { - throw new Error(`Invalid value: ${error}`); - } + JsonSchemaProxyHandler.validate(value, propertySchema); target[property] = value; return true; @@ -193,184 +190,173 @@ class JsonSchemaProxyHandler { } static validate(value, schema) { - let result = JsonSchemaProxyHandler.validateSingleSchema(value, schema); - if (result !== null) { return result; } - - result = JsonSchemaProxyHandler.validateConditional(value, schema); - if (result !== null) { return result; } - - result = JsonSchemaProxyHandler.validateAllOf(value, schema); - if (result !== null) { return result; } - - result = JsonSchemaProxyHandler.validateAnyOf(value, schema); - if (result !== null) { return result; } - - result = JsonSchemaProxyHandler.validateOneOf(value, schema); - if (result !== null) { return result; } - - result = JsonSchemaProxyHandler.validateNoneOf(value, schema); - if (result !== null) { return result; } - - return null; + JsonSchemaProxyHandler.validateSingleSchema(value, schema); + JsonSchemaProxyHandler.validateConditional(value, schema); + JsonSchemaProxyHandler.validateAllOf(value, schema); + JsonSchemaProxyHandler.validateAnyOf(value, schema); + JsonSchemaProxyHandler.validateOneOf(value, schema); + JsonSchemaProxyHandler.validateNoneOf(value, schema); } static validateConditional(value, schema) { const ifCondition = schema.if; - if (!JsonSchemaProxyHandler.isObject(ifCondition)) { return null; } + if (!JsonSchemaProxyHandler.isObject(ifCondition)) { return; } const thenSchema = schema.then; if (JsonSchemaProxyHandler.isObject(thenSchema)) { - const result = JsonSchemaProxyHandler.validate(value, thenSchema); - if (result !== null) { return `then conditional didn't match: ${result}`; } + JsonSchemaProxyHandler.validate(value, thenSchema); } const elseSchema = schema.else; if (JsonSchemaProxyHandler.isObject(elseSchema)) { - const result = JsonSchemaProxyHandler.validate(value, thenSchema); - if (result !== null) { return `else conditional didn't match: ${result}`; } + JsonSchemaProxyHandler.validate(value, thenSchema); } - - return null; } static validateAllOf(value, schema) { const subSchemas = schema.allOf; - if (!Array.isArray(subSchemas)) { return null; } + if (!Array.isArray(subSchemas)) { return; } for (let i = 0; i < subSchemas.length; ++i) { - const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]); - if (result !== null) { return `allOf[${i}] schema didn't match: ${result}`; } + JsonSchemaProxyHandler.validate(value, subSchemas[i]); } - return null; } static validateAnyOf(value, schema) { const subSchemas = schema.anyOf; - if (!Array.isArray(subSchemas)) { return null; } + if (!Array.isArray(subSchemas)) { return; } for (let i = 0; i < subSchemas.length; ++i) { - const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]); - if (result === null) { return null; } + try { + JsonSchemaProxyHandler.validate(value, subSchemas[i]); + return; + } catch (e) { + // NOP + } } - return '0 anyOf schemas matched'; + + throw new JsonSchemaValidationError('0 anyOf schemas matched', value, schema); } static validateOneOf(value, schema) { const subSchemas = schema.oneOf; - if (!Array.isArray(subSchemas)) { return null; } + if (!Array.isArray(subSchemas)) { return; } let count = 0; for (let i = 0; i < subSchemas.length; ++i) { - const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]); - if (result === null) { ++count; } + try { + JsonSchemaProxyHandler.validate(value, subSchemas[i]); + ++count; + } catch (e) { + // NOP + } + } + + if (count !== 1) { + throw new JsonSchemaValidationError(`${count} oneOf schemas matched`, value, schema); } - return count === 1 ? null : `${count} oneOf schemas matched`; } static validateNoneOf(value, schema) { const subSchemas = schema.not; - if (!Array.isArray(subSchemas)) { return null; } + if (!Array.isArray(subSchemas)) { return; } for (let i = 0; i < subSchemas.length; ++i) { - const result = JsonSchemaProxyHandler.validate(value, subSchemas[i]); - if (result === null) { return `not[${i}] schema matched`; } + try { + JsonSchemaProxyHandler.validate(value, subSchemas[i]); + } catch (e) { + continue; + } + throw new JsonSchemaValidationError(`not[${i}] schema matched`, value, schema); } - return null; } static validateSingleSchema(value, schema) { const type = JsonSchemaProxyHandler.getValueType(value); const schemaType = schema.type; if (!JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType)) { - return `Value type ${type} does not match schema type ${schemaType}`; + throw new JsonSchemaValidationError(`Value type ${type} does not match schema type ${schemaType}`, value, schema); } const schemaEnum = schema.enum; if (Array.isArray(schemaEnum) && !JsonSchemaProxyHandler.valuesAreEqualAny(value, schemaEnum)) { - return 'Invalid enum value'; + throw new JsonSchemaValidationError('Invalid enum value', value, schema); } switch (type) { case 'number': - return JsonSchemaProxyHandler.validateNumber(value, schema); + JsonSchemaProxyHandler.validateNumber(value, schema); + break; case 'string': - return JsonSchemaProxyHandler.validateString(value, schema); + JsonSchemaProxyHandler.validateString(value, schema); + break; case 'array': - return JsonSchemaProxyHandler.validateArray(value, schema); + JsonSchemaProxyHandler.validateArray(value, schema); + break; case 'object': - return JsonSchemaProxyHandler.validateObject(value, schema); - default: - return null; + JsonSchemaProxyHandler.validateObject(value, schema); + break; } } static validateNumber(value, schema) { const multipleOf = schema.multipleOf; if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) { - return `Number is not a multiple of ${multipleOf}`; + throw new JsonSchemaValidationError(`Number is not a multiple of ${multipleOf}`, value, schema); } const minimum = schema.minimum; if (typeof minimum === 'number' && value < minimum) { - return `Number is less than ${minimum}`; + throw new JsonSchemaValidationError(`Number is less than ${minimum}`, value, schema); } const exclusiveMinimum = schema.exclusiveMinimum; if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) { - return `Number is less than or equal to ${exclusiveMinimum}`; + throw new JsonSchemaValidationError(`Number is less than or equal to ${exclusiveMinimum}`, value, schema); } const maximum = schema.maximum; if (typeof maximum === 'number' && value > maximum) { - return `Number is greater than ${maximum}`; + throw new JsonSchemaValidationError(`Number is greater than ${maximum}`, value, schema); } const exclusiveMaximum = schema.exclusiveMaximum; if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) { - return `Number is greater than or equal to ${exclusiveMaximum}`; + throw new JsonSchemaValidationError(`Number is greater than or equal to ${exclusiveMaximum}`, value, schema); } - - return null; } static validateString(value, schema) { const minLength = schema.minLength; if (typeof minLength === 'number' && value.length < minLength) { - return 'String length too short'; + throw new JsonSchemaValidationError('String length too short', value, schema); } const maxLength = schema.maxLength; if (typeof maxLength === 'number' && value.length > maxLength) { - return 'String length too long'; + throw new JsonSchemaValidationError('String length too long', value, schema); } - - return null; } static validateArray(value, schema) { const minItems = schema.minItems; if (typeof minItems === 'number' && value.length < minItems) { - return 'Array length too short'; + throw new JsonSchemaValidationError('Array length too short', value, schema); } const maxItems = schema.maxItems; if (typeof maxItems === 'number' && value.length > maxItems) { - return 'Array length too long'; + throw new JsonSchemaValidationError('Array length too long', value, schema); } for (let i = 0, ii = value.length; i < ii; ++i) { const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i, value); if (propertySchema === null) { - return `No schema found for array[${i}]`; + throw new JsonSchemaValidationError(`No schema found for array[${i}]`, value, schema); } - const error = JsonSchemaProxyHandler.validate(value[i], propertySchema); - if (error !== null) { - return error; - } + JsonSchemaProxyHandler.validate(value[i], propertySchema); } - - return null; } static validateObject(value, schema) { @@ -380,33 +366,28 @@ class JsonSchemaProxyHandler { if (Array.isArray(required)) { for (const property of required) { if (!properties.has(property)) { - return `Missing property ${property}`; + throw new JsonSchemaValidationError(`Missing property ${property}`, value, schema); } } } const minProperties = schema.minProperties; if (typeof minProperties === 'number' && properties.length < minProperties) { - return 'Not enough object properties'; + throw new JsonSchemaValidationError('Not enough object properties', value, schema); } const maxProperties = schema.maxProperties; if (typeof maxProperties === 'number' && properties.length > maxProperties) { - return 'Too many object properties'; + throw new JsonSchemaValidationError('Too many object properties', value, schema); } for (const property of properties) { const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value); if (propertySchema === null) { - return `No schema found for ${property}`; - } - const error = JsonSchemaProxyHandler.validate(value[property], propertySchema); - if (error !== null) { - return error; + throw new JsonSchemaValidationError(`No schema found for ${property}`, value, schema); } + JsonSchemaProxyHandler.validate(value[property], propertySchema); } - - return null; } static isValueTypeAny(value, type, schemaTypes) { @@ -547,6 +528,15 @@ class JsonSchemaProxyHandler { JsonSchemaProxyHandler._unconstrainedSchema = {}; +class JsonSchemaValidationError extends Error { + constructor(message, value, schema, path) { + super(message); + this.value = value; + this.schema = schema; + this.path = path; + } +} + class JsonSchema { static createProxy(target, schema) { return new Proxy(target, new JsonSchemaProxyHandler(schema)); From 7c9fe2c6cf52e61620ff36853fa0dee1b93594f5 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 2 Feb 2020 10:17:16 -0500 Subject: [PATCH 11/17] Fix conditional logic --- ext/bg/js/json-schema.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 65fbce41..97429211 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -199,17 +199,19 @@ class JsonSchemaProxyHandler { } static validateConditional(value, schema) { - const ifCondition = schema.if; - if (!JsonSchemaProxyHandler.isObject(ifCondition)) { return; } + const ifSchema = schema.if; + if (!JsonSchemaProxyHandler.isObject(ifSchema)) { return; } - const thenSchema = schema.then; - if (JsonSchemaProxyHandler.isObject(thenSchema)) { + let okay = true; + try { JsonSchemaProxyHandler.validate(value, thenSchema); + } catch (e) { + okay = false; } - const elseSchema = schema.else; - if (JsonSchemaProxyHandler.isObject(elseSchema)) { - JsonSchemaProxyHandler.validate(value, thenSchema); + const nextSchema = okay ? schema.then : schema.else; + if (JsonSchemaProxyHandler.isObject(nextSchema)) { + JsonSchemaProxyHandler.validate(value, nextSchema); } } From fca5c7515160bcae84fd3dc3284ec54babe75c72 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 2 Feb 2020 10:35:41 -0500 Subject: [PATCH 12/17] Fix ifSchema --- ext/bg/js/json-schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 97429211..43ba0c1d 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -204,7 +204,7 @@ class JsonSchemaProxyHandler { let okay = true; try { - JsonSchemaProxyHandler.validate(value, thenSchema); + JsonSchemaProxyHandler.validate(value, ifSchema, info); } catch (e) { okay = false; } From 3bef380e3be0a641ec5095ef73ddb5c8d48a342c Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 2 Feb 2020 10:47:30 -0500 Subject: [PATCH 13/17] Add improved error information when validation fails --- ext/bg/js/json-schema.js | 165 +++++++++++++++++++++++++++------------ 1 file changed, 114 insertions(+), 51 deletions(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 43ba0c1d..49f1e082 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -93,7 +93,7 @@ class JsonSchemaProxyHandler { value = JsonSchema.isolate(value); - JsonSchemaProxyHandler.validate(value, propertySchema); + JsonSchemaProxyHandler.validate(value, propertySchema, new JsonSchemaTraversalInfo(value, propertySchema)); target[property] = value; return true; @@ -189,206 +189,244 @@ class JsonSchemaProxyHandler { return type; } - static validate(value, schema) { - JsonSchemaProxyHandler.validateSingleSchema(value, schema); - JsonSchemaProxyHandler.validateConditional(value, schema); - JsonSchemaProxyHandler.validateAllOf(value, schema); - JsonSchemaProxyHandler.validateAnyOf(value, schema); - JsonSchemaProxyHandler.validateOneOf(value, schema); - JsonSchemaProxyHandler.validateNoneOf(value, schema); + static validate(value, schema, info) { + JsonSchemaProxyHandler.validateSingleSchema(value, schema, info); + JsonSchemaProxyHandler.validateConditional(value, schema, info); + JsonSchemaProxyHandler.validateAllOf(value, schema, info); + JsonSchemaProxyHandler.validateAnyOf(value, schema, info); + JsonSchemaProxyHandler.validateOneOf(value, schema, info); + JsonSchemaProxyHandler.validateNoneOf(value, schema, info); } - static validateConditional(value, schema) { + static validateConditional(value, schema, info) { const ifSchema = schema.if; if (!JsonSchemaProxyHandler.isObject(ifSchema)) { return; } let okay = true; + info.schemaPush('if', ifSchema); try { JsonSchemaProxyHandler.validate(value, ifSchema, info); } catch (e) { okay = false; } + info.schemaPop(); const nextSchema = okay ? schema.then : schema.else; if (JsonSchemaProxyHandler.isObject(nextSchema)) { - JsonSchemaProxyHandler.validate(value, nextSchema); + info.schemaPush(okay ? 'then' : 'else', nextSchema); + JsonSchemaProxyHandler.validate(value, nextSchema, info); + info.schemaPop(); } } - static validateAllOf(value, schema) { + static validateAllOf(value, schema, info) { const subSchemas = schema.allOf; if (!Array.isArray(subSchemas)) { return; } + info.schemaPush('allOf', subSchemas); for (let i = 0; i < subSchemas.length; ++i) { - JsonSchemaProxyHandler.validate(value, subSchemas[i]); + const subSchema = subSchemas[i]; + info.schemaPush(i, subSchema); + JsonSchemaProxyHandler.validate(value, subSchema, info); + info.schemaPop(); } + info.schemaPop(); } - static validateAnyOf(value, schema) { + static validateAnyOf(value, schema, info) { const subSchemas = schema.anyOf; if (!Array.isArray(subSchemas)) { return; } + info.schemaPush('anyOf', subSchemas); for (let i = 0; i < subSchemas.length; ++i) { + const subSchema = subSchemas[i]; + info.schemaPush(i, subSchema); try { - JsonSchemaProxyHandler.validate(value, subSchemas[i]); + JsonSchemaProxyHandler.validate(value, subSchema, info); return; } catch (e) { // NOP } + info.schemaPop(); } - throw new JsonSchemaValidationError('0 anyOf schemas matched', value, schema); + throw new JsonSchemaValidationError('0 anyOf schemas matched', value, schema, info); + // info.schemaPop(); // Unreachable } - static validateOneOf(value, schema) { + static validateOneOf(value, schema, info) { const subSchemas = schema.oneOf; if (!Array.isArray(subSchemas)) { return; } + info.schemaPush('oneOf', subSchemas); let count = 0; for (let i = 0; i < subSchemas.length; ++i) { + const subSchema = subSchemas[i]; + info.schemaPush(i, subSchema); try { - JsonSchemaProxyHandler.validate(value, subSchemas[i]); + JsonSchemaProxyHandler.validate(value, subSchema, info); ++count; } catch (e) { // NOP } + info.schemaPop(); } if (count !== 1) { - throw new JsonSchemaValidationError(`${count} oneOf schemas matched`, value, schema); + throw new JsonSchemaValidationError(`${count} oneOf schemas matched`, value, schema, info); } + + info.schemaPop(); } - static validateNoneOf(value, schema) { + static validateNoneOf(value, schema, info) { const subSchemas = schema.not; if (!Array.isArray(subSchemas)) { return; } + info.schemaPush('not', subSchemas); for (let i = 0; i < subSchemas.length; ++i) { + const subSchema = subSchemas[i]; + info.schemaPush(i, subSchema); try { - JsonSchemaProxyHandler.validate(value, subSchemas[i]); + JsonSchemaProxyHandler.validate(value, subSchema, info); } catch (e) { + info.schemaPop(); continue; } - throw new JsonSchemaValidationError(`not[${i}] schema matched`, value, schema); + throw new JsonSchemaValidationError(`not[${i}] schema matched`, value, schema, info); } + info.schemaPop(); } - static validateSingleSchema(value, schema) { + static validateSingleSchema(value, schema, info) { const type = JsonSchemaProxyHandler.getValueType(value); const schemaType = schema.type; if (!JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType)) { - throw new JsonSchemaValidationError(`Value type ${type} does not match schema type ${schemaType}`, value, schema); + throw new JsonSchemaValidationError(`Value type ${type} does not match schema type ${schemaType}`, value, schema, info); } const schemaEnum = schema.enum; if (Array.isArray(schemaEnum) && !JsonSchemaProxyHandler.valuesAreEqualAny(value, schemaEnum)) { - throw new JsonSchemaValidationError('Invalid enum value', value, schema); + throw new JsonSchemaValidationError('Invalid enum value', value, schema, info); } switch (type) { case 'number': - JsonSchemaProxyHandler.validateNumber(value, schema); + JsonSchemaProxyHandler.validateNumber(value, schema, info); break; case 'string': - JsonSchemaProxyHandler.validateString(value, schema); + JsonSchemaProxyHandler.validateString(value, schema, info); break; case 'array': - JsonSchemaProxyHandler.validateArray(value, schema); + JsonSchemaProxyHandler.validateArray(value, schema, info); break; case 'object': - JsonSchemaProxyHandler.validateObject(value, schema); + JsonSchemaProxyHandler.validateObject(value, schema, info); break; } } - static validateNumber(value, schema) { + static validateNumber(value, schema, info) { const multipleOf = schema.multipleOf; if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) { - throw new JsonSchemaValidationError(`Number is not a multiple of ${multipleOf}`, value, schema); + throw new JsonSchemaValidationError(`Number is not a multiple of ${multipleOf}`, value, schema, info); } const minimum = schema.minimum; if (typeof minimum === 'number' && value < minimum) { - throw new JsonSchemaValidationError(`Number is less than ${minimum}`, value, schema); + throw new JsonSchemaValidationError(`Number is less than ${minimum}`, value, schema, info); } const exclusiveMinimum = schema.exclusiveMinimum; if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) { - throw new JsonSchemaValidationError(`Number is less than or equal to ${exclusiveMinimum}`, value, schema); + throw new JsonSchemaValidationError(`Number is less than or equal to ${exclusiveMinimum}`, value, schema, info); } const maximum = schema.maximum; if (typeof maximum === 'number' && value > maximum) { - throw new JsonSchemaValidationError(`Number is greater than ${maximum}`, value, schema); + throw new JsonSchemaValidationError(`Number is greater than ${maximum}`, value, schema, info); } const exclusiveMaximum = schema.exclusiveMaximum; if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) { - throw new JsonSchemaValidationError(`Number is greater than or equal to ${exclusiveMaximum}`, value, schema); + throw new JsonSchemaValidationError(`Number is greater than or equal to ${exclusiveMaximum}`, value, schema, info); } } - static validateString(value, schema) { + static validateString(value, schema, info) { const minLength = schema.minLength; if (typeof minLength === 'number' && value.length < minLength) { - throw new JsonSchemaValidationError('String length too short', value, schema); + throw new JsonSchemaValidationError('String length too short', value, schema, info); } const maxLength = schema.maxLength; if (typeof maxLength === 'number' && value.length > maxLength) { - throw new JsonSchemaValidationError('String length too long', value, schema); + throw new JsonSchemaValidationError('String length too long', value, schema, info); } } - static validateArray(value, schema) { + static validateArray(value, schema, info) { const minItems = schema.minItems; if (typeof minItems === 'number' && value.length < minItems) { - throw new JsonSchemaValidationError('Array length too short', value, schema); + throw new JsonSchemaValidationError('Array length too short', value, schema, info); } const maxItems = schema.maxItems; if (typeof maxItems === 'number' && value.length > maxItems) { - throw new JsonSchemaValidationError('Array length too long', value, schema); + throw new JsonSchemaValidationError('Array length too long', value, schema, info); } for (let i = 0, ii = value.length; i < ii; ++i) { const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i, value); if (propertySchema === null) { - throw new JsonSchemaValidationError(`No schema found for array[${i}]`, value, schema); + throw new JsonSchemaValidationError(`No schema found for array[${i}]`, value, schema, info); } - JsonSchemaProxyHandler.validate(value[i], propertySchema); + const propertyValue = value[i]; + + info.valuePush(i, propertyValue); + info.schemaPush(i, propertySchema); + JsonSchemaProxyHandler.validate(propertyValue, propertySchema, info); + info.schemaPop(); + info.valuePop(); } } - static validateObject(value, schema) { + static validateObject(value, schema, info) { const properties = new Set(Object.getOwnPropertyNames(value)); const required = schema.required; if (Array.isArray(required)) { for (const property of required) { if (!properties.has(property)) { - throw new JsonSchemaValidationError(`Missing property ${property}`, value, schema); + throw new JsonSchemaValidationError(`Missing property ${property}`, value, schema, info); } } } const minProperties = schema.minProperties; if (typeof minProperties === 'number' && properties.length < minProperties) { - throw new JsonSchemaValidationError('Not enough object properties', value, schema); + throw new JsonSchemaValidationError('Not enough object properties', value, schema, info); } const maxProperties = schema.maxProperties; if (typeof maxProperties === 'number' && properties.length > maxProperties) { - throw new JsonSchemaValidationError('Too many object properties', value, schema); + throw new JsonSchemaValidationError('Too many object properties', value, schema, info); } for (const property of properties) { const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value); if (propertySchema === null) { - throw new JsonSchemaValidationError(`No schema found for ${property}`, value, schema); + throw new JsonSchemaValidationError(`No schema found for ${property}`, value, schema, info); } - JsonSchemaProxyHandler.validate(value[property], propertySchema); + + const propertyValue = value[property]; + + info.valuePush(property, propertyValue); + info.schemaPush(property, propertySchema); + JsonSchemaProxyHandler.validate(propertyValue, propertySchema, info); + info.schemaPop(); + info.valuePop(); } } @@ -530,12 +568,37 @@ class JsonSchemaProxyHandler { JsonSchemaProxyHandler._unconstrainedSchema = {}; +class JsonSchemaTraversalInfo { + constructor(value, schema) { + this.valuePath = []; + this.schemaPath = []; + this.valuePush([null, value]); + this.schemaPush([null, schema]); + } + + valuePush(path, value) { + this.valuePath.push([path, value]); + } + + valuePop() { + this.valuePath.pop(); + } + + schemaPush(path, schema) { + this.schemaPath.push([path, schema]); + } + + schemaPop() { + this.schemaPath.pop(); + } +} + class JsonSchemaValidationError extends Error { - constructor(message, value, schema, path) { + constructor(message, value, schema, info) { super(message); this.value = value; this.schema = schema; - this.path = path; + this.info = info; } } @@ -545,7 +608,7 @@ class JsonSchema { } static validate(value, schema) { - return JsonSchemaProxyHandler.validate(value, schema); + return JsonSchemaProxyHandler.validate(value, schema, new JsonSchemaTraversalInfo(value, schema)); } static getValidValueOrDefault(schema, value) { From ea808024d70e95b94dca7f846e7910573fed8466 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 2 Feb 2020 11:04:38 -0500 Subject: [PATCH 14/17] Fix missing else --- ext/bg/js/json-schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 49f1e082..0ca1183e 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -134,7 +134,7 @@ class JsonSchemaProxyHandler { const additionalProperties = schema.additionalProperties; if (additionalProperties === false) { return null; - } if (JsonSchemaProxyHandler.isObject(additionalProperties)) { + } else if (JsonSchemaProxyHandler.isObject(additionalProperties)) { return additionalProperties; } else { return JsonSchemaProxyHandler._unconstrainedSchema; From b1fc9c024ae84ed603d7f7bec54fadc73a39f1b8 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 2 Feb 2020 11:13:26 -0500 Subject: [PATCH 15/17] Update how property schemas are returned --- ext/bg/js/json-schema.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 0ca1183e..8db5411c 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -126,8 +126,9 @@ class JsonSchemaProxyHandler { { const properties = schema.properties; if (JsonSchemaProxyHandler.isObject(properties)) { - if (Object.prototype.hasOwnProperty.call(properties, property)) { - return properties[property]; + const propertySchema = properties[property]; + if (JsonSchemaProxyHandler.isObject(propertySchema)) { + return propertySchema; } } @@ -148,7 +149,10 @@ class JsonSchemaProxyHandler { } if (Array.isArray(items)) { if (property >= 0 && property < items.length) { - return items[property]; + const propertySchema = items[property]; + if (JsonSchemaProxyHandler.isObject(propertySchema)) { + return propertySchema; + } } } From fff1e67a5e52cb104c77069903f975e114d7a835 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 2 Feb 2020 11:18:13 -0500 Subject: [PATCH 16/17] Improve schema path when using getPropertySchema --- ext/bg/js/json-schema.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 8db5411c..ad6372df 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -119,7 +119,7 @@ class JsonSchemaProxyHandler { throw new Error('construct not supported'); } - static getPropertySchema(schema, property, value) { + static getPropertySchema(schema, property, value, path=null) { const type = JsonSchemaProxyHandler.getSchemaOrValueType(schema, value); switch (type) { case 'object': @@ -128,6 +128,7 @@ class JsonSchemaProxyHandler { if (JsonSchemaProxyHandler.isObject(properties)) { const propertySchema = properties[property]; if (JsonSchemaProxyHandler.isObject(propertySchema)) { + if (path !== null) { path.push(['properties', properties], [property, propertySchema]); } return propertySchema; } } @@ -136,9 +137,12 @@ class JsonSchemaProxyHandler { if (additionalProperties === false) { return null; } else if (JsonSchemaProxyHandler.isObject(additionalProperties)) { + if (path !== null) { path.push(['additionalProperties', additionalProperties]); } return additionalProperties; } else { - return JsonSchemaProxyHandler._unconstrainedSchema; + const result = JsonSchemaProxyHandler._unconstrainedSchema; + if (path !== null) { path.push([null, result]); } + return result; } } case 'array': @@ -151,6 +155,7 @@ class JsonSchemaProxyHandler { if (property >= 0 && property < items.length) { const propertySchema = items[property]; if (JsonSchemaProxyHandler.isObject(propertySchema)) { + if (path !== null) { path.push(['items', items], [property, propertySchema]); } return propertySchema; } } @@ -160,9 +165,12 @@ class JsonSchemaProxyHandler { if (additionalItems === false) { return null; } else if (JsonSchemaProxyHandler.isObject(additionalItems)) { + if (path !== null) { path.push(['additionalItems', additionalItems]); } return additionalItems; } else { - return JsonSchemaProxyHandler._unconstrainedSchema; + const result = JsonSchemaProxyHandler._unconstrainedSchema; + if (path !== null) { path.push([null, result]); } + return result; } } default: @@ -381,18 +389,19 @@ class JsonSchemaProxyHandler { } for (let i = 0, ii = value.length; i < ii; ++i) { - const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i, value); + const schemaPath = []; + const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i, value, schemaPath); if (propertySchema === null) { throw new JsonSchemaValidationError(`No schema found for array[${i}]`, value, schema, info); } const propertyValue = value[i]; + for (const [p, s] of schemaPath) { info.schemaPush(p, s); } info.valuePush(i, propertyValue); - info.schemaPush(i, propertySchema); JsonSchemaProxyHandler.validate(propertyValue, propertySchema, info); - info.schemaPop(); info.valuePop(); + for (let i = 0; i < schemaPath.length; ++i) { info.schemaPop(); } } } @@ -419,18 +428,19 @@ class JsonSchemaProxyHandler { } for (const property of properties) { - const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value); + const schemaPath = []; + const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property, value, schemaPath); if (propertySchema === null) { throw new JsonSchemaValidationError(`No schema found for ${property}`, value, schema, info); } const propertyValue = value[property]; + for (const [p, s] of schemaPath) { info.schemaPush(p, s); } info.valuePush(property, propertyValue); - info.schemaPush(property, propertySchema); JsonSchemaProxyHandler.validate(propertyValue, propertySchema, info); - info.schemaPop(); info.valuePop(); + for (let i = 0; i < schemaPath.length; ++i) { info.schemaPop(); } } } From 3c28c7dd7cdbf4af91b0b4044f03e0877569e3b8 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 2 Feb 2020 11:22:22 -0500 Subject: [PATCH 17/17] Fix init --- ext/bg/js/json-schema.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index ad6372df..3cf24c35 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -586,8 +586,8 @@ class JsonSchemaTraversalInfo { constructor(value, schema) { this.valuePath = []; this.schemaPath = []; - this.valuePush([null, value]); - this.schemaPush([null, schema]); + this.valuePush(null, value); + this.schemaPush(null, schema); } valuePush(path, value) {