mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
1361 lines
43 KiB
JavaScript
1361 lines
43 KiB
JavaScript
/*
|
|
* Copyright (C) 2023-2025 Yomitan Authors
|
|
* Copyright (C) 2019-2022 Yomichan Authors
|
|
*
|
|
* 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
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import {clone} from '../core/utilities.js';
|
|
import {CacheMap} from '../general/cache-map.js';
|
|
|
|
export class JsonSchemaError extends Error {
|
|
/**
|
|
* @param {string} message
|
|
* @param {import('ext/json-schema').ValueStackItem[]} valueStack
|
|
* @param {import('ext/json-schema').SchemaStackItem[]} schemaStack
|
|
*/
|
|
constructor(message, valueStack, schemaStack) {
|
|
super(message);
|
|
/** @type {string} */
|
|
this.name = 'JsonSchemaError';
|
|
/** @type {import('ext/json-schema').ValueStackItem[]} */
|
|
this._valueStack = valueStack;
|
|
/** @type {import('ext/json-schema').SchemaStackItem[]} */
|
|
this._schemaStack = schemaStack;
|
|
}
|
|
|
|
/** @type {unknown|undefined} */
|
|
get value() { return this._valueStack.length > 0 ? this._valueStack[this._valueStack.length - 1].value : void 0; }
|
|
|
|
/** @type {import('ext/json-schema').Schema|import('ext/json-schema').Schema[]|undefined} */
|
|
get schema() { return this._schemaStack.length > 0 ? this._schemaStack[this._schemaStack.length - 1].schema : void 0; }
|
|
|
|
/** @type {import('ext/json-schema').ValueStackItem[]} */
|
|
get valueStack() { return this._valueStack; }
|
|
|
|
/** @type {import('ext/json-schema').SchemaStackItem[]} */
|
|
get schemaStack() { return this._schemaStack; }
|
|
}
|
|
|
|
export class JsonSchema {
|
|
/**
|
|
* @param {import('ext/json-schema').Schema} schema
|
|
* @param {import('ext/json-schema').Schema} [rootSchema]
|
|
*/
|
|
constructor(schema, rootSchema) {
|
|
/** @type {import('ext/json-schema').Schema} */
|
|
this._startSchema = schema;
|
|
/** @type {import('ext/json-schema').Schema} */
|
|
this._rootSchema = typeof rootSchema !== 'undefined' ? rootSchema : schema;
|
|
/** @type {?CacheMap<string, RegExp>} */
|
|
this._regexCache = null;
|
|
/** @type {?Map<string, {schema: import('ext/json-schema').Schema, stack: import('ext/json-schema').SchemaStackItem[]}>} */
|
|
this._refCache = null;
|
|
/** @type {import('ext/json-schema').ValueStackItem[]} */
|
|
this._valueStack = [];
|
|
/** @type {import('ext/json-schema').SchemaStackItem[]} */
|
|
this._schemaStack = [];
|
|
/** @type {?(jsonSchema: JsonSchema) => void} */
|
|
this._progress = null;
|
|
/** @type {number} */
|
|
this._progressCounter = 0;
|
|
/** @type {number} */
|
|
this._progressInterval = 1;
|
|
}
|
|
|
|
/** @type {import('ext/json-schema').Schema} */
|
|
get schema() {
|
|
return this._startSchema;
|
|
}
|
|
|
|
/** @type {import('ext/json-schema').Schema} */
|
|
get rootSchema() {
|
|
return this._rootSchema;
|
|
}
|
|
|
|
/** @type {?(jsonSchema: JsonSchema) => void} */
|
|
get progress() {
|
|
return this._progress;
|
|
}
|
|
|
|
set progress(value) {
|
|
this._progress = value;
|
|
}
|
|
|
|
/** @type {number} */
|
|
get progressInterval() {
|
|
return this._progressInterval;
|
|
}
|
|
|
|
set progressInterval(value) {
|
|
this._progressInterval = value;
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Value} value
|
|
* @returns {import('ext/json-schema').Value}
|
|
*/
|
|
createProxy(value) {
|
|
return (
|
|
typeof value === 'object' && value !== null ?
|
|
new Proxy(value, new JsonSchemaProxyHandler(this)) :
|
|
value
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} value
|
|
* @returns {boolean}
|
|
*/
|
|
isValid(value) {
|
|
try {
|
|
this.validate(value);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} value
|
|
*/
|
|
validate(value) {
|
|
const schema = this._startSchema;
|
|
this._schemaPush(schema, null);
|
|
this._valuePush(value, null);
|
|
try {
|
|
this._validate(schema, value);
|
|
} finally {
|
|
this._valuePop();
|
|
this._schemaPop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} [value]
|
|
* @returns {import('ext/json-schema').Value}
|
|
*/
|
|
getValidValueOrDefault(value) {
|
|
const schema = this._startSchema;
|
|
return this._getValidValueOrDefault(schema, null, value, [{schema, path: null}]);
|
|
}
|
|
|
|
/**
|
|
* @param {string} property
|
|
* @returns {?JsonSchema}
|
|
*/
|
|
getObjectPropertySchema(property) {
|
|
const schema = this._startSchema;
|
|
const {schema: schema2, stack} = this._getResolvedSchemaInfo(schema, [{schema, path: null}]);
|
|
this._schemaPushMultiple(stack);
|
|
try {
|
|
const {schema: propertySchema} = this._getObjectPropertySchemaInfo(schema2, property);
|
|
return propertySchema !== false ? new JsonSchema(propertySchema, this._rootSchema) : null;
|
|
} finally {
|
|
this._schemaPopMultiple(stack.length);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {number} index
|
|
* @returns {?JsonSchema}
|
|
*/
|
|
getArrayItemSchema(index) {
|
|
const schema = this._startSchema;
|
|
const {schema: schema2, stack} = this._getResolvedSchemaInfo(schema, [{schema, path: null}]);
|
|
this._schemaPushMultiple(stack);
|
|
try {
|
|
const {schema: itemSchema} = this._getArrayItemSchemaInfo(schema2, index);
|
|
return itemSchema !== false ? new JsonSchema(itemSchema, this._rootSchema) : null;
|
|
} finally {
|
|
this._schemaPopMultiple(stack.length);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} property
|
|
* @returns {boolean}
|
|
*/
|
|
isObjectPropertyRequired(property) {
|
|
const schema = this._startSchema;
|
|
if (typeof schema === 'boolean') { return false; }
|
|
const {required} = schema;
|
|
return Array.isArray(required) && required.includes(property);
|
|
}
|
|
|
|
// Internal state functions for error construction and progress callback
|
|
|
|
/**
|
|
* @returns {import('ext/json-schema').ValueStackItem[]}
|
|
*/
|
|
getValueStack() {
|
|
const result = [];
|
|
for (const {value, path} of this._valueStack) {
|
|
result.push({value, path});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @returns {import('ext/json-schema').SchemaStackItem[]}
|
|
*/
|
|
getSchemaStack() {
|
|
const result = [];
|
|
for (const {schema, path} of this._schemaStack) {
|
|
result.push({schema, path});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @returns {number}
|
|
*/
|
|
getValueStackLength() {
|
|
return this._valueStack.length - 1;
|
|
}
|
|
|
|
/**
|
|
* @param {number} index
|
|
* @returns {import('ext/json-schema').ValueStackItem}
|
|
*/
|
|
getValueStackItem(index) {
|
|
const {value, path} = this._valueStack[index + 1];
|
|
return {value, path};
|
|
}
|
|
|
|
/**
|
|
* @returns {number}
|
|
*/
|
|
getSchemaStackLength() {
|
|
return this._schemaStack.length - 1;
|
|
}
|
|
|
|
/**
|
|
* @param {number} index
|
|
* @returns {import('ext/json-schema').SchemaStackItem}
|
|
*/
|
|
getSchemaStackItem(index) {
|
|
const {schema, path} = this._schemaStack[index + 1];
|
|
return {schema, path};
|
|
}
|
|
|
|
/**
|
|
* @template [T=unknown]
|
|
* @param {T} value
|
|
* @returns {T}
|
|
*/
|
|
static clone(value) {
|
|
return clone(value);
|
|
}
|
|
|
|
// Stack
|
|
|
|
/**
|
|
* @param {unknown} value
|
|
* @param {string|number|null} path
|
|
*/
|
|
_valuePush(value, path) {
|
|
this._valueStack.push({value, path});
|
|
}
|
|
|
|
/**
|
|
* @returns {void}
|
|
*/
|
|
_valuePop() {
|
|
this._valueStack.pop();
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Schema|import('ext/json-schema').Schema[]} schema
|
|
* @param {string|number|null} path
|
|
*/
|
|
_schemaPush(schema, path) {
|
|
this._schemaStack.push({schema, path});
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaStackItem[]} items
|
|
*/
|
|
_schemaPushMultiple(items) {
|
|
this._schemaStack.push(...items);
|
|
}
|
|
|
|
/**
|
|
* @returns {void}
|
|
*/
|
|
_schemaPop() {
|
|
this._schemaStack.pop();
|
|
}
|
|
|
|
/**
|
|
* @param {number} count
|
|
*/
|
|
_schemaPopMultiple(count) {
|
|
for (let i = 0; i < count; ++i) {
|
|
this._schemaStack.pop();
|
|
}
|
|
}
|
|
|
|
// Private
|
|
|
|
/**
|
|
* @param {string} message
|
|
* @returns {JsonSchemaError}
|
|
*/
|
|
_createError(message) {
|
|
const valueStack = this.getValueStack();
|
|
const schemaStack = this.getSchemaStack();
|
|
return new JsonSchemaError(message, valueStack, schemaStack);
|
|
}
|
|
|
|
/**
|
|
* @param {string} pattern
|
|
* @param {string} flags
|
|
* @returns {RegExp}
|
|
*/
|
|
_getRegex(pattern, flags) {
|
|
if (this._regexCache === null) {
|
|
this._regexCache = new CacheMap(100);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Schema} schema
|
|
* @param {string} property
|
|
* @returns {{schema: import('ext/json-schema').Schema, stack: import('ext/json-schema').SchemaStackItem[]}}
|
|
*/
|
|
_getObjectPropertySchemaInfo(schema, property) {
|
|
if (typeof schema === 'boolean') {
|
|
return {schema, stack: [{schema, path: null}]};
|
|
}
|
|
const {properties} = schema;
|
|
if (typeof properties !== 'undefined' && Object.prototype.hasOwnProperty.call(properties, property)) {
|
|
const propertySchema = properties[property];
|
|
if (typeof propertySchema !== 'undefined') {
|
|
return {
|
|
schema: propertySchema,
|
|
stack: [
|
|
{schema: properties, path: 'properties'},
|
|
{schema: propertySchema, path: property},
|
|
],
|
|
};
|
|
}
|
|
}
|
|
return this._getOptionalSchemaInfo(schema.additionalProperties, 'additionalProperties');
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Schema} schema
|
|
* @param {number} index
|
|
* @returns {{schema: import('ext/json-schema').Schema, stack: import('ext/json-schema').SchemaStackItem[]}}
|
|
*/
|
|
_getArrayItemSchemaInfo(schema, index) {
|
|
if (typeof schema === 'boolean') {
|
|
return {schema, stack: [{schema, path: null}]};
|
|
}
|
|
const {prefixItems} = schema;
|
|
if (typeof prefixItems !== 'undefined' && index >= 0 && index < prefixItems.length) {
|
|
const itemSchema = prefixItems[index];
|
|
if (typeof itemSchema !== 'undefined') {
|
|
return {
|
|
schema: itemSchema,
|
|
stack: [
|
|
{schema: prefixItems, path: 'prefixItems'},
|
|
{schema: itemSchema, path: index},
|
|
],
|
|
};
|
|
}
|
|
}
|
|
const {items} = schema;
|
|
if (typeof items !== 'undefined') {
|
|
if (Array.isArray(items)) { // Legacy schema format
|
|
if (index >= 0 && index < items.length) {
|
|
const itemSchema = items[index];
|
|
if (typeof itemSchema !== 'undefined') {
|
|
return {
|
|
schema: itemSchema,
|
|
stack: [
|
|
{schema: items, path: 'items'},
|
|
{schema: itemSchema, path: index},
|
|
],
|
|
};
|
|
}
|
|
}
|
|
} else {
|
|
return {
|
|
schema: items,
|
|
stack: [{schema: items, path: 'items'}],
|
|
};
|
|
}
|
|
}
|
|
return this._getOptionalSchemaInfo(schema.additionalItems, 'additionalItems');
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Schema|undefined} schema
|
|
* @param {string|number|null} path
|
|
* @returns {{schema: import('ext/json-schema').Schema, stack: import('ext/json-schema').SchemaStackItem[]}}
|
|
*/
|
|
_getOptionalSchemaInfo(schema, path) {
|
|
switch (typeof schema) {
|
|
case 'boolean':
|
|
case 'object':
|
|
break;
|
|
default:
|
|
schema = true;
|
|
path = null;
|
|
break;
|
|
}
|
|
return {schema, stack: [{schema, path}]};
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} value
|
|
* @returns {?import('ext/json-schema').Type}
|
|
* @throws {Error}
|
|
*/
|
|
_getValueType(value) {
|
|
const type = typeof value;
|
|
switch (type) {
|
|
case 'object':
|
|
if (value === null) { return 'null'; }
|
|
if (Array.isArray(value)) { return 'array'; }
|
|
return 'object';
|
|
case 'string':
|
|
case 'number':
|
|
case 'boolean':
|
|
return type;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} value
|
|
* @param {?import('ext/json-schema').Type} type
|
|
* @param {import('ext/json-schema').Type|import('ext/json-schema').Type[]|undefined} schemaTypes
|
|
* @returns {boolean}
|
|
*/
|
|
_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;
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} value
|
|
* @param {?import('ext/json-schema').Type} type
|
|
* @param {import('ext/json-schema').Type} schemaType
|
|
* @returns {boolean}
|
|
*/
|
|
_isValueType(value, type, schemaType) {
|
|
return (
|
|
type === schemaType ||
|
|
(schemaType === 'integer' && typeof value === 'number' && Math.floor(value) === value)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} value1
|
|
* @param {import('ext/json-schema').Value[]} valueList
|
|
* @returns {boolean}
|
|
*/
|
|
_valuesAreEqualAny(value1, valueList) {
|
|
for (const value2 of valueList) {
|
|
if (this._valuesAreEqual(value1, value2)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} value1
|
|
* @param {import('ext/json-schema').Value} value2
|
|
* @returns {boolean}
|
|
*/
|
|
_valuesAreEqual(value1, value2) {
|
|
return value1 === value2;
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Schema} schema
|
|
* @param {import('ext/json-schema').SchemaStackItem[]} stack
|
|
* @returns {{schema: import('ext/json-schema').Schema, stack: import('ext/json-schema').SchemaStackItem[]}}
|
|
*/
|
|
_getResolvedSchemaInfo(schema, stack) {
|
|
if (typeof schema !== 'boolean') {
|
|
const ref = schema.$ref;
|
|
if (typeof ref === 'string') {
|
|
const {schema: schema2, stack: stack2} = this._getReference(ref);
|
|
return {
|
|
schema: schema2,
|
|
stack: [...stack, ...stack2],
|
|
};
|
|
}
|
|
}
|
|
return {schema, stack};
|
|
}
|
|
|
|
/**
|
|
* @param {string} ref
|
|
* @returns {{schema: import('ext/json-schema').Schema, stack: import('ext/json-schema').SchemaStackItem[]}}
|
|
* @throws {Error}
|
|
*/
|
|
_getReference(ref) {
|
|
if (!ref.startsWith('#/')) {
|
|
throw this._createError(`Unsupported reference path: ${ref}`);
|
|
}
|
|
|
|
/** @type {{schema: import('ext/json-schema').Schema, stack: import('ext/json-schema').SchemaStackItem[]}|undefined} */
|
|
let info;
|
|
if (this._refCache !== null) {
|
|
info = this._refCache.get(ref);
|
|
} else {
|
|
this._refCache = new Map();
|
|
}
|
|
|
|
if (typeof info === 'undefined') {
|
|
info = this._getReferenceUncached(ref);
|
|
this._refCache.set(ref, info);
|
|
}
|
|
|
|
info.stack = this._copySchemaStack(info.stack);
|
|
return info;
|
|
}
|
|
|
|
/**
|
|
* @param {string} ref
|
|
* @returns {{schema: import('ext/json-schema').Schema, stack: import('ext/json-schema').SchemaStackItem[]}}
|
|
* @throws {Error}
|
|
*/
|
|
_getReferenceUncached(ref) {
|
|
/** @type {Set<string>} */
|
|
const visited = new Set();
|
|
/** @type {import('ext/json-schema').SchemaStackItem[]} */
|
|
const stack = [];
|
|
while (true) {
|
|
if (visited.has(ref)) {
|
|
throw this._createError(`Recursive reference: ${ref}`);
|
|
}
|
|
visited.add(ref);
|
|
|
|
const pathParts = ref.substring(2).split('/');
|
|
let schema = this._rootSchema;
|
|
stack.push({schema, path: null});
|
|
for (const pathPart of pathParts) {
|
|
if (!(typeof schema === 'object' && schema !== null && Object.prototype.hasOwnProperty.call(schema, pathPart))) {
|
|
throw this._createError(`Invalid reference: ${ref}`);
|
|
}
|
|
const schemaNext = /** @type {import('core').UnknownObject} */ (schema)[pathPart];
|
|
if (!(typeof schemaNext === 'boolean' || (typeof schemaNext === 'object' && schemaNext !== null))) {
|
|
throw this._createError(`Invalid reference: ${ref}`);
|
|
}
|
|
schema = schemaNext;
|
|
stack.push({schema, path: pathPart});
|
|
}
|
|
if (Array.isArray(schema)) {
|
|
throw this._createError(`Invalid reference: ${ref}`);
|
|
}
|
|
|
|
const refNext = typeof schema === 'object' && schema !== null ? schema.$ref : void 0;
|
|
if (typeof refNext !== 'string') {
|
|
return {schema, stack};
|
|
}
|
|
ref = refNext;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaStackItem[]} schemaStack
|
|
* @returns {import('ext/json-schema').SchemaStackItem[]}
|
|
*/
|
|
_copySchemaStack(schemaStack) {
|
|
/** @type {import('ext/json-schema').SchemaStackItem[]} */
|
|
const results = [];
|
|
for (const {schema, path} of schemaStack) {
|
|
results.push({schema, path});
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// Validation
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {unknown} value
|
|
* @returns {boolean}
|
|
*/
|
|
_isValidCurrent(schema, value) {
|
|
try {
|
|
this._validate(schema, value);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Schema} schema
|
|
* @param {unknown} value
|
|
*/
|
|
_validate(schema, value) {
|
|
if (this._progress !== null) {
|
|
const counter = (this._progressCounter + 1) % this._progressInterval;
|
|
this._progressCounter = counter;
|
|
if (counter === 0) { this._progress(this); }
|
|
}
|
|
|
|
const {schema: schema2, stack} = this._getResolvedSchemaInfo(schema, []);
|
|
this._schemaPushMultiple(stack);
|
|
try {
|
|
this._validateInner(schema2, value);
|
|
} finally {
|
|
this._schemaPopMultiple(stack.length);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Schema} schema
|
|
* @param {unknown} value
|
|
* @throws {Error}
|
|
*/
|
|
_validateInner(schema, value) {
|
|
if (schema === true) { return; }
|
|
if (schema === false) { throw this._createError('False schema'); }
|
|
this._validateSingleSchema(schema, value);
|
|
this._validateConditional(schema, value);
|
|
this._validateAllOf(schema, value);
|
|
this._validateAnyOf(schema, value);
|
|
this._validateOneOf(schema, value);
|
|
this._validateNot(schema, value);
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {unknown} value
|
|
*/
|
|
_validateConditional(schema, value) {
|
|
const ifSchema = schema.if;
|
|
if (typeof ifSchema === 'undefined') { return; }
|
|
|
|
let okay = true;
|
|
this._schemaPush(ifSchema, 'if');
|
|
try {
|
|
this._validate(ifSchema, value);
|
|
} catch (e) {
|
|
okay = false;
|
|
} finally {
|
|
this._schemaPop();
|
|
}
|
|
|
|
const nextSchema = okay ? schema.then : schema.else;
|
|
if (typeof nextSchema === 'undefined') { return; }
|
|
|
|
this._schemaPush(nextSchema, okay ? 'then' : 'else');
|
|
try {
|
|
this._validate(nextSchema, value);
|
|
} finally {
|
|
this._schemaPop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {unknown} value
|
|
*/
|
|
_validateAllOf(schema, value) {
|
|
const subSchemas = schema.allOf;
|
|
if (!Array.isArray(subSchemas)) { return; }
|
|
|
|
this._schemaPush(subSchemas, 'allOf');
|
|
try {
|
|
for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
|
|
const subSchema = subSchemas[i];
|
|
this._schemaPush(subSchema, i);
|
|
try {
|
|
this._validate(subSchema, value);
|
|
} finally {
|
|
this._schemaPop();
|
|
}
|
|
}
|
|
} finally {
|
|
this._schemaPop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {unknown} value
|
|
*/
|
|
_validateAnyOf(schema, value) {
|
|
const subSchemas = schema.anyOf;
|
|
if (!Array.isArray(subSchemas)) { return; }
|
|
|
|
this._schemaPush(subSchemas, 'anyOf');
|
|
try {
|
|
for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
|
|
const subSchema = subSchemas[i];
|
|
this._schemaPush(subSchema, i);
|
|
try {
|
|
this._validate(subSchema, value);
|
|
return;
|
|
} catch (e) {
|
|
// NOP
|
|
} finally {
|
|
this._schemaPop();
|
|
}
|
|
}
|
|
|
|
throw this._createError('0 anyOf schemas matched');
|
|
} finally {
|
|
this._schemaPop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {unknown} value
|
|
*/
|
|
_validateOneOf(schema, value) {
|
|
const subSchemas = schema.oneOf;
|
|
if (!Array.isArray(subSchemas)) { return; }
|
|
|
|
this._schemaPush(subSchemas, 'oneOf');
|
|
try {
|
|
let count = 0;
|
|
for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
|
|
const subSchema = subSchemas[i];
|
|
this._schemaPush(subSchema, i);
|
|
try {
|
|
this._validate(subSchema, value);
|
|
++count;
|
|
} catch (e) {
|
|
// NOP
|
|
} finally {
|
|
this._schemaPop();
|
|
}
|
|
}
|
|
|
|
if (count !== 1) {
|
|
throw this._createError(`${count} oneOf schemas matched`);
|
|
}
|
|
} finally {
|
|
this._schemaPop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {unknown} value
|
|
* @throws {Error}
|
|
*/
|
|
_validateNot(schema, value) {
|
|
const notSchema = schema.not;
|
|
if (typeof notSchema === 'undefined') { return; }
|
|
|
|
if (Array.isArray(notSchema)) {
|
|
throw this._createError('not schema is an array');
|
|
}
|
|
|
|
this._schemaPush(notSchema, 'not');
|
|
try {
|
|
this._validate(notSchema, value);
|
|
} catch (e) {
|
|
return;
|
|
} finally {
|
|
this._schemaPop();
|
|
}
|
|
throw this._createError('not schema matched');
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {unknown} value
|
|
* @throws {Error}
|
|
*/
|
|
_validateSingleSchema(schema, value) {
|
|
const {type: schemaType, const: schemaConst, enum: schemaEnum} = schema;
|
|
const type = this._getValueType(value);
|
|
if (!this._isValueTypeAny(value, type, schemaType)) {
|
|
throw this._createError(`Value type ${type} does not match schema type ${Array.isArray(schemaType) ? schemaType.join(',') : schemaType}`);
|
|
}
|
|
|
|
if (typeof schemaConst !== 'undefined' && !this._valuesAreEqual(value, schemaConst)) {
|
|
throw this._createError('Invalid constant value');
|
|
}
|
|
|
|
if (Array.isArray(schemaEnum) && !this._valuesAreEqualAny(value, schemaEnum)) {
|
|
throw this._createError('Invalid enum value');
|
|
}
|
|
|
|
switch (type) {
|
|
case 'number':
|
|
this._validateNumber(schema, /** @type {number} */ (value));
|
|
break;
|
|
case 'string':
|
|
this._validateString(schema, /** @type {string} */ (value));
|
|
break;
|
|
case 'array':
|
|
this._validateArray(schema, /** @type {import('ext/json-schema').Value[]} */ (value));
|
|
break;
|
|
case 'object':
|
|
this._validateObject(schema, /** @type {import('ext/json-schema').ValueObject} */ (value));
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {number} value
|
|
* @throws {Error}
|
|
*/
|
|
_validateNumber(schema, value) {
|
|
const {multipleOf, minimum, exclusiveMinimum, maximum, exclusiveMaximum} = schema;
|
|
if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) {
|
|
throw this._createError(`Number is not a multiple of ${multipleOf}`);
|
|
}
|
|
|
|
if (typeof minimum === 'number' && value < minimum) {
|
|
throw this._createError(`Number is less than ${minimum}`);
|
|
}
|
|
|
|
if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) {
|
|
throw this._createError(`Number is less than or equal to ${exclusiveMinimum}`);
|
|
}
|
|
|
|
if (typeof maximum === 'number' && value > maximum) {
|
|
throw this._createError(`Number is greater than ${maximum}`);
|
|
}
|
|
|
|
if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) {
|
|
throw this._createError(`Number is greater than or equal to ${exclusiveMaximum}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {string} value
|
|
* @throws {Error}
|
|
*/
|
|
_validateString(schema, value) {
|
|
const {minLength, maxLength, pattern} = schema;
|
|
if (typeof minLength === 'number' && value.length < minLength) {
|
|
throw this._createError('String length too short');
|
|
}
|
|
|
|
if (typeof maxLength === 'number' && value.length > maxLength) {
|
|
throw this._createError('String length too long');
|
|
}
|
|
|
|
if (typeof pattern === 'string') {
|
|
let {patternFlags} = schema;
|
|
if (typeof patternFlags !== 'string') { patternFlags = ''; }
|
|
|
|
let regex;
|
|
try {
|
|
regex = this._getRegex(pattern, patternFlags);
|
|
} catch (e) {
|
|
throw this._createError(`Pattern is invalid (${e instanceof Error ? e.message : `${e}`})`);
|
|
}
|
|
|
|
if (!regex.test(value)) {
|
|
throw this._createError('Pattern match failed');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {unknown[]} value
|
|
* @throws {Error}
|
|
*/
|
|
_validateArray(schema, value) {
|
|
const {minItems, maxItems} = schema;
|
|
const {length} = value;
|
|
|
|
if (typeof minItems === 'number' && length < minItems) {
|
|
throw this._createError('Array length too short');
|
|
}
|
|
|
|
if (typeof maxItems === 'number' && length > maxItems) {
|
|
throw this._createError('Array length too long');
|
|
}
|
|
|
|
this._validateArrayContains(schema, value);
|
|
|
|
for (let i = 0; i < length; ++i) {
|
|
const {schema: itemSchema, stack} = this._getArrayItemSchemaInfo(schema, i);
|
|
if (itemSchema === false) {
|
|
throw this._createError(`No schema found for array[${i}]`);
|
|
}
|
|
|
|
const propertyValue = value[i];
|
|
|
|
this._schemaPushMultiple(stack);
|
|
this._valuePush(propertyValue, i);
|
|
try {
|
|
this._validate(itemSchema, propertyValue);
|
|
} finally {
|
|
this._valuePop();
|
|
this._schemaPopMultiple(stack.length);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {unknown[]} value
|
|
* @throws {Error}
|
|
*/
|
|
_validateArrayContains(schema, value) {
|
|
const containsSchema = schema.contains;
|
|
if (typeof containsSchema === 'undefined') { return; }
|
|
|
|
this._schemaPush(containsSchema, 'contains');
|
|
try {
|
|
for (let i = 0, ii = value.length; i < ii; ++i) {
|
|
const propertyValue = value[i];
|
|
this._valuePush(propertyValue, i);
|
|
try {
|
|
this._validate(containsSchema, propertyValue);
|
|
return;
|
|
} catch (e) {
|
|
// NOP
|
|
} finally {
|
|
this._valuePop();
|
|
}
|
|
}
|
|
throw this._createError('contains schema didn\'t match');
|
|
} finally {
|
|
this._schemaPop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {import('ext/json-schema').ValueObject} value
|
|
* @throws {Error}
|
|
*/
|
|
_validateObject(schema, value) {
|
|
const {required, minProperties, maxProperties} = schema;
|
|
const properties = Object.getOwnPropertyNames(value);
|
|
const {length} = properties;
|
|
|
|
if (Array.isArray(required)) {
|
|
for (const property of required) {
|
|
if (!Object.prototype.hasOwnProperty.call(value, property)) {
|
|
throw this._createError(`Missing property ${property}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof minProperties === 'number' && length < minProperties) {
|
|
throw this._createError('Not enough object properties');
|
|
}
|
|
|
|
if (typeof maxProperties === 'number' && length > maxProperties) {
|
|
throw this._createError('Too many object properties');
|
|
}
|
|
|
|
for (let i = 0; i < length; ++i) {
|
|
const property = properties[i];
|
|
const {schema: propertySchema, stack} = this._getObjectPropertySchemaInfo(schema, property);
|
|
if (propertySchema === false) {
|
|
throw this._createError(`No schema found for ${property}`);
|
|
}
|
|
|
|
const propertyValue = value[property];
|
|
|
|
this._schemaPushMultiple(stack);
|
|
this._valuePush(propertyValue, property);
|
|
try {
|
|
this._validate(propertySchema, propertyValue);
|
|
} finally {
|
|
this._valuePop();
|
|
this._schemaPopMultiple(stack.length);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Creation
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Type|import('ext/json-schema').Type[]|undefined} type
|
|
* @returns {import('ext/json-schema').Value}
|
|
*/
|
|
_getDefaultTypeValue(type) {
|
|
if (Array.isArray(type)) { type = type[0]; }
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @returns {import('ext/json-schema').Value}
|
|
*/
|
|
_getDefaultSchemaValue(schema) {
|
|
const {type: schemaType, default: schemaDefault} = schema;
|
|
return (
|
|
typeof schemaDefault !== 'undefined' &&
|
|
this._isValueTypeAny(schemaDefault, this._getValueType(schemaDefault), schemaType) ?
|
|
JsonSchema.clone(schemaDefault) :
|
|
this._getDefaultTypeValue(schemaType)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Schema} schema
|
|
* @param {string|number|null} path
|
|
* @param {unknown} value
|
|
* @param {import('ext/json-schema').SchemaStackItem[]} stack
|
|
* @returns {import('ext/json-schema').Value}
|
|
*/
|
|
_getValidValueOrDefault(schema, path, value, stack) {
|
|
({schema, stack} = this._getResolvedSchemaInfo(schema, stack));
|
|
this._schemaPushMultiple(stack);
|
|
this._valuePush(value, path);
|
|
try {
|
|
return this._getValidValueOrDefaultInner(schema, value);
|
|
} finally {
|
|
this._valuePop();
|
|
this._schemaPopMultiple(stack.length);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').Schema} schema
|
|
* @param {unknown} value
|
|
* @returns {import('ext/json-schema').Value}
|
|
*/
|
|
_getValidValueOrDefaultInner(schema, value) {
|
|
let type = this._getValueType(value);
|
|
if (typeof schema === 'boolean') {
|
|
return type !== null ? /** @type {import('ext/json-schema').ValueObject} */ (value) : null;
|
|
}
|
|
if (typeof value === 'undefined' || !this._isValueTypeAny(value, type, schema.type)) {
|
|
value = this._getDefaultSchemaValue(schema);
|
|
type = this._getValueType(value);
|
|
}
|
|
|
|
switch (type) {
|
|
case 'object':
|
|
return this._populateObjectDefaults(schema, /** @type {import('ext/json-schema').ValueObject} */ (value));
|
|
case 'array':
|
|
return this._populateArrayDefaults(schema, /** @type {import('ext/json-schema').Value[]} */ (value));
|
|
default:
|
|
if (!this._isValidCurrent(schema, value)) {
|
|
const schemaDefault = this._getDefaultSchemaValue(schema);
|
|
if (this._isValidCurrent(schema, schemaDefault)) {
|
|
return schemaDefault;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return /** @type {import('ext/json-schema').ValueObject} */ (value);
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {import('ext/json-schema').ValueObject} value
|
|
* @returns {import('ext/json-schema').ValueObject}
|
|
*/
|
|
_populateObjectDefaults(schema, value) {
|
|
const properties = new Set(Object.getOwnPropertyNames(value));
|
|
|
|
const {required} = schema;
|
|
if (Array.isArray(required)) {
|
|
for (const property of required) {
|
|
properties.delete(property);
|
|
const {schema: propertySchema, stack} = this._getObjectPropertySchemaInfo(schema, property);
|
|
if (propertySchema === false) { continue; }
|
|
const propertyValue = Object.prototype.hasOwnProperty.call(value, property) ? value[property] : void 0;
|
|
value[property] = this._getValidValueOrDefault(propertySchema, property, propertyValue, stack);
|
|
}
|
|
}
|
|
|
|
for (const property of properties) {
|
|
const {schema: propertySchema, stack} = this._getObjectPropertySchemaInfo(schema, property);
|
|
if (propertySchema === false) {
|
|
Reflect.deleteProperty(value, property);
|
|
} else {
|
|
value[property] = this._getValidValueOrDefault(propertySchema, property, value[property], stack);
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').SchemaObject} schema
|
|
* @param {import('ext/json-schema').Value[]} value
|
|
* @returns {import('ext/json-schema').Value[]}
|
|
*/
|
|
_populateArrayDefaults(schema, value) {
|
|
for (let i = 0, ii = value.length; i < ii; ++i) {
|
|
const {schema: itemSchema, stack} = this._getArrayItemSchemaInfo(schema, i);
|
|
if (itemSchema === false) { continue; }
|
|
const propertyValue = value[i];
|
|
value[i] = this._getValidValueOrDefault(itemSchema, i, propertyValue, stack);
|
|
}
|
|
|
|
const {minItems, maxItems} = schema;
|
|
if (typeof minItems === 'number' && value.length < minItems) {
|
|
for (let i = value.length; i < minItems; ++i) {
|
|
const {schema: itemSchema, stack} = this._getArrayItemSchemaInfo(schema, i);
|
|
if (itemSchema === false) { break; }
|
|
const item = this._getValidValueOrDefault(itemSchema, i, void 0, stack);
|
|
value.push(item);
|
|
}
|
|
}
|
|
|
|
if (typeof maxItems === 'number' && value.length > maxItems) {
|
|
value.splice(maxItems, value.length - maxItems);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @implements {ProxyHandler<import('ext/json-schema').ValueObjectOrArray>}
|
|
*/
|
|
class JsonSchemaProxyHandler {
|
|
/**
|
|
* @param {JsonSchema} schemaValidator
|
|
*/
|
|
constructor(schemaValidator) {
|
|
/** @type {JsonSchema} */
|
|
this._schemaValidator = schemaValidator;
|
|
/** @type {RegExp} */
|
|
this._numberPattern = /^(?:0|[1-9]\d*)$/;
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').ValueObjectOrArray} target
|
|
* @returns {?import('core').UnknownObject}
|
|
*/
|
|
getPrototypeOf(target) {
|
|
return Object.getPrototypeOf(target);
|
|
}
|
|
|
|
/**
|
|
* @type {(target: import('ext/json-schema').ValueObjectOrArray, newPrototype: ?unknown) => boolean}
|
|
*/
|
|
setPrototypeOf() {
|
|
throw new Error('setPrototypeOf not supported');
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').ValueObjectOrArray} target
|
|
* @returns {boolean}
|
|
*/
|
|
isExtensible(target) {
|
|
return Object.isExtensible(target);
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').ValueObjectOrArray} target
|
|
* @returns {boolean}
|
|
*/
|
|
preventExtensions(target) {
|
|
Object.preventExtensions(target);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').ValueObjectOrArray} target
|
|
* @param {string|symbol} property
|
|
* @returns {PropertyDescriptor|undefined}
|
|
*/
|
|
getOwnPropertyDescriptor(target, property) {
|
|
return Object.getOwnPropertyDescriptor(target, property);
|
|
}
|
|
|
|
/**
|
|
* @type {(target: import('ext/json-schema').ValueObjectOrArray, property: string|symbol, attributes: PropertyDescriptor) => boolean}
|
|
*/
|
|
defineProperty() {
|
|
throw new Error('defineProperty not supported');
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').ValueObjectOrArray} target
|
|
* @param {string|symbol} property
|
|
* @returns {boolean}
|
|
*/
|
|
has(target, property) {
|
|
return property in target;
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').ValueObjectOrArray} target
|
|
* @param {string|symbol} property
|
|
* @param {import('core').SafeAny} _receiver
|
|
* @returns {import('core').SafeAny}
|
|
*/
|
|
get(target, property, _receiver) {
|
|
if (typeof property === 'symbol') { return /** @type {import('core').UnknownObject} */ (target)[property]; }
|
|
|
|
let propertySchema;
|
|
if (Array.isArray(target)) {
|
|
const index = this._getArrayIndex(property);
|
|
if (index === null) {
|
|
// Note: this does not currently wrap mutating functions like push, pop, shift, unshift, splice
|
|
return /** @type {import('core').SafeAny} */ (target)[property];
|
|
}
|
|
property = `${index}`;
|
|
propertySchema = this._schemaValidator.getArrayItemSchema(index);
|
|
} else {
|
|
propertySchema = this._schemaValidator.getObjectPropertySchema(property);
|
|
}
|
|
|
|
if (propertySchema === null) { return void 0; }
|
|
|
|
const value = /** @type {import('core').UnknownObject} */ (target)[property];
|
|
return value !== null && typeof value === 'object' ? propertySchema.createProxy(/** @type {import('ext/json-schema').Value} */ (value)) : value;
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').ValueObjectOrArray} target
|
|
* @param {string|number|symbol} property
|
|
* @param {unknown} value
|
|
* @returns {boolean}
|
|
* @throws {Error}
|
|
*/
|
|
set(target, property, value) {
|
|
if (typeof property === 'symbol') { throw new Error(`Cannot assign symbol property ${typeof property === 'symbol' ? '<symbol>' : property}`); }
|
|
|
|
let propertySchema;
|
|
if (Array.isArray(target)) {
|
|
const index = this._getArrayIndex(property);
|
|
if (index === null) {
|
|
/** @type {import('core').SafeAny} */ (target)[property] = value;
|
|
return true;
|
|
}
|
|
if (index > target.length) { throw new Error('Array index out of range'); }
|
|
property = index;
|
|
propertySchema = this._schemaValidator.getArrayItemSchema(property);
|
|
} else {
|
|
if (typeof property !== 'string') {
|
|
property = `${property}`;
|
|
}
|
|
propertySchema = this._schemaValidator.getObjectPropertySchema(property);
|
|
}
|
|
|
|
if (propertySchema === null) { throw new Error(`Property ${property} not supported`); }
|
|
|
|
value = JsonSchema.clone(value);
|
|
propertySchema.validate(value);
|
|
|
|
/** @type {import('core').UnknownObject} */ (target)[property] = value;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').ValueObjectOrArray} target
|
|
* @param {string|symbol} property
|
|
* @returns {boolean}
|
|
* @throws {Error}
|
|
*/
|
|
deleteProperty(target, property) {
|
|
const required = (
|
|
(typeof target === 'object' && target !== null) ?
|
|
(!Array.isArray(target) && typeof property === 'string' && this._schemaValidator.isObjectPropertyRequired(property)) :
|
|
true
|
|
);
|
|
if (required) {
|
|
throw new Error(`${typeof property === 'symbol' ? '<symbol>' : property} cannot be deleted`);
|
|
}
|
|
return Reflect.deleteProperty(target, property);
|
|
}
|
|
|
|
/**
|
|
* @param {import('ext/json-schema').ValueObjectOrArray} target
|
|
* @returns {ArrayLike<string|symbol>}
|
|
*/
|
|
ownKeys(target) {
|
|
return Reflect.ownKeys(target);
|
|
}
|
|
|
|
/**
|
|
* @type {(target: import('ext/json-schema').ValueObjectOrArray, thisArg: import('core').SafeAny, argArray: import('core').SafeAny[]) => import('core').SafeAny}
|
|
*/
|
|
apply() {
|
|
throw new Error('apply not supported');
|
|
}
|
|
|
|
/**
|
|
* @type {(target: import('ext/json-schema').ValueObjectOrArray, argArray: import('core').SafeAny[], newTarget: import('core').SafeFunction) => import('ext/json-schema').ValueObjectOrArray}
|
|
*/
|
|
construct() {
|
|
throw new Error('construct not supported');
|
|
}
|
|
|
|
// Private
|
|
|
|
/**
|
|
* @param {string|symbol|number} property
|
|
* @returns {?number}
|
|
*/
|
|
_getArrayIndex(property) {
|
|
switch (typeof property) {
|
|
case 'string':
|
|
if (this._numberPattern.test(property)) {
|
|
return Number.parseInt(property, 10);
|
|
}
|
|
break;
|
|
case 'number':
|
|
if (Math.floor(property) === property && property >= 0) {
|
|
return property;
|
|
}
|
|
break;
|
|
}
|
|
return null;
|
|
}
|
|
}
|