summaryrefslogtreecommitdiff
path: root/ext/js/data/json-schema.js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-05-22 15:45:20 -0400
committerGitHub <noreply@github.com>2021-05-22 15:45:20 -0400
commitd16739a83a20e1729e08dbcbbc155be15972d146 (patch)
treef7c12fa946a688750365ecf0f66227fcad4f927d /ext/js/data/json-schema.js
parentb48052ff320f1a68aac317158c4c757a70b14f04 (diff)
Json schema validation improvements (#1697)
* Create new JsonSchema class * Add proxy handler * Update tests * Update validation scripts * Update backend * Update audio downloader * Update options util * Update dictionary importer * Update json schema file reference * Remove old json-schema.js * Rename new json-schema.js * Update file names * Rename class
Diffstat (limited to 'ext/js/data/json-schema.js')
-rw-r--r--ext/js/data/json-schema.js967
1 files changed, 525 insertions, 442 deletions
diff --git a/ext/js/data/json-schema.js b/ext/js/data/json-schema.js
index 6a590911..b5fac452 100644
--- a/ext/js/data/json-schema.js
+++ b/ext/js/data/json-schema.js
@@ -19,218 +19,202 @@
* CacheMap
*/
-class JsonSchemaProxyHandler {
- constructor(schema, jsonSchemaValidator) {
- this._schema = schema;
- this._jsonSchemaValidator = jsonSchemaValidator;
- }
+class JsonSchema {
+ constructor(schema, rootSchema) {
+ this._schema = null;
+ this._startSchema = schema;
+ this._rootSchema = typeof rootSchema !== 'undefined' ? rootSchema : schema;
+ this._regexCache = null;
+ this._valuePath = [];
+ this._schemaPath = [];
- getPrototypeOf(target) {
- return Object.getPrototypeOf(target);
+ this._schemaPush(null, null);
+ this._valuePush(null, null);
}
- setPrototypeOf() {
- throw new Error('setPrototypeOf not supported');
+ get schema() {
+ return this._startSchema;
}
- isExtensible(target) {
- return Object.isExtensible(target);
+ get rootSchema() {
+ return this._rootSchema;
}
- preventExtensions(target) {
- Object.preventExtensions(target);
- return true;
+ createProxy(value) {
+ return (
+ typeof value === 'object' && value !== null ?
+ new Proxy(value, new JsonSchemaProxyHandler(this)) :
+ value
+ );
}
- getOwnPropertyDescriptor(target, property) {
- return Object.getOwnPropertyDescriptor(target, property);
+ isValid(value) {
+ try {
+ this.validate(value);
+ return true;
+ } catch (e) {
+ return false;
+ }
}
- defineProperty() {
- throw new Error('defineProperty not supported');
+ validate(value) {
+ this._schemaPush(null, this._startSchema);
+ this._valuePush(null, value);
+ try {
+ this._validate(value);
+ } finally {
+ this._valuePop();
+ this._schemaPop();
+ }
}
- has(target, property) {
- return property in target;
+ getValidValueOrDefault(value) {
+ return this._getValidValueOrDefault(null, value, [{path: null, schema: this._startSchema}]);
}
- get(target, property) {
- if (typeof property === 'symbol') {
- return target[property];
- }
-
- if (Array.isArray(target)) {
- if (typeof property === 'string' && /^\d+$/.test(property)) {
- property = parseInt(property, 10);
- } else if (typeof property === 'string') {
- return target[property];
- }
- }
-
- const propertySchema = this._jsonSchemaValidator.getPropertySchema(this._schema, property, target);
- if (propertySchema === null) {
- return;
+ getObjectPropertySchema(property) {
+ this._schemaPush(null, this._startSchema);
+ try {
+ const schemaPath = this._getObjectPropertySchemaPath(property);
+ return schemaPath !== null ? new JsonSchema(schemaPath[schemaPath.length - 1].schema, this._rootSchema) : null;
+ } finally {
+ this._schemaPop();
}
-
- const value = target[property];
- return value !== null && typeof value === 'object' ? this._jsonSchemaValidator.createProxy(value, propertySchema) : value;
}
- set(target, property, value) {
- if (Array.isArray(target)) {
- if (typeof property === 'string' && /^\d+$/.test(property)) {
- property = parseInt(property, 10);
- if (property > target.length) {
- throw new Error('Array index out of range');
- }
- } else if (typeof property === 'string') {
- target[property] = value;
- return true;
- }
- }
-
- const propertySchema = this._jsonSchemaValidator.getPropertySchema(this._schema, property, target);
- if (propertySchema === null) {
- throw new Error(`Property ${property} not supported`);
+ getArrayItemSchema(index) {
+ this._schemaPush(null, this._startSchema);
+ try {
+ const schemaPath = this._getArrayItemSchemaPath(index);
+ return schemaPath !== null ? new JsonSchema(schemaPath[schemaPath.length - 1].schema, this._rootSchema) : null;
+ } finally {
+ this._schemaPop();
}
-
- value = clone(value);
-
- this._jsonSchemaValidator.validate(value, propertySchema);
-
- target[property] = value;
- return true;
}
- deleteProperty(target, property) {
- const required = (
- (typeof target === 'object' && target !== null) ?
- (Array.isArray(target) || this._jsonSchemaValidator.isObjectPropertyRequired(this._schema, property)) :
- true
- );
- if (required) {
- throw new Error(`${property} cannot be deleted`);
- }
- return Reflect.deleteProperty(target, property);
+ isObjectPropertyRequired(property) {
+ const {required} = this._startSchema;
+ return Array.isArray(required) && required.includes(property);
}
- ownKeys(target) {
- return Reflect.ownKeys(target);
- }
+ // Stack
- apply() {
- throw new Error('apply not supported');
+ _valuePush(path, value) {
+ this._valuePath.push({path, value});
}
- construct() {
- throw new Error('construct not supported');
+ _valuePop() {
+ this._valuePath.pop();
}
-}
-class JsonSchemaValidator {
- constructor() {
- this._regexCache = new CacheMap(100);
+ _schemaPush(path, schema) {
+ this._schemaPath.push({path, schema});
+ this._schema = schema;
}
- createProxy(target, schema) {
- return new Proxy(target, new JsonSchemaProxyHandler(schema, this));
+ _schemaPop() {
+ this._schemaPath.pop();
+ this._schema = this._schemaPath[this._schemaPath.length - 1].schema;
}
- isValid(value, schema) {
- try {
- this.validate(value, schema);
- return true;
- } catch (e) {
- return false;
+ // Private
+
+ _createError(message) {
+ const valuePath = [];
+ for (let i = 1, ii = this._valuePath.length; i < ii; ++i) {
+ const {path, value} = this._valuePath[i];
+ valuePath.push({path, value});
}
- }
- validate(value, schema) {
- const info = new JsonSchemaTraversalInfo(value, schema);
- this._validate(value, schema, info);
- }
+ const schemaPath = [];
+ for (let i = 1, ii = this._schemaPath.length; i < ii; ++i) {
+ const {path, schema} = this._schemaPath[i];
+ schemaPath.push({path, schema});
+ }
- getValidValueOrDefault(schema, value) {
- const info = new JsonSchemaTraversalInfo(value, schema);
- return this._getValidValueOrDefault(schema, value, info);
+ const error = new Error(message);
+ error.value = valuePath[valuePath.length - 1].value;
+ error.schema = this._schema;
+ error.valuePath = valuePath;
+ error.schemaPath = schemaPath;
+ return error;
}
- getPropertySchema(schema, property, value) {
- return this._getPropertySchema(schema, property, value, null);
+ _isObject(value) {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
}
- clearCache() {
- this._regexCache.clear();
- }
+ _getRegex(pattern, flags) {
+ if (this._regexCache === null) {
+ this._regexCache = new CacheMap(100);
+ }
- isObjectPropertyRequired(schema, property) {
- const {required} = schema;
- return Array.isArray(required) && required.includes(property);
+ const key = `${flags}:${pattern}`;
+ let regex = this._regexCache.get(key);
+ if (typeof regex === 'undefined') {
+ regex = new RegExp(pattern, flags);
+ this._regexCache.set(key, regex);
+ }
+ return regex;
}
- // Private
-
- _getPropertySchema(schema, property, value, path) {
- const type = this._getSchemaOrValueType(schema, value);
- switch (type) {
- case 'object':
- {
- const properties = schema.properties;
- if (this._isObject(properties)) {
- const propertySchema = properties[property];
- if (this._isObject(propertySchema)) {
- if (path !== null) { path.push(['properties', properties], [property, propertySchema]); }
- return propertySchema;
- }
- }
+ _getUnconstrainedSchema() {
+ return {};
+ }
- const additionalProperties = schema.additionalProperties;
- if (additionalProperties === false) {
- return null;
- } else if (this._isObject(additionalProperties)) {
- if (path !== null) { path.push(['additionalProperties', additionalProperties]); }
- return additionalProperties;
- } else {
- const result = JsonSchemaValidator.unconstrainedSchema;
- if (path !== null) { path.push([null, result]); }
- return result;
- }
+ _getObjectPropertySchemaPath(property) {
+ const {properties} = this._schema;
+ if (this._isObject(properties)) {
+ const propertySchema = properties[property];
+ if (this._isObject(propertySchema)) {
+ return [
+ {path: 'properties', schema: properties},
+ {path: property, schema: propertySchema}
+ ];
}
- case 'array':
- {
- const items = schema.items;
- if (this._isObject(items)) {
- return items;
- }
- if (Array.isArray(items)) {
- if (property >= 0 && property < items.length) {
- const propertySchema = items[property];
- if (this._isObject(propertySchema)) {
- if (path !== null) { path.push(['items', items], [property, propertySchema]); }
- return propertySchema;
- }
- }
- }
+ }
- const additionalItems = schema.additionalItems;
- if (additionalItems === false) {
- return null;
- } else if (this._isObject(additionalItems)) {
- if (path !== null) { path.push(['additionalItems', additionalItems]); }
- return additionalItems;
- } else {
- const result = JsonSchemaValidator.unconstrainedSchema;
- if (path !== null) { path.push([null, result]); }
- return result;
+ const {additionalProperties} = this._schema;
+ if (additionalProperties === false) {
+ return null;
+ } else if (this._isObject(additionalProperties)) {
+ return [{path: 'additionalProperties', schema: additionalProperties}];
+ } else {
+ const result = this._getUnconstrainedSchema();
+ return [{path: null, schema: result}];
+ }
+ }
+
+ _getArrayItemSchemaPath(index) {
+ const {items} = this._schema;
+ if (this._isObject(items)) {
+ return [{path: 'items', schema: items}];
+ }
+ if (Array.isArray(items)) {
+ if (index >= 0 && index < items.length) {
+ const propertySchema = items[index];
+ if (this._isObject(propertySchema)) {
+ return [
+ {path: 'items', schema: items},
+ {path: index, schema: propertySchema}
+ ];
}
}
- default:
- return null;
+ }
+
+ const {additionalItems} = this._schema;
+ if (additionalItems === false) {
+ return null;
+ } else if (this._isObject(additionalItems)) {
+ return [{path: 'additionalItems', schema: additionalItems}];
+ } else {
+ const result = this._getUnconstrainedSchema();
+ return [{path: null, schema: result}];
}
}
- _getSchemaOrValueType(schema, value) {
- const type = schema.type;
+ _getSchemaOrValueType(value) {
+ const {type} = this._schema;
if (Array.isArray(type)) {
if (typeof value !== 'undefined') {
@@ -242,345 +226,391 @@ class JsonSchemaValidator {
return null;
}
- if (typeof type === 'undefined') {
- if (typeof value !== 'undefined') {
- return this._getValueType(value);
+ if (typeof type !== 'undefined') { return type; }
+ return (typeof value !== 'undefined') ? this._getValueType(value) : null;
+ }
+
+ _getValueType(value) {
+ const type = typeof value;
+ if (type === 'object') {
+ if (value === null) { return 'null'; }
+ if (Array.isArray(value)) { return 'array'; }
+ }
+ return type;
+ }
+
+ _isValueTypeAny(value, type, schemaTypes) {
+ if (typeof schemaTypes === 'string') {
+ return this._isValueType(value, type, schemaTypes);
+ } else if (Array.isArray(schemaTypes)) {
+ for (const schemaType of schemaTypes) {
+ if (this._isValueType(value, type, schemaType)) {
+ return true;
+ }
}
- return null;
+ return false;
}
+ return true;
+ }
- return type;
+ _isValueType(value, type, schemaType) {
+ return (
+ type === schemaType ||
+ (schemaType === 'integer' && Math.floor(value) === value)
+ );
}
- _validate(value, schema, info) {
- this._validateSingleSchema(value, schema, info);
- this._validateConditional(value, schema, info);
- this._validateAllOf(value, schema, info);
- this._validateAnyOf(value, schema, info);
- this._validateOneOf(value, schema, info);
- this._validateNoneOf(value, schema, info);
+ _valuesAreEqualAny(value1, valueList) {
+ for (const value2 of valueList) {
+ if (this._valuesAreEqual(value1, value2)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ _valuesAreEqual(value1, value2) {
+ return value1 === value2;
+ }
+
+ // Validation
+
+ _isValidCurrent(value) {
+ try {
+ this._validate(value);
+ return true;
+ } catch (e) {
+ return false;
+ }
}
- _validateConditional(value, schema, info) {
- const ifSchema = schema.if;
+ _validate(value) {
+ this._validateSingleSchema(value);
+ this._validateConditional(value);
+ this._validateAllOf(value);
+ this._validateAnyOf(value);
+ this._validateOneOf(value);
+ this._validateNoneOf(value);
+ }
+
+ _validateConditional(value) {
+ const ifSchema = this._schema.if;
if (!this._isObject(ifSchema)) { return; }
let okay = true;
- info.schemaPush('if', ifSchema);
+ this._schemaPush('if', ifSchema);
try {
- this._validate(value, ifSchema, info);
+ this._validate(value);
} catch (e) {
okay = false;
+ } finally {
+ this._schemaPop();
}
- info.schemaPop();
- const nextSchema = okay ? schema.then : schema.else;
- if (this._isObject(nextSchema)) {
- info.schemaPush(okay ? 'then' : 'else', nextSchema);
- this._validate(value, nextSchema, info);
- info.schemaPop();
+ const nextSchema = okay ? this._schema.then : this._schema.else;
+ if (this._isObject(nextSchema)) { return; }
+
+ this._schemaPush(okay ? 'then' : 'else', nextSchema);
+ try {
+ this._validate(value);
+ } finally {
+ this._schemaPop();
}
}
- _validateAllOf(value, schema, info) {
- const subSchemas = schema.allOf;
+ _validateAllOf(value) {
+ const subSchemas = this._schema.allOf;
if (!Array.isArray(subSchemas)) { return; }
- info.schemaPush('allOf', subSchemas);
- for (let i = 0; i < subSchemas.length; ++i) {
- const subSchema = subSchemas[i];
- info.schemaPush(i, subSchema);
- this._validate(value, subSchema, info);
- info.schemaPop();
+ this._schemaPush('allOf', subSchemas);
+ try {
+ for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
+ const subSchema = subSchemas[i];
+ if (!this._isObject(subSchema)) { continue; }
+
+ this._schemaPush(i, subSchema);
+ try {
+ this._validate(value);
+ } finally {
+ this._schemaPop();
+ }
+ }
+ } finally {
+ this._schemaPop();
}
- info.schemaPop();
}
- _validateAnyOf(value, schema, info) {
- const subSchemas = schema.anyOf;
+ _validateAnyOf(value) {
+ const subSchemas = this._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 {
- this._validate(value, subSchema, info);
- return;
- } catch (e) {
- // NOP
+ this._schemaPush('anyOf', subSchemas);
+ try {
+ for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
+ const subSchema = subSchemas[i];
+ if (!this._isObject(subSchema)) { continue; }
+
+ this._schemaPush(i, subSchema);
+ try {
+ this._validate(value);
+ return;
+ } catch (e) {
+ // NOP
+ } finally {
+ this._schemaPop();
+ }
}
- info.schemaPop();
- }
- throw new JsonSchemaValidationError('0 anyOf schemas matched', value, schema, info);
- // info.schemaPop(); // Unreachable
+ throw this._createError('0 anyOf schemas matched');
+ } finally {
+ this._schemaPop();
+ }
}
- _validateOneOf(value, schema, info) {
- const subSchemas = schema.oneOf;
+ _validateOneOf(value) {
+ const subSchemas = this._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 {
- this._validate(value, subSchema, info);
- ++count;
- } catch (e) {
- // NOP
+ this._schemaPush('oneOf', subSchemas);
+ try {
+ let count = 0;
+ for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
+ const subSchema = subSchemas[i];
+ if (!this._isObject(subSchema)) { continue; }
+
+ this._schemaPush(i, subSchema);
+ try {
+ this._validate(value);
+ ++count;
+ } catch (e) {
+ // NOP
+ } finally {
+ this._schemaPop();
+ }
}
- info.schemaPop();
- }
- if (count !== 1) {
- throw new JsonSchemaValidationError(`${count} oneOf schemas matched`, value, schema, info);
+ if (count !== 1) {
+ throw this._createError(`${count} oneOf schemas matched`);
+ }
+ } finally {
+ this._schemaPop();
}
-
- info.schemaPop();
}
- _validateNoneOf(value, schema, info) {
- const subSchemas = schema.not;
+ _validateNoneOf(value) {
+ const subSchemas = this._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 {
- this._validate(value, subSchema, info);
- } catch (e) {
- info.schemaPop();
- continue;
+ this._schemaPush('not', subSchemas);
+ try {
+ for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
+ const subSchema = subSchemas[i];
+ if (!this._isObject(subSchema)) { continue; }
+
+ this._schemaPush(i, subSchema);
+ try {
+ this._validate(value);
+ } catch (e) {
+ continue;
+ } finally {
+ this._schemaPop();
+ }
+ throw this._createError(`not[${i}] schema matched`);
}
- throw new JsonSchemaValidationError(`not[${i}] schema matched`, value, schema, info);
+ } finally {
+ this._schemaPop();
}
- info.schemaPop();
}
- _validateSingleSchema(value, schema, info) {
+ _validateSingleSchema(value) {
const type = this._getValueType(value);
- const schemaType = schema.type;
+ const schemaType = this._schema.type;
if (!this._isValueTypeAny(value, type, schemaType)) {
- throw new JsonSchemaValidationError(`Value type ${type} does not match schema type ${schemaType}`, value, schema, info);
+ throw this._createError(`Value type ${type} does not match schema type ${schemaType}`);
}
- const schemaConst = schema.const;
+ const schemaConst = this._schema.const;
if (typeof schemaConst !== 'undefined' && !this._valuesAreEqual(value, schemaConst)) {
- throw new JsonSchemaValidationError('Invalid constant value', value, schema, info);
+ throw this._createError('Invalid constant value');
}
- const schemaEnum = schema.enum;
+ const schemaEnum = this._schema.enum;
if (Array.isArray(schemaEnum) && !this._valuesAreEqualAny(value, schemaEnum)) {
- throw new JsonSchemaValidationError('Invalid enum value', value, schema, info);
+ throw this._createError('Invalid enum value');
}
switch (type) {
case 'number':
- this._validateNumber(value, schema, info);
+ this._validateNumber(value);
break;
case 'string':
- this._validateString(value, schema, info);
+ this._validateString(value);
break;
case 'array':
- this._validateArray(value, schema, info);
+ this._validateArray(value);
break;
case 'object':
- this._validateObject(value, schema, info);
+ this._validateObject(value);
break;
}
}
- _validateNumber(value, schema, info) {
- const multipleOf = schema.multipleOf;
+ _validateNumber(value) {
+ const {multipleOf} = this._schema;
if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) {
- throw new JsonSchemaValidationError(`Number is not a multiple of ${multipleOf}`, value, schema, info);
+ throw this._createError(`Number is not a multiple of ${multipleOf}`);
}
- const minimum = schema.minimum;
+ const {minimum} = this._schema;
if (typeof minimum === 'number' && value < minimum) {
- throw new JsonSchemaValidationError(`Number is less than ${minimum}`, value, schema, info);
+ throw this._createError(`Number is less than ${minimum}`);
}
- const exclusiveMinimum = schema.exclusiveMinimum;
+ const {exclusiveMinimum} = this._schema;
if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) {
- throw new JsonSchemaValidationError(`Number is less than or equal to ${exclusiveMinimum}`, value, schema, info);
+ throw this._createError(`Number is less than or equal to ${exclusiveMinimum}`);
}
- const maximum = schema.maximum;
+ const {maximum} = this._schema;
if (typeof maximum === 'number' && value > maximum) {
- throw new JsonSchemaValidationError(`Number is greater than ${maximum}`, value, schema, info);
+ throw this._createError(`Number is greater than ${maximum}`);
}
- const exclusiveMaximum = schema.exclusiveMaximum;
+ const {exclusiveMaximum} = this._schema;
if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) {
- throw new JsonSchemaValidationError(`Number is greater than or equal to ${exclusiveMaximum}`, value, schema, info);
+ throw this._createError(`Number is greater than or equal to ${exclusiveMaximum}`);
}
}
- _validateString(value, schema, info) {
- const minLength = schema.minLength;
+ _validateString(value) {
+ const {minLength} = this._schema;
if (typeof minLength === 'number' && value.length < minLength) {
- throw new JsonSchemaValidationError('String length too short', value, schema, info);
+ throw this._createError('String length too short');
}
- const maxLength = schema.maxLength;
+ const {maxLength} = this._schema;
if (typeof maxLength === 'number' && value.length > maxLength) {
- throw new JsonSchemaValidationError('String length too long', value, schema, info);
+ throw this._createError('String length too long');
}
- const pattern = schema.pattern;
+ const {pattern} = this._schema;
if (typeof pattern === 'string') {
- let patternFlags = schema.patternFlags;
+ let {patternFlags} = this._schema;
if (typeof patternFlags !== 'string') { patternFlags = ''; }
let regex;
try {
regex = this._getRegex(pattern, patternFlags);
} catch (e) {
- throw new JsonSchemaValidationError(`Pattern is invalid (${e.message})`, value, schema, info);
+ throw this._createError(`Pattern is invalid (${e.message})`);
}
if (!regex.test(value)) {
- throw new JsonSchemaValidationError('Pattern match failed', value, schema, info);
+ throw this._createError('Pattern match failed');
}
}
}
- _validateArray(value, schema, info) {
- const minItems = schema.minItems;
- if (typeof minItems === 'number' && value.length < minItems) {
- throw new JsonSchemaValidationError('Array length too short', value, schema, info);
+ _validateArray(value) {
+ const {length} = value;
+
+ const {minItems} = this._schema;
+ if (typeof minItems === 'number' && length < minItems) {
+ throw this._createError('Array length too short');
}
- const maxItems = schema.maxItems;
- if (typeof maxItems === 'number' && value.length > maxItems) {
- throw new JsonSchemaValidationError('Array length too long', value, schema, info);
+ const {maxItems} = this._schema;
+ if (typeof maxItems === 'number' && length > maxItems) {
+ throw this._createError('Array length too long');
}
- this._validateArrayContains(value, schema, info);
+ this._validateArrayContains(value);
- for (let i = 0, ii = value.length; i < ii; ++i) {
- const schemaPath = [];
- const propertySchema = this._getPropertySchema(schema, i, value, schemaPath);
- if (propertySchema === null) {
- throw new JsonSchemaValidationError(`No schema found for array[${i}]`, value, schema, info);
+ for (let i = 0; i < length; ++i) {
+ const schemaPath = this._getArrayItemSchemaPath(i);
+ if (schemaPath === null) {
+ throw this._createError(`No schema found for array[${i}]`);
}
const propertyValue = value[i];
- for (const [p, s] of schemaPath) { info.schemaPush(p, s); }
- info.valuePush(i, propertyValue);
- this._validate(propertyValue, propertySchema, info);
- info.valuePop();
- for (let j = 0, jj = schemaPath.length; j < jj; ++j) { info.schemaPop(); }
+ for (const {path, schema} of schemaPath) { this._schemaPush(path, schema); }
+ this._valuePush(i, propertyValue);
+ try {
+ this._validate(propertyValue);
+ } finally {
+ this._valuePop();
+ for (let j = 0, jj = schemaPath.length; j < jj; ++j) { this._schemaPop(); }
+ }
}
}
- _validateArrayContains(value, schema, info) {
- const containsSchema = schema.contains;
+ _validateArrayContains(value) {
+ const containsSchema = this._schema.contains;
if (!this._isObject(containsSchema)) { return; }
- info.schemaPush('contains', containsSchema);
- for (let i = 0, ii = value.length; i < ii; ++i) {
- const propertyValue = value[i];
- info.valuePush(i, propertyValue);
- try {
- this._validate(propertyValue, containsSchema, info);
- info.schemaPop();
- return;
- } catch (e) {
- // NOP
+ this._schemaPush('contains', containsSchema);
+ try {
+ for (let i = 0, ii = value.length; i < ii; ++i) {
+ const propertyValue = value[i];
+ this._valuePush(i, propertyValue);
+ try {
+ this._validate(propertyValue);
+ return;
+ } catch (e) {
+ // NOP
+ } finally {
+ this._valuePop();
+ }
}
- info.valuePop();
+ throw this._createError('contains schema didn\'t match');
+ } finally {
+ this._schemaPop();
}
- throw new JsonSchemaValidationError('contains schema didn\'t match', value, schema, info);
}
- _validateObject(value, schema, info) {
+ _validateObject(value) {
const properties = new Set(Object.getOwnPropertyNames(value));
- const required = schema.required;
+ const {required} = this._schema;
if (Array.isArray(required)) {
for (const property of required) {
if (!properties.has(property)) {
- throw new JsonSchemaValidationError(`Missing property ${property}`, value, schema, info);
+ throw this._createError(`Missing property ${property}`);
}
}
}
- const minProperties = schema.minProperties;
+ const {minProperties} = this._schema;
if (typeof minProperties === 'number' && properties.length < minProperties) {
- throw new JsonSchemaValidationError('Not enough object properties', value, schema, info);
+ throw this._createError('Not enough object properties');
}
- const maxProperties = schema.maxProperties;
+ const {maxProperties} = this._schema;
if (typeof maxProperties === 'number' && properties.length > maxProperties) {
- throw new JsonSchemaValidationError('Too many object properties', value, schema, info);
+ throw this._createError('Too many object properties');
}
for (const property of properties) {
- const schemaPath = [];
- const propertySchema = this._getPropertySchema(schema, property, value, schemaPath);
- if (propertySchema === null) {
- throw new JsonSchemaValidationError(`No schema found for ${property}`, value, schema, info);
+ const schemaPath = this._getObjectPropertySchemaPath(property);
+ if (schemaPath === null) {
+ throw this._createError(`No schema found for ${property}`);
}
const propertyValue = value[property];
- for (const [p, s] of schemaPath) { info.schemaPush(p, s); }
- info.valuePush(property, propertyValue);
- this._validate(propertyValue, propertySchema, info);
- info.valuePop();
- for (let i = 0; i < schemaPath.length; ++i) { info.schemaPop(); }
- }
- }
-
- _isValueTypeAny(value, type, schemaTypes) {
- if (typeof schemaTypes === 'string') {
- return this._isValueType(value, type, schemaTypes);
- } else if (Array.isArray(schemaTypes)) {
- for (const schemaType of schemaTypes) {
- if (this._isValueType(value, type, schemaType)) {
- return true;
- }
- }
- return false;
- }
- return true;
- }
-
- _isValueType(value, type, schemaType) {
- return (
- type === schemaType ||
- (schemaType === 'integer' && Math.floor(value) === value)
- );
- }
-
- _getValueType(value) {
- const type = typeof value;
- if (type === 'object') {
- if (value === null) { return 'null'; }
- if (Array.isArray(value)) { return 'array'; }
- }
- return type;
- }
-
- _valuesAreEqualAny(value1, valueList) {
- for (const value2 of valueList) {
- if (this._valuesAreEqual(value1, value2)) {
- return true;
+ for (const {path, schema} of schemaPath) { this._schemaPush(path, schema); }
+ this._valuePush(property, propertyValue);
+ try {
+ this._validate(propertyValue);
+ } finally {
+ this._valuePop();
+ for (let j = 0, jj = schemaPath.length; j < jj; ++j) { this._schemaPop(); }
}
}
- return false;
}
- _valuesAreEqual(value1, value2) {
- return value1 === value2;
- }
+ // Creation
_getDefaultTypeValue(type) {
if (typeof type === 'string') {
@@ -603,9 +633,8 @@ class JsonSchemaValidator {
return null;
}
- _getDefaultSchemaValue(schema) {
- const schemaType = schema.type;
- const schemaDefault = schema.default;
+ _getDefaultSchemaValue() {
+ const {type: schemaType, default: schemaDefault} = this._schema;
return (
typeof schemaDefault !== 'undefined' &&
this._isValueTypeAny(schemaDefault, this._getValueType(schemaDefault), schemaType) ?
@@ -614,24 +643,35 @@ class JsonSchemaValidator {
);
}
- _getValidValueOrDefault(schema, value, info) {
+ _getValidValueOrDefault(path, value, schemaPath) {
+ this._valuePush(path, value);
+ for (const {path: path2, schema} of schemaPath) { this._schemaPush(path2, schema); }
+ try {
+ return this._getValidValueOrDefaultInner(value);
+ } finally {
+ for (let i = 0, ii = schemaPath.length; i < ii; ++i) { this._schemaPop(); }
+ this._valuePop();
+ }
+ }
+
+ _getValidValueOrDefaultInner(value) {
let type = this._getValueType(value);
- if (typeof value === 'undefined' || !this._isValueTypeAny(value, type, schema.type)) {
- value = this._getDefaultSchemaValue(schema);
+ if (typeof value === 'undefined' || !this._isValueTypeAny(value, type, this._schema.type)) {
+ value = this._getDefaultSchemaValue();
type = this._getValueType(value);
}
switch (type) {
case 'object':
- value = this._populateObjectDefaults(value, schema, info);
+ value = this._populateObjectDefaults(value);
break;
case 'array':
- value = this._populateArrayDefaults(value, schema, info);
+ value = this._populateArrayDefaults(value);
break;
default:
- if (!this.isValid(value, schema)) {
- const schemaDefault = this._getDefaultSchemaValue(schema);
- if (this.isValid(schemaDefault, schema)) {
+ if (!this._isValidCurrent(value)) {
+ const schemaDefault = this._getDefaultSchemaValue();
+ if (this._isValidCurrent(schemaDefault)) {
value = schemaDefault;
}
}
@@ -641,126 +681,169 @@ class JsonSchemaValidator {
return value;
}
- _populateObjectDefaults(value, schema, info) {
+ _populateObjectDefaults(value) {
const properties = new Set(Object.getOwnPropertyNames(value));
- const required = schema.required;
+ const {required} = this._schema;
if (Array.isArray(required)) {
for (const property of required) {
properties.delete(property);
-
- const propertySchema = this._getPropertySchema(schema, property, value, null);
- if (propertySchema === null) { continue; }
- info.valuePush(property, value);
- info.schemaPush(property, propertySchema);
- const hasValue = Object.prototype.hasOwnProperty.call(value, property);
- value[property] = this._getValidValueOrDefault(propertySchema, hasValue ? value[property] : void 0, info);
- info.schemaPop();
- info.valuePop();
+ const schemaPath = this._getObjectPropertySchemaPath(property);
+ if (schemaPath === null) { continue; }
+ const propertyValue = Object.prototype.hasOwnProperty.call(value, property) ? value[property] : void 0;
+ value[property] = this._getValidValueOrDefault(property, propertyValue, schemaPath);
}
}
for (const property of properties) {
- const propertySchema = this._getPropertySchema(schema, property, value, null);
- if (propertySchema === null) {
+ const schemaPath = this._getObjectPropertySchemaPath(property);
+ if (schemaPath === null) {
Reflect.deleteProperty(value, property);
} else {
- info.valuePush(property, value);
- info.schemaPush(property, propertySchema);
- value[property] = this._getValidValueOrDefault(propertySchema, value[property], info);
- info.schemaPop();
- info.valuePop();
+ value[property] = this._getValidValueOrDefault(property, value[property], schemaPath);
}
}
return value;
}
- _populateArrayDefaults(value, schema, info) {
+ _populateArrayDefaults(value) {
for (let i = 0, ii = value.length; i < ii; ++i) {
- const propertySchema = this._getPropertySchema(schema, i, value, null);
- if (propertySchema === null) { continue; }
- info.valuePush(i, value);
- info.schemaPush(i, propertySchema);
- value[i] = this._getValidValueOrDefault(propertySchema, value[i], info);
- info.schemaPop();
- info.valuePop();
+ const schemaPath = this._getArrayItemSchemaPath(i);
+ if (schemaPath === null) { continue; }
+ const propertyValue = value[i];
+ value[i] = this._getValidValueOrDefault(i, propertyValue, schemaPath);
}
- const minItems = schema.minItems;
+ const {minItems, maxItems} = this._schema;
if (typeof minItems === 'number' && value.length < minItems) {
for (let i = value.length; i < minItems; ++i) {
- const propertySchema = this._getPropertySchema(schema, i, value, null);
- if (propertySchema === null) { break; }
- info.valuePush(i, value);
- info.schemaPush(i, propertySchema);
- const item = this._getValidValueOrDefault(propertySchema, void 0, info);
- info.schemaPop();
- info.valuePop();
+ const schemaPath = this._getArrayItemSchemaPath(i);
+ if (schemaPath === null) { break; }
+ const item = this._getValidValueOrDefault(i, void 0, schemaPath);
value.push(item);
}
}
- const maxItems = schema.maxItems;
if (typeof maxItems === 'number' && value.length > maxItems) {
value.splice(maxItems, value.length - maxItems);
}
return value;
}
+}
- _isObject(value) {
- return typeof value === 'object' && value !== null && !Array.isArray(value);
+class JsonSchemaProxyHandler {
+ constructor(schema) {
+ this._schema = schema;
+ this._numberPattern = /^(?:0|[1-9]\d*)$/;
}
- _getRegex(pattern, flags) {
- const key = `${flags}:${pattern}`;
- let regex = this._regexCache.get(key);
- if (typeof regex === 'undefined') {
- regex = new RegExp(pattern, flags);
- this._regexCache.set(key, regex);
+ getPrototypeOf(target) {
+ return Object.getPrototypeOf(target);
+ }
+
+ setPrototypeOf() {
+ throw new Error('setPrototypeOf not supported');
+ }
+
+ isExtensible(target) {
+ return Object.isExtensible(target);
+ }
+
+ preventExtensions(target) {
+ Object.preventExtensions(target);
+ return true;
+ }
+
+ getOwnPropertyDescriptor(target, property) {
+ return Object.getOwnPropertyDescriptor(target, property);
+ }
+
+ defineProperty() {
+ throw new Error('defineProperty not supported');
+ }
+
+ has(target, property) {
+ return property in target;
+ }
+
+ get(target, property) {
+ if (typeof property === 'symbol') { return target[property]; }
+
+ let propertySchema;
+ if (Array.isArray(target)) {
+ property = this._getArrayIndex(property);
+ if (property === null) {
+ // Note: this does not currently wrap mutating functions like push, pop, shift, unshift, splice
+ return target[property];
+ }
+ propertySchema = this._schema.getArrayItemSchema(property);
+ } else {
+ propertySchema = this._schema.getObjectPropertySchema(property);
}
- return regex;
+
+ if (propertySchema === null) { return void 0; }
+
+ const value = target[property];
+ return value !== null && typeof value === 'object' ? propertySchema.createProxy(value) : value;
}
-}
-Object.defineProperty(JsonSchemaValidator, 'unconstrainedSchema', {
- value: Object.freeze({}),
- configurable: false,
- enumerable: true,
- writable: false
-});
+ set(target, property, value) {
+ if (typeof property === 'symbol') { throw new Error(`Cannot assign symbol property ${property}`); }
+
+ let propertySchema;
+ if (Array.isArray(target)) {
+ property = this._getArrayIndex(property);
+ if (property === null) { throw new Error(`Property ${property} cannot be assigned to array`); }
+ if (property > target.length) { throw new Error('Array index out of range'); }
+ propertySchema = this._schema.getArrayItemSchema(property);
+ } else {
+ propertySchema = this._schema.getObjectPropertySchema(property);
+ }
+
+ if (propertySchema === null) { throw new Error(`Property ${property} not supported`); }
+
+ value = clone(value);
+ propertySchema.validate(value);
-class JsonSchemaTraversalInfo {
- constructor(value, schema) {
- this.valuePath = [];
- this.schemaPath = [];
- this.valuePush(null, value);
- this.schemaPush(null, schema);
+ target[property] = value;
+ return true;
}
- valuePush(path, value) {
- this.valuePath.push([path, value]);
+ deleteProperty(target, property) {
+ const required = (
+ (typeof target === 'object' && target !== null) ?
+ (Array.isArray(target) || this._schema.isObjectPropertyRequired(property)) :
+ true
+ );
+ if (required) {
+ throw new Error(`${property} cannot be deleted`);
+ }
+ return Reflect.deleteProperty(target, property);
}
- valuePop() {
- this.valuePath.pop();
+ ownKeys(target) {
+ return Reflect.ownKeys(target);
}
- schemaPush(path, schema) {
- this.schemaPath.push([path, schema]);
+ apply() {
+ throw new Error('apply not supported');
}
- schemaPop() {
- this.schemaPath.pop();
+ construct() {
+ throw new Error('construct not supported');
}
-}
-class JsonSchemaValidationError extends Error {
- constructor(message, value, schema, info) {
- super(message);
- this.value = value;
- this.schema = schema;
- this.info = info;
+ // Private
+
+ _getArrayIndex(property) {
+ if (typeof property === 'string' && this._numberPattern.test(property)) {
+ return Number.parseInt(property, 10);
+ } else if (typeof property === 'number' && Math.floor(property) === property && property >= 0) {
+ return property;
+ } else {
+ return null;
+ }
}
}