/* * Copyright (C) 2023-2024 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 {import('core').SafeAny} 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; } }