From 3bef380e3be0a641ec5095ef73ddb5c8d48a342c Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 2 Feb 2020 10:47:30 -0500 Subject: [PATCH] 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) {