2019-11-28 17:19:15 +00:00
|
|
|
/*
|
2020-04-10 18:06:55 +00:00
|
|
|
* Copyright (C) 2019-2020 Yomichan Authors
|
2019-11-28 17:19:15 +00:00
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
2020-01-01 17:00:31 +00:00
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
2019-11-28 17:19:15 +00:00
|
|
|
*/
|
|
|
|
|
2020-08-09 18:18:59 +00:00
|
|
|
/* global
|
|
|
|
* CacheMap
|
|
|
|
*/
|
2019-11-28 17:19:15 +00:00
|
|
|
|
|
|
|
class JsonSchemaProxyHandler {
|
2020-08-09 18:18:59 +00:00
|
|
|
constructor(schema, jsonSchemaValidator) {
|
2019-11-28 17:19:15 +00:00
|
|
|
this._schema = schema;
|
2020-08-09 18:18:59 +00:00
|
|
|
this._jsonSchemaValidator = jsonSchemaValidator;
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Array.isArray(target)) {
|
|
|
|
if (typeof property === 'string' && /^\d+$/.test(property)) {
|
|
|
|
property = parseInt(property, 10);
|
|
|
|
} else if (typeof property === 'string') {
|
|
|
|
return target[property];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-09 18:18:59 +00:00
|
|
|
const propertySchema = this._jsonSchemaValidator.getPropertySchema(this._schema, property, target);
|
2019-11-28 17:19:15 +00:00
|
|
|
if (propertySchema === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const value = target[property];
|
2020-08-15 21:23:09 +00:00
|
|
|
return value !== null && typeof value === 'object' ? this._jsonSchemaValidator.createProxy(value, propertySchema) : value;
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-09 18:18:59 +00:00
|
|
|
const propertySchema = this._jsonSchemaValidator.getPropertySchema(this._schema, property, target);
|
2019-11-28 17:19:15 +00:00
|
|
|
if (propertySchema === null) {
|
|
|
|
throw new Error(`Property ${property} not supported`);
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
value = clone(value);
|
2019-11-28 17:19:15 +00:00
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
this._jsonSchemaValidator.validate(value, propertySchema);
|
2019-11-28 17:19:15 +00:00
|
|
|
|
|
|
|
target[property] = value;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
deleteProperty(target, property) {
|
|
|
|
const required = this._schema.required;
|
|
|
|
if (Array.isArray(required) && required.includes(property)) {
|
|
|
|
throw new Error(`${property} cannot be deleted`);
|
|
|
|
}
|
|
|
|
return Reflect.deleteProperty(target, property);
|
|
|
|
}
|
|
|
|
|
|
|
|
ownKeys(target) {
|
|
|
|
return Reflect.ownKeys(target);
|
|
|
|
}
|
|
|
|
|
|
|
|
apply() {
|
|
|
|
throw new Error('apply not supported');
|
|
|
|
}
|
|
|
|
|
|
|
|
construct() {
|
|
|
|
throw new Error('construct not supported');
|
|
|
|
}
|
2020-08-09 18:18:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class JsonSchemaValidator {
|
|
|
|
constructor() {
|
|
|
|
this._regexCache = new CacheMap(100, (pattern, flags) => new RegExp(pattern, flags));
|
|
|
|
}
|
2019-11-28 17:19:15 +00:00
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
createProxy(target, schema) {
|
|
|
|
return new Proxy(target, new JsonSchemaProxyHandler(schema, this));
|
|
|
|
}
|
|
|
|
|
|
|
|
isValid(value, schema) {
|
|
|
|
try {
|
|
|
|
this.validate(value, schema);
|
|
|
|
return true;
|
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
validate(value, schema) {
|
|
|
|
const info = new JsonSchemaTraversalInfo(value, schema);
|
|
|
|
this._validate(value, schema, info);
|
|
|
|
}
|
|
|
|
|
|
|
|
getValidValueOrDefault(schema, value) {
|
|
|
|
let type = this._getValueType(value);
|
|
|
|
const schemaType = schema.type;
|
2020-09-11 18:15:08 +00:00
|
|
|
if (typeof value === 'undefined' || !this._isValueTypeAny(value, type, schemaType)) {
|
2020-08-15 21:23:09 +00:00
|
|
|
let assignDefault = true;
|
|
|
|
|
|
|
|
const schemaDefault = schema.default;
|
|
|
|
if (typeof schemaDefault !== 'undefined') {
|
|
|
|
value = clone(schemaDefault);
|
|
|
|
type = this._getValueType(value);
|
|
|
|
assignDefault = !this._isValueTypeAny(value, type, schemaType);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (assignDefault) {
|
|
|
|
value = this._getDefaultTypeValue(schemaType);
|
|
|
|
type = this._getValueType(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case 'object':
|
|
|
|
value = this._populateObjectDefaults(value, schema);
|
|
|
|
break;
|
|
|
|
case 'array':
|
|
|
|
value = this._populateArrayDefaults(value, schema);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
getPropertySchema(schema, property, value) {
|
|
|
|
return this._getPropertySchema(schema, property, value, null);
|
|
|
|
}
|
|
|
|
|
2020-09-04 21:44:00 +00:00
|
|
|
clearCache() {
|
|
|
|
this._regexCache.clear();
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
// Private
|
|
|
|
|
|
|
|
_getPropertySchema(schema, property, value, path) {
|
|
|
|
const type = this._getSchemaOrValueType(schema, value);
|
2019-11-28 17:19:15 +00:00
|
|
|
switch (type) {
|
|
|
|
case 'object':
|
|
|
|
{
|
|
|
|
const properties = schema.properties;
|
2020-08-15 21:23:09 +00:00
|
|
|
if (this._isObject(properties)) {
|
2020-02-02 16:13:26 +00:00
|
|
|
const propertySchema = properties[property];
|
2020-08-15 21:23:09 +00:00
|
|
|
if (this._isObject(propertySchema)) {
|
2020-02-02 16:18:13 +00:00
|
|
|
if (path !== null) { path.push(['properties', properties], [property, propertySchema]); }
|
2020-02-02 16:13:26 +00:00
|
|
|
return propertySchema;
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const additionalProperties = schema.additionalProperties;
|
2020-01-26 20:45:31 +00:00
|
|
|
if (additionalProperties === false) {
|
|
|
|
return null;
|
2020-08-15 21:23:09 +00:00
|
|
|
} else if (this._isObject(additionalProperties)) {
|
2020-02-02 16:18:13 +00:00
|
|
|
if (path !== null) { path.push(['additionalProperties', additionalProperties]); }
|
2020-01-26 20:45:31 +00:00
|
|
|
return additionalProperties;
|
|
|
|
} else {
|
2020-08-09 18:18:59 +00:00
|
|
|
const result = JsonSchemaValidator.unconstrainedSchema;
|
2020-02-02 16:18:13 +00:00
|
|
|
if (path !== null) { path.push([null, result]); }
|
|
|
|
return result;
|
2020-01-26 20:45:31 +00:00
|
|
|
}
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
case 'array':
|
|
|
|
{
|
|
|
|
const items = schema.items;
|
2020-08-15 21:23:09 +00:00
|
|
|
if (this._isObject(items)) {
|
2020-01-26 17:34:27 +00:00
|
|
|
return items;
|
|
|
|
}
|
|
|
|
if (Array.isArray(items)) {
|
|
|
|
if (property >= 0 && property < items.length) {
|
2020-02-02 16:13:26 +00:00
|
|
|
const propertySchema = items[property];
|
2020-08-15 21:23:09 +00:00
|
|
|
if (this._isObject(propertySchema)) {
|
2020-02-02 16:18:13 +00:00
|
|
|
if (path !== null) { path.push(['items', items], [property, propertySchema]); }
|
2020-02-02 16:13:26 +00:00
|
|
|
return propertySchema;
|
|
|
|
}
|
2020-01-26 17:34:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const additionalItems = schema.additionalItems;
|
|
|
|
if (additionalItems === false) {
|
|
|
|
return null;
|
2020-08-15 21:23:09 +00:00
|
|
|
} else if (this._isObject(additionalItems)) {
|
2020-02-02 16:18:13 +00:00
|
|
|
if (path !== null) { path.push(['additionalItems', additionalItems]); }
|
2020-01-26 17:34:27 +00:00
|
|
|
return additionalItems;
|
|
|
|
} else {
|
2020-08-09 18:18:59 +00:00
|
|
|
const result = JsonSchemaValidator.unconstrainedSchema;
|
2020-02-02 16:18:13 +00:00
|
|
|
if (path !== null) { path.push([null, result]); }
|
|
|
|
return result;
|
2020-01-26 17:34:27 +00:00
|
|
|
}
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
default:
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_getSchemaOrValueType(schema, value) {
|
2020-01-26 20:57:39 +00:00
|
|
|
const type = schema.type;
|
|
|
|
|
|
|
|
if (Array.isArray(type)) {
|
|
|
|
if (typeof value !== 'undefined') {
|
2020-08-15 21:23:09 +00:00
|
|
|
const valueType = this._getValueType(value);
|
2020-01-26 20:57:39 +00:00
|
|
|
if (type.indexOf(valueType) >= 0) {
|
|
|
|
return valueType;
|
|
|
|
}
|
|
|
|
}
|
2020-02-02 03:57:27 +00:00
|
|
|
return null;
|
2020-01-26 20:57:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof type === 'undefined') {
|
|
|
|
if (typeof value !== 'undefined') {
|
2020-08-15 21:23:09 +00:00
|
|
|
return this._getValueType(value);
|
2020-01-26 20:57:39 +00:00
|
|
|
}
|
2020-02-02 03:57:27 +00:00
|
|
|
return null;
|
2020-01-26 20:57:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_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);
|
2020-01-26 15:57:52 +00:00
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateConditional(value, schema, info) {
|
2020-02-02 15:17:16 +00:00
|
|
|
const ifSchema = schema.if;
|
2020-08-15 21:23:09 +00:00
|
|
|
if (!this._isObject(ifSchema)) { return; }
|
2020-01-26 16:13:13 +00:00
|
|
|
|
2020-02-02 15:17:16 +00:00
|
|
|
let okay = true;
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPush('if', ifSchema);
|
2020-02-02 15:17:16 +00:00
|
|
|
try {
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validate(value, ifSchema, info);
|
2020-02-02 15:17:16 +00:00
|
|
|
} catch (e) {
|
|
|
|
okay = false;
|
2020-01-26 16:13:13 +00:00
|
|
|
}
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPop();
|
2020-01-26 16:13:13 +00:00
|
|
|
|
2020-02-02 15:17:16 +00:00
|
|
|
const nextSchema = okay ? schema.then : schema.else;
|
2020-08-15 21:23:09 +00:00
|
|
|
if (this._isObject(nextSchema)) {
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPush(okay ? 'then' : 'else', nextSchema);
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validate(value, nextSchema, info);
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPop();
|
2020-01-26 16:13:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateAllOf(value, schema, info) {
|
2020-01-26 15:57:52 +00:00
|
|
|
const subSchemas = schema.allOf;
|
2020-02-02 05:13:46 +00:00
|
|
|
if (!Array.isArray(subSchemas)) { return; }
|
2020-01-26 15:57:52 +00:00
|
|
|
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPush('allOf', subSchemas);
|
2020-01-26 15:57:52 +00:00
|
|
|
for (let i = 0; i < subSchemas.length; ++i) {
|
2020-02-02 15:47:30 +00:00
|
|
|
const subSchema = subSchemas[i];
|
|
|
|
info.schemaPush(i, subSchema);
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validate(value, subSchema, info);
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPop();
|
2020-01-26 15:57:52 +00:00
|
|
|
}
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPop();
|
2020-01-26 15:57:52 +00:00
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateAnyOf(value, schema, info) {
|
2020-01-26 15:57:52 +00:00
|
|
|
const subSchemas = schema.anyOf;
|
2020-02-02 05:13:46 +00:00
|
|
|
if (!Array.isArray(subSchemas)) { return; }
|
2020-01-26 15:57:52 +00:00
|
|
|
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPush('anyOf', subSchemas);
|
2020-01-26 15:57:52 +00:00
|
|
|
for (let i = 0; i < subSchemas.length; ++i) {
|
2020-02-02 15:47:30 +00:00
|
|
|
const subSchema = subSchemas[i];
|
|
|
|
info.schemaPush(i, subSchema);
|
2020-02-02 05:13:46 +00:00
|
|
|
try {
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validate(value, subSchema, info);
|
2020-02-02 05:13:46 +00:00
|
|
|
return;
|
|
|
|
} catch (e) {
|
|
|
|
// NOP
|
|
|
|
}
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPop();
|
2020-01-26 15:57:52 +00:00
|
|
|
}
|
2020-02-02 05:13:46 +00:00
|
|
|
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError('0 anyOf schemas matched', value, schema, info);
|
|
|
|
// info.schemaPop(); // Unreachable
|
2020-01-26 15:57:52 +00:00
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateOneOf(value, schema, info) {
|
2020-01-26 15:57:52 +00:00
|
|
|
const subSchemas = schema.oneOf;
|
2020-02-02 05:13:46 +00:00
|
|
|
if (!Array.isArray(subSchemas)) { return; }
|
2020-01-26 15:57:52 +00:00
|
|
|
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPush('oneOf', subSchemas);
|
2020-01-26 15:57:52 +00:00
|
|
|
let count = 0;
|
|
|
|
for (let i = 0; i < subSchemas.length; ++i) {
|
2020-02-02 15:47:30 +00:00
|
|
|
const subSchema = subSchemas[i];
|
|
|
|
info.schemaPush(i, subSchema);
|
2020-02-02 05:13:46 +00:00
|
|
|
try {
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validate(value, subSchema, info);
|
2020-02-02 05:13:46 +00:00
|
|
|
++count;
|
|
|
|
} catch (e) {
|
|
|
|
// NOP
|
|
|
|
}
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPop();
|
2020-02-02 05:13:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (count !== 1) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`${count} oneOf schemas matched`, value, schema, info);
|
2020-01-26 15:57:52 +00:00
|
|
|
}
|
2020-02-02 15:47:30 +00:00
|
|
|
|
|
|
|
info.schemaPop();
|
2020-01-26 15:57:52 +00:00
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateNoneOf(value, schema, info) {
|
2020-01-26 15:57:52 +00:00
|
|
|
const subSchemas = schema.not;
|
2020-02-02 05:13:46 +00:00
|
|
|
if (!Array.isArray(subSchemas)) { return; }
|
2020-01-26 15:57:52 +00:00
|
|
|
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPush('not', subSchemas);
|
2020-01-26 15:57:52 +00:00
|
|
|
for (let i = 0; i < subSchemas.length; ++i) {
|
2020-02-02 15:47:30 +00:00
|
|
|
const subSchema = subSchemas[i];
|
|
|
|
info.schemaPush(i, subSchema);
|
2020-02-02 05:13:46 +00:00
|
|
|
try {
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validate(value, subSchema, info);
|
2020-02-02 05:13:46 +00:00
|
|
|
} catch (e) {
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPop();
|
2020-02-02 05:13:46 +00:00
|
|
|
continue;
|
|
|
|
}
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`not[${i}] schema matched`, value, schema, info);
|
2020-01-26 15:57:52 +00:00
|
|
|
}
|
2020-02-02 15:47:30 +00:00
|
|
|
info.schemaPop();
|
2020-01-26 15:57:52 +00:00
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateSingleSchema(value, schema, info) {
|
|
|
|
const type = this._getValueType(value);
|
2019-11-28 17:19:15 +00:00
|
|
|
const schemaType = schema.type;
|
2020-08-15 21:23:09 +00:00
|
|
|
if (!this._isValueTypeAny(value, type, schemaType)) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`Value type ${type} does not match schema type ${schemaType}`, value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
2020-08-11 23:21:26 +00:00
|
|
|
const schemaConst = schema.const;
|
2020-08-15 21:23:09 +00:00
|
|
|
if (typeof schemaConst !== 'undefined' && !this._valuesAreEqual(value, schemaConst)) {
|
2020-08-11 23:21:26 +00:00
|
|
|
throw new JsonSchemaValidationError('Invalid constant value', value, schema, info);
|
|
|
|
}
|
|
|
|
|
2019-11-28 17:19:15 +00:00
|
|
|
const schemaEnum = schema.enum;
|
2020-08-15 21:23:09 +00:00
|
|
|
if (Array.isArray(schemaEnum) && !this._valuesAreEqualAny(value, schemaEnum)) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError('Invalid enum value', value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case 'number':
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validateNumber(value, schema, info);
|
2020-02-02 05:13:46 +00:00
|
|
|
break;
|
2019-11-28 17:19:15 +00:00
|
|
|
case 'string':
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validateString(value, schema, info);
|
2020-02-02 05:13:46 +00:00
|
|
|
break;
|
2019-11-28 17:19:15 +00:00
|
|
|
case 'array':
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validateArray(value, schema, info);
|
2020-02-02 05:13:46 +00:00
|
|
|
break;
|
2019-11-28 17:19:15 +00:00
|
|
|
case 'object':
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validateObject(value, schema, info);
|
2020-02-02 05:13:46 +00:00
|
|
|
break;
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateNumber(value, schema, info) {
|
2019-11-28 17:19:15 +00:00
|
|
|
const multipleOf = schema.multipleOf;
|
|
|
|
if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`Number is not a multiple of ${multipleOf}`, value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const minimum = schema.minimum;
|
|
|
|
if (typeof minimum === 'number' && value < minimum) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`Number is less than ${minimum}`, value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const exclusiveMinimum = schema.exclusiveMinimum;
|
|
|
|
if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`Number is less than or equal to ${exclusiveMinimum}`, value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const maximum = schema.maximum;
|
|
|
|
if (typeof maximum === 'number' && value > maximum) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`Number is greater than ${maximum}`, value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const exclusiveMaximum = schema.exclusiveMaximum;
|
|
|
|
if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`Number is greater than or equal to ${exclusiveMaximum}`, value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateString(value, schema, info) {
|
2019-11-28 17:19:15 +00:00
|
|
|
const minLength = schema.minLength;
|
|
|
|
if (typeof minLength === 'number' && value.length < minLength) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError('String length too short', value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
2020-02-02 03:20:47 +00:00
|
|
|
const maxLength = schema.maxLength;
|
2019-11-28 17:19:15 +00:00
|
|
|
if (typeof maxLength === 'number' && value.length > maxLength) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError('String length too long', value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
2020-08-09 18:18:59 +00:00
|
|
|
|
|
|
|
const pattern = schema.pattern;
|
|
|
|
if (typeof pattern === 'string') {
|
|
|
|
let patternFlags = schema.patternFlags;
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!regex.test(value)) {
|
|
|
|
throw new JsonSchemaValidationError('Pattern match failed', value, schema, info);
|
|
|
|
}
|
|
|
|
}
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateArray(value, schema, info) {
|
2019-11-28 17:19:15 +00:00
|
|
|
const minItems = schema.minItems;
|
|
|
|
if (typeof minItems === 'number' && value.length < minItems) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError('Array length too short', value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const maxItems = schema.maxItems;
|
|
|
|
if (typeof maxItems === 'number' && value.length > maxItems) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError('Array length too long', value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validateArrayContains(value, schema, info);
|
2020-08-11 23:21:26 +00:00
|
|
|
|
2020-01-26 17:34:27 +00:00
|
|
|
for (let i = 0, ii = value.length; i < ii; ++i) {
|
2020-02-02 16:18:13 +00:00
|
|
|
const schemaPath = [];
|
2020-08-15 21:23:09 +00:00
|
|
|
const propertySchema = this._getPropertySchema(schema, i, value, schemaPath);
|
2020-01-26 17:34:27 +00:00
|
|
|
if (propertySchema === null) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`No schema found for array[${i}]`, value, schema, info);
|
2020-01-26 17:34:27 +00:00
|
|
|
}
|
|
|
|
|
2020-02-02 15:47:30 +00:00
|
|
|
const propertyValue = value[i];
|
|
|
|
|
2020-02-02 16:18:13 +00:00
|
|
|
for (const [p, s] of schemaPath) { info.schemaPush(p, s); }
|
2020-02-02 15:47:30 +00:00
|
|
|
info.valuePush(i, propertyValue);
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validate(propertyValue, propertySchema, info);
|
2020-02-02 15:47:30 +00:00
|
|
|
info.valuePop();
|
2020-02-17 20:21:30 +00:00
|
|
|
for (let j = 0, jj = schemaPath.length; j < jj; ++j) { info.schemaPop(); }
|
2020-01-26 17:34:27 +00:00
|
|
|
}
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateArrayContains(value, schema, info) {
|
2020-08-11 23:21:26 +00:00
|
|
|
const containsSchema = schema.contains;
|
2020-08-15 21:23:09 +00:00
|
|
|
if (!this._isObject(containsSchema)) { return; }
|
2020-08-11 23:21:26 +00:00
|
|
|
|
|
|
|
info.schemaPush('contains', containsSchema);
|
|
|
|
for (let i = 0, ii = value.length; i < ii; ++i) {
|
|
|
|
const propertyValue = value[i];
|
|
|
|
info.valuePush(i, propertyValue);
|
|
|
|
try {
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validate(propertyValue, containsSchema, info);
|
2020-08-11 23:21:26 +00:00
|
|
|
info.schemaPop();
|
|
|
|
return;
|
|
|
|
} catch (e) {
|
|
|
|
// NOP
|
|
|
|
}
|
|
|
|
info.valuePop();
|
|
|
|
}
|
|
|
|
throw new JsonSchemaValidationError('contains schema didn\'t match', value, schema, info);
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_validateObject(value, schema, info) {
|
2019-11-28 17:19:15 +00:00
|
|
|
const properties = new Set(Object.getOwnPropertyNames(value));
|
|
|
|
|
|
|
|
const required = schema.required;
|
|
|
|
if (Array.isArray(required)) {
|
|
|
|
for (const property of required) {
|
|
|
|
if (!properties.has(property)) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`Missing property ${property}`, value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const minProperties = schema.minProperties;
|
|
|
|
if (typeof minProperties === 'number' && properties.length < minProperties) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError('Not enough object properties', value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const maxProperties = schema.maxProperties;
|
|
|
|
if (typeof maxProperties === 'number' && properties.length > maxProperties) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError('Too many object properties', value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (const property of properties) {
|
2020-02-02 16:18:13 +00:00
|
|
|
const schemaPath = [];
|
2020-08-15 21:23:09 +00:00
|
|
|
const propertySchema = this._getPropertySchema(schema, property, value, schemaPath);
|
2019-11-28 17:19:15 +00:00
|
|
|
if (propertySchema === null) {
|
2020-02-02 15:47:30 +00:00
|
|
|
throw new JsonSchemaValidationError(`No schema found for ${property}`, value, schema, info);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
2020-02-02 15:47:30 +00:00
|
|
|
|
|
|
|
const propertyValue = value[property];
|
|
|
|
|
2020-02-02 16:18:13 +00:00
|
|
|
for (const [p, s] of schemaPath) { info.schemaPush(p, s); }
|
2020-02-02 15:47:30 +00:00
|
|
|
info.valuePush(property, propertyValue);
|
2020-08-15 21:23:09 +00:00
|
|
|
this._validate(propertyValue, propertySchema, info);
|
2020-02-02 15:47:30 +00:00
|
|
|
info.valuePop();
|
2020-02-02 16:18:13 +00:00
|
|
|
for (let i = 0; i < schemaPath.length; ++i) { info.schemaPop(); }
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_isValueTypeAny(value, type, schemaTypes) {
|
2019-11-28 17:19:15 +00:00
|
|
|
if (typeof schemaTypes === 'string') {
|
2020-08-15 21:23:09 +00:00
|
|
|
return this._isValueType(value, type, schemaTypes);
|
2019-11-28 17:19:15 +00:00
|
|
|
} else if (Array.isArray(schemaTypes)) {
|
|
|
|
for (const schemaType of schemaTypes) {
|
2020-08-15 21:23:09 +00:00
|
|
|
if (this._isValueType(value, type, schemaType)) {
|
2019-11-28 17:19:15 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_isValueType(value, type, schemaType) {
|
2019-11-28 17:19:15 +00:00
|
|
|
return (
|
|
|
|
type === schemaType ||
|
|
|
|
(schemaType === 'integer' && Math.floor(value) === value)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_getValueType(value) {
|
2019-11-28 17:19:15 +00:00
|
|
|
const type = typeof value;
|
|
|
|
if (type === 'object') {
|
|
|
|
if (value === null) { return 'null'; }
|
|
|
|
if (Array.isArray(value)) { return 'array'; }
|
|
|
|
}
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_valuesAreEqualAny(value1, valueList) {
|
2019-11-28 17:19:15 +00:00
|
|
|
for (const value2 of valueList) {
|
2020-08-15 21:23:09 +00:00
|
|
|
if (this._valuesAreEqual(value1, value2)) {
|
2019-11-28 17:19:15 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_valuesAreEqual(value1, value2) {
|
2019-11-28 17:19:15 +00:00
|
|
|
return value1 === value2;
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_getDefaultTypeValue(type) {
|
2019-11-28 17:19:15 +00:00
|
|
|
if (typeof type === 'string') {
|
|
|
|
switch (type) {
|
|
|
|
case 'null':
|
|
|
|
return null;
|
|
|
|
case 'boolean':
|
|
|
|
return false;
|
|
|
|
case 'number':
|
|
|
|
case 'integer':
|
|
|
|
return 0;
|
|
|
|
case 'string':
|
|
|
|
return '';
|
|
|
|
case 'array':
|
|
|
|
return [];
|
|
|
|
case 'object':
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_populateObjectDefaults(value, schema) {
|
2019-11-28 17:19:15 +00:00
|
|
|
const properties = new Set(Object.getOwnPropertyNames(value));
|
|
|
|
|
|
|
|
const required = schema.required;
|
|
|
|
if (Array.isArray(required)) {
|
|
|
|
for (const property of required) {
|
|
|
|
properties.delete(property);
|
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
const propertySchema = this._getPropertySchema(schema, property, value, null);
|
2019-11-28 17:19:15 +00:00
|
|
|
if (propertySchema === null) { continue; }
|
2020-08-09 18:18:59 +00:00
|
|
|
value[property] = this.getValidValueOrDefault(propertySchema, value[property]);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const property of properties) {
|
2020-08-15 21:23:09 +00:00
|
|
|
const propertySchema = this._getPropertySchema(schema, property, value, null);
|
2019-11-28 17:19:15 +00:00
|
|
|
if (propertySchema === null) {
|
|
|
|
Reflect.deleteProperty(value, property);
|
|
|
|
} else {
|
2020-08-09 18:18:59 +00:00
|
|
|
value[property] = this.getValidValueOrDefault(propertySchema, value[property]);
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
2019-12-29 23:57:29 +00:00
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_populateArrayDefaults(value, schema) {
|
2019-12-29 23:57:29 +00:00
|
|
|
for (let i = 0, ii = value.length; i < ii; ++i) {
|
2020-08-15 21:23:09 +00:00
|
|
|
const propertySchema = this._getPropertySchema(schema, i, value, null);
|
2019-12-29 23:57:29 +00:00
|
|
|
if (propertySchema === null) { continue; }
|
2020-08-09 18:18:59 +00:00
|
|
|
value[i] = this.getValidValueOrDefault(propertySchema, value[i]);
|
2019-12-29 23:57:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
2020-01-26 16:06:03 +00:00
|
|
|
|
2020-08-15 21:23:09 +00:00
|
|
|
_isObject(value) {
|
2020-01-26 16:06:03 +00:00
|
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
|
|
}
|
2020-08-09 18:18:59 +00:00
|
|
|
|
|
|
|
_getRegex(pattern, flags) {
|
|
|
|
const regex = this._regexCache.get(pattern, flags);
|
|
|
|
regex.lastIndex = 0;
|
|
|
|
return regex;
|
|
|
|
}
|
2019-11-28 17:19:15 +00:00
|
|
|
}
|
|
|
|
|
2020-08-09 18:18:59 +00:00
|
|
|
Object.defineProperty(JsonSchemaValidator, 'unconstrainedSchema', {
|
2020-07-03 15:55:39 +00:00
|
|
|
value: Object.freeze({}),
|
|
|
|
configurable: false,
|
|
|
|
enumerable: true,
|
|
|
|
writable: false
|
|
|
|
});
|
2020-01-26 20:45:31 +00:00
|
|
|
|
2020-02-02 15:47:30 +00:00
|
|
|
class JsonSchemaTraversalInfo {
|
|
|
|
constructor(value, schema) {
|
|
|
|
this.valuePath = [];
|
|
|
|
this.schemaPath = [];
|
2020-02-02 16:22:22 +00:00
|
|
|
this.valuePush(null, value);
|
|
|
|
this.schemaPush(null, schema);
|
2020-02-02 15:47:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
valuePush(path, value) {
|
|
|
|
this.valuePath.push([path, value]);
|
|
|
|
}
|
|
|
|
|
|
|
|
valuePop() {
|
|
|
|
this.valuePath.pop();
|
|
|
|
}
|
|
|
|
|
|
|
|
schemaPush(path, schema) {
|
|
|
|
this.schemaPath.push([path, schema]);
|
|
|
|
}
|
|
|
|
|
|
|
|
schemaPop() {
|
|
|
|
this.schemaPath.pop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-02 05:13:46 +00:00
|
|
|
class JsonSchemaValidationError extends Error {
|
2020-02-02 15:47:30 +00:00
|
|
|
constructor(message, value, schema, info) {
|
2020-02-02 05:13:46 +00:00
|
|
|
super(message);
|
|
|
|
this.value = value;
|
|
|
|
this.schema = schema;
|
2020-02-02 15:47:30 +00:00
|
|
|
this.info = info;
|
2020-02-02 05:13:46 +00:00
|
|
|
}
|
|
|
|
}
|