diff options
Diffstat (limited to 'ext/js/data/json-schema.js')
| -rw-r--r-- | ext/js/data/json-schema.js | 927 | 
1 files changed, 652 insertions, 275 deletions
diff --git a/ext/js/data/json-schema.js b/ext/js/data/json-schema.js index 93c8cd59..d63cfd1a 100644 --- a/ext/js/data/json-schema.js +++ b/ext/js/data/json-schema.js @@ -19,31 +19,70 @@  import {clone} from '../core.js';  import {CacheMap} from '../general/cache-map.js'; +export class JsonSchemaError extends Error { +    /** +     * @param {string} message +     * @param {import('json-schema').ValueStackItem[]} valueStack +     * @param {import('json-schema').SchemaStackItem[]} schemaStack +     */ +    constructor(message, valueStack, schemaStack) { +        super(message); +        /** @type {import('json-schema').ValueStackItem[]} */ +        this._valueStack = valueStack; +        /** @type {import('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('json-schema').Schema|import('json-schema').Schema[]|undefined} */ +    get schema() { return this._schemaStack.length > 0 ? this._schemaStack[this._schemaStack.length - 1].schema : void 0; } + +    /** @type {import('json-schema').ValueStackItem[]} */ +    get valueStack() { return this._valueStack; } + +    /** @type {import('json-schema').SchemaStackItem[]} */ +    get schemaStack() { return this._schemaStack; } +} +  export class JsonSchema { +    /** +     * @param {import('json-schema').Schema} schema +     * @param {import('json-schema').Schema} [rootSchema] +     */      constructor(schema, rootSchema) { -        this._schema = null; +        /** @type {import('json-schema').Schema} */          this._startSchema = schema; +        /** @type {import('json-schema').Schema} */          this._rootSchema = typeof rootSchema !== 'undefined' ? rootSchema : schema; +        /** @type {?CacheMap<string, RegExp>} */          this._regexCache = null; +        /** @type {?Map<string, {schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}>} */          this._refCache = null; +        /** @type {import('json-schema').ValueStackItem[]} */          this._valueStack = []; +        /** @type {import('json-schema').SchemaStackItem[]} */          this._schemaStack = []; +        /** @type {?(jsonSchema: JsonSchema) => void} */          this._progress = null; +        /** @type {number} */          this._progressCounter = 0; +        /** @type {number} */          this._progressInterval = 1; - -        this._schemaPush(null, null); -        this._valuePush(null, null);      } +    /** @type {import('json-schema').Schema} */      get schema() {          return this._startSchema;      } +    /** @type {import('json-schema').Schema} */      get rootSchema() {          return this._rootSchema;      } +    /** @type {?(jsonSchema: JsonSchema) => void} */      get progress() {          return this._progress;      } @@ -52,6 +91,7 @@ export class JsonSchema {          this._progress = value;      } +    /** @type {number} */      get progressInterval() {          return this._progressInterval;      } @@ -60,6 +100,10 @@ export class JsonSchema {          this._progressInterval = value;      } +    /** +     * @param {import('json-schema').Value} value +     * @returns {import('json-schema').Value} +     */      createProxy(value) {          return (              typeof value === 'object' && value !== null ? @@ -68,6 +112,10 @@ export class JsonSchema {          );      } +    /** +     * @param {unknown} value +     * @returns {boolean} +     */      isValid(value) {          try {              this.validate(value); @@ -77,123 +125,203 @@ export class JsonSchema {          }      } +    /** +     * @param {unknown} value +     */      validate(value) { -        this._schemaPush(this._startSchema, null); +        const schema = this._startSchema; +        this._schemaPush(schema, null);          this._valuePush(value, null);          try { -            this._validate(value); +            this._validate(schema, value);          } finally {              this._valuePop();              this._schemaPop();          }      } +    /** +     * @param {unknown} [value] +     * @returns {import('json-schema').Value} +     */      getValidValueOrDefault(value) { -        return this._getValidValueOrDefault(null, value, {schema: this._startSchema, path: null}); +        const schema = this._startSchema; +        return this._getValidValueOrDefault(schema, null, value, [{schema, path: null}]);      } +    /** +     * @param {string} property +     * @returns {?JsonSchema} +     */      getObjectPropertySchema(property) { -        const startSchemaInfo = this._getResolveSchemaInfo({schema: this._startSchema, path: null}); -        this._schemaPush(startSchemaInfo.schema, startSchemaInfo.path); +        const schema = this._startSchema; +        const {schema: schema2, stack} = this._getResolvedSchemaInfo(schema, [{schema, path: null}]); +        this._schemaPushMultiple(stack);          try { -            const schemaInfo = this._getObjectPropertySchemaInfo(property); -            return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null; +            const {schema: propertySchema} = this._getObjectPropertySchemaInfo(schema2, property); +            return propertySchema !== false ? new JsonSchema(propertySchema, this._rootSchema) : null;          } finally { -            this._schemaPop(); +            this._schemaPopMultiple(stack.length);          }      } +    /** +     * @param {number} index +     * @returns {?JsonSchema} +     */      getArrayItemSchema(index) { -        const startSchemaInfo = this._getResolveSchemaInfo({schema: this._startSchema, path: null}); -        this._schemaPush(startSchemaInfo.schema, startSchemaInfo.path); +        const schema = this._startSchema; +        const {schema: schema2, stack} = this._getResolvedSchemaInfo(schema, [{schema, path: null}]); +        this._schemaPushMultiple(stack);          try { -            const schemaInfo = this._getArrayItemSchemaInfo(index); -            return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null; +            const {schema: itemSchema} = this._getArrayItemSchemaInfo(schema2, index); +            return itemSchema !== false ? new JsonSchema(itemSchema, this._rootSchema) : null;          } finally { -            this._schemaPop(); +            this._schemaPopMultiple(stack.length);          }      } +    /** +     * @param {string} property +     * @returns {boolean} +     */      isObjectPropertyRequired(property) { -        const {required} = this._startSchema; +        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('json-schema').ValueStackItem[]} +     */      getValueStack() { -        const valueStack = []; -        for (let i = 1, ii = this._valueStack.length; i < ii; ++i) { -            const {value, path} = this._valueStack[i]; -            valueStack.push({value, path}); +        const result = []; +        for (const {value, path} of this._valueStack) { +            result.push({value, path});          } -        return valueStack; +        return result;      } +    /** +     * @returns {import('json-schema').SchemaStackItem[]} +     */      getSchemaStack() { -        const schemaStack = []; -        for (let i = 1, ii = this._schemaStack.length; i < ii; ++i) { -            const {schema, path} = this._schemaStack[i]; -            schemaStack.push({schema, path}); +        const result = []; +        for (const {schema, path} of this._schemaStack) { +            result.push({schema, path});          } -        return schemaStack; +        return result;      } +    /** +     * @returns {number} +     */      getValueStackLength() {          return this._valueStack.length - 1;      } +    /** +     * @param {number} index +     * @returns {import('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('json-schema').SchemaStackItem} +     */      getSchemaStackItem(index) {          const {schema, path} = this._schemaStack[index + 1];          return {schema, path};      } +    /** +     * @template T +     * @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('json-schema').Schema|import('json-schema').Schema[]} schema +     * @param {string|number|null} path +     */      _schemaPush(schema, path) {          this._schemaStack.push({schema, path}); -        this._schema = schema;      } +    /** +     * @param {import('json-schema').SchemaStackItem[]} items +     */ +    _schemaPushMultiple(items) { +        this._schemaStack.push(...items); +    } + +    /** +     * @returns {void} +     */      _schemaPop() {          this._schemaStack.pop(); -        this._schema = this._schemaStack[this._schemaStack.length - 1].schema; +    } + +    /** +     * @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(); -        const error = new Error(message); -        error.value = valueStack[valueStack.length - 1].value; -        error.schema = schemaStack[schemaStack.length - 1].schema; -        error.valueStack = valueStack; -        error.schemaStack = schemaStack; -        return error; -    } - -    _isObject(value) { -        return typeof value === 'object' && value !== null && !Array.isArray(value); +        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); @@ -208,81 +336,125 @@ export class JsonSchema {          return regex;      } -    _getUnconstrainedSchema() { -        return {}; -    } - -    _getObjectPropertySchemaInfo(property) { -        const {properties} = this._schema; -        if (this._isObject(properties)) { +    /** +     * @param {import('json-schema').Schema} schema +     * @param {string} property +     * @returns {{schema: import('json-schema').Schema, stack: import('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 (this._isObject(propertySchema)) { -                return {schema: propertySchema, path: ['properties', property]}; +            if (typeof propertySchema !== 'undefined') { +                return { +                    schema: propertySchema, +                    stack: [ +                        {schema: properties, path: 'properties'}, +                        {schema: propertySchema, path: property} +                    ] +                };              }          } - -        const {additionalProperties} = this._schema; -        if (additionalProperties === false) { -            return null; -        } else if (this._isObject(additionalProperties)) { -            return {schema: additionalProperties, path: 'additionalProperties'}; -        } else { -            const result = this._getUnconstrainedSchema(); -            return {schema: result, path: null}; -        } -    } - -    _getArrayItemSchemaInfo(index) { -        const {items} = this._schema; -        if (this._isObject(items)) { -            return {schema: items, path: 'items'}; -        } -        if (Array.isArray(items)) { -            if (index >= 0 && index < items.length) { -                const propertySchema = items[index]; -                if (this._isObject(propertySchema)) { -                    return {schema: propertySchema, path: ['items', index]}; +        return this._getOptionalSchemaInfo(schema.additionalProperties, 'additionalProperties'); +    } + +    /** +     * @param {import('json-schema').Schema} schema +     * @param {number} index +     * @returns {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}} +     */ +    _getArrayItemSchemaInfo(schema, index) { +        if (typeof schema === 'boolean') { +            return {schema, stack: [{schema, path: null}]}; +        } +        const {prefixItems} = schema; +        if (typeof prefixItems !== 'undefined') { +            if (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 {additionalItems} = this._schema; -        if (additionalItems === false) { -            return null; -        } else if (this._isObject(additionalItems)) { -            return {schema: additionalItems, path: 'additionalItems'}; -        } else { -            const result = this._getUnconstrainedSchema(); -            return {schema: result, path: null}; -        } -    } - -    _getSchemaOrValueType(value) { -        const {type} = this._schema; - -        if (Array.isArray(type)) { -            if (typeof value !== 'undefined') { -                const valueType = this._getValueType(value); -                if (type.indexOf(valueType) >= 0) { -                    return valueType; +        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 null;          } +        return this._getOptionalSchemaInfo(schema.additionalItems, 'additionalItems'); +    } -        if (typeof type !== 'undefined') { return type; } -        return (typeof value !== 'undefined') ? this._getValueType(value) : null; +    /** +     * @param {import('json-schema').Schema|undefined} schema +     * @param {string|number|null} path +     * @returns {{schema: import('json-schema').Schema, stack: import('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('json-schema').Type} +     * @throws {Error} +     */      _getValueType(value) {          const type = typeof value; -        if (type === 'object') { -            if (value === null) { return 'null'; } -            if (Array.isArray(value)) { return 'array'; } +        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;          } -        return type;      } +    /** +     * @param {unknown} value +     * @param {?import('json-schema').Type} type +     * @param {import('json-schema').Type|import('json-schema').Type[]|undefined} schemaTypes +     * @returns {boolean} +     */      _isValueTypeAny(value, type, schemaTypes) {          if (typeof schemaTypes === 'string') {              return this._isValueType(value, type, schemaTypes); @@ -297,13 +469,24 @@ export class JsonSchema {          return true;      } +    /** +     * @param {unknown} value +     * @param {?import('json-schema').Type} type +     * @param {import('json-schema').Type} schemaType +     * @returns {boolean} +     */      _isValueType(value, type, schemaType) {          return (              type === schemaType || -            (schemaType === 'integer' && Math.floor(value) === value) +            (schemaType === 'integer' && typeof value === 'number' && Math.floor(value) === value)          );      } +    /** +     * @param {unknown} value1 +     * @param {import('json-schema').Value[]} valueList +     * @returns {boolean} +     */      _valuesAreEqualAny(value1, valueList) {          for (const value2 of valueList) {              if (this._valuesAreEqual(value1, value2)) { @@ -313,29 +496,45 @@ export class JsonSchema {          return false;      } +    /** +     * @param {unknown} value1 +     * @param {import('json-schema').Value} value2 +     * @returns {boolean} +     */      _valuesAreEqual(value1, value2) {          return value1 === value2;      } -    _getResolveSchemaInfo(schemaInfo) { -        const ref = schemaInfo.schema.$ref; -        if (typeof ref !== 'string') { return schemaInfo; } - -        const {path: basePath} = schemaInfo; -        const {schema, path} = this._getReference(ref); -        if (Array.isArray(basePath)) { -            path.unshift(...basePath); -        } else { -            path.unshift(basePath); +    /** +     * @param {import('json-schema').Schema} schema +     * @param {import('json-schema').SchemaStackItem[]} stack +     * @returns {{schema: import('json-schema').Schema, stack: import('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, path}; +        return {schema, stack};      } +    /** +     * @param {string} ref +     * @returns {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}} +     * @throws {Error} +     */      _getReference(ref) {          if (!ref.startsWith('#/')) {              throw this._createError(`Unsupported reference path: ${ref}`);          } +        /** @type {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}|undefined} */          let info;          if (this._refCache !== null) {              info = this._refCache.get(ref); @@ -348,12 +547,20 @@ export class JsonSchema {              this._refCache.set(ref, info);          } -        return {schema: info.schema, path: [...info.path]}; +        info.stack = this._copySchemaStack(info.stack); +        return info;      } +    /** +     * @param {string} ref +     * @returns {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}} +     * @throws {Error} +     */      _getReferenceUncached(ref) { +        /** @type {Set<string>} */          const visited = new Set(); -        const path = []; +        /** @type {import('json-schema').SchemaStackItem[]} */ +        const stack = [];          while (true) {              if (visited.has(ref)) {                  throw this._createError(`Recursive reference: ${ref}`); @@ -362,106 +569,139 @@ export class JsonSchema {              const pathParts = ref.substring(2).split('/');              let schema = this._rootSchema; -            try { -                for (const pathPart of pathParts) { -                    schema = schema[pathPart]; +            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}`);                  } -            } catch (e) { -                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 (!this._isObject(schema)) { +            if (Array.isArray(schema)) {                  throw this._createError(`Invalid reference: ${ref}`);              } -            path.push(null, ...pathParts); - -            ref = schema.$ref; -            if (typeof ref !== 'string') { -                return {schema, path}; +            const refNext = typeof schema === 'object' && schema !== null ? schema.$ref : void 0; +            if (typeof refNext !== 'string') { +                return {schema, stack};              } +            ref = refNext; +        } +    } + +    /** +     * @param {import('json-schema').SchemaStackItem[]} schemaStack +     * @returns {import('json-schema').SchemaStackItem[]} +     */ +    _copySchemaStack(schemaStack) { +        /** @type {import('json-schema').SchemaStackItem[]} */ +        const results = []; +        for (const {schema, path} of schemaStack) { +            results.push({schema, path});          } +        return results;      }      // Validation -    _isValidCurrent(value) { +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown} value +     * @returns {boolean} +     */ +    _isValidCurrent(schema, value) {          try { -            this._validate(value); +            this._validate(schema, value);              return true;          } catch (e) {              return false;          }      } -    _validate(value) { +    /** +     * @param {import('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 ref = this._schema.$ref; -        const schemaInfo = (typeof ref === 'string') ? this._getReference(ref) : null; - -        if (schemaInfo === null) { -            this._validateInner(value); -        } else { -            this._schemaPush(schemaInfo.schema, schemaInfo.path); -            try { -                this._validateInner(value); -            } finally { -                this._schemaPop(); -            } -        } -    } - -    _validateInner(value) { -        this._validateSingleSchema(value); -        this._validateConditional(value); -        this._validateAllOf(value); -        this._validateAnyOf(value); -        this._validateOneOf(value); -        this._validateNoneOf(value); -    } - -    _validateConditional(value) { -        const ifSchema = this._schema.if; -        if (!this._isObject(ifSchema)) { return; } +        const {schema: schema2, stack} = this._getResolvedSchemaInfo(schema, []); +        this._schemaPushMultiple(stack); +        try { +            this._validateInner(schema2, value); +        } finally { +            this._schemaPopMultiple(stack.length); +        } +    } + +    /** +     * @param {import('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('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(value); +            this._validate(ifSchema, value);          } catch (e) {              okay = false;          } finally {              this._schemaPop();          } -        const nextSchema = okay ? this._schema.then : this._schema.else; -        if (this._isObject(nextSchema)) { return; } +        const nextSchema = okay ? schema.then : schema.else; +        if (typeof nextSchema === 'undefined') { return; }          this._schemaPush(nextSchema, okay ? 'then' : 'else');          try { -            this._validate(value); +            this._validate(nextSchema, value);          } finally {              this._schemaPop();          }      } -    _validateAllOf(value) { -        const subSchemas = this._schema.allOf; +    /** +     * @param {import('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]; -                if (!this._isObject(subSchema)) { continue; } -                  this._schemaPush(subSchema, i);                  try { -                    this._validate(value); +                    this._validate(subSchema, value);                  } finally {                      this._schemaPop();                  } @@ -471,19 +711,21 @@ export class JsonSchema {          }      } -    _validateAnyOf(value) { -        const subSchemas = this._schema.anyOf; +    /** +     * @param {import('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]; -                if (!this._isObject(subSchema)) { continue; } -                  this._schemaPush(subSchema, i);                  try { -                    this._validate(value); +                    this._validate(subSchema, value);                      return;                  } catch (e) {                      // NOP @@ -498,8 +740,12 @@ export class JsonSchema {          }      } -    _validateOneOf(value) { -        const subSchemas = this._schema.oneOf; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown} value +     */ +    _validateOneOf(schema, value) { +        const subSchemas = schema.oneOf;          if (!Array.isArray(subSchemas)) { return; }          this._schemaPush(subSchemas, 'oneOf'); @@ -507,11 +753,9 @@ export class JsonSchema {              let count = 0;              for (let i = 0, ii = subSchemas.length; i < ii; ++i) {                  const subSchema = subSchemas[i]; -                if (!this._isObject(subSchema)) { continue; } -                  this._schemaPush(subSchema, i);                  try { -                    this._validate(value); +                    this._validate(subSchema, value);                      ++count;                  } catch (e) {                      // NOP @@ -528,33 +772,37 @@ export class JsonSchema {          }      } -    _validateNoneOf(value) { -        const subSchemas = this._schema.not; -        if (!Array.isArray(subSchemas)) { return; } +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown} value +     * @throws {Error} +     */ +    _validateNot(schema, value) { +        const notSchema = schema.not; +        if (typeof notSchema === 'undefined') { return; } -        this._schemaPush(subSchemas, 'not'); -        try { -            for (let i = 0, ii = subSchemas.length; i < ii; ++i) { -                const subSchema = subSchemas[i]; -                if (!this._isObject(subSchema)) { continue; } +        if (Array.isArray(notSchema)) { +            throw this._createError('not schema is an array'); +        } -                this._schemaPush(subSchema, i); -                try { -                    this._validate(value); -                } catch (e) { -                    continue; -                } finally { -                    this._schemaPop(); -                } -                throw this._createError(`not[${i}] schema matched`); -            } +        this._schemaPush(notSchema, 'not'); +        try { +            this._validate(notSchema, value); +        } catch (e) { +            return;          } finally {              this._schemaPop();          } +        throw this._createError('not schema matched');      } -    _validateSingleSchema(value) { -        const {type: schemaType, const: schemaConst, enum: schemaEnum} = this._schema; +    /** +     * @param {import('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 ${schemaType}`); @@ -570,22 +818,27 @@ export class JsonSchema {          switch (type) {              case 'number': -                this._validateNumber(value); +                this._validateNumber(schema, /** @type {number} */ (value));                  break;              case 'string': -                this._validateString(value); +                this._validateString(schema, /** @type {string} */ (value));                  break;              case 'array': -                this._validateArray(value); +                this._validateArray(schema, /** @type {import('json-schema').Value[]} */ (value));                  break;              case 'object': -                this._validateObject(value); +                this._validateObject(schema, /** @type {import('json-schema').ValueObject} */ (value));                  break;          }      } -    _validateNumber(value) { -        const {multipleOf, minimum, exclusiveMinimum, maximum, exclusiveMaximum} = this._schema; +    /** +     * @param {import('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}`);          } @@ -607,8 +860,13 @@ export class JsonSchema {          }      } -    _validateString(value) { -        const {minLength, maxLength, pattern} = this._schema; +    /** +     * @param {import('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');          } @@ -618,14 +876,14 @@ export class JsonSchema {          }          if (typeof pattern === 'string') { -            let {patternFlags} = this._schema; +            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.message})`); +                throw this._createError(`Pattern is invalid (${e instanceof Error ? e.message : `${e}`})`);              }              if (!regex.test(value)) { @@ -634,8 +892,13 @@ export class JsonSchema {          }      } -    _validateArray(value) { -        const {minItems, maxItems} = this._schema; +    /** +     * @param {import('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) { @@ -646,30 +909,35 @@ export class JsonSchema {              throw this._createError('Array length too long');          } -        this._validateArrayContains(value); +        this._validateArrayContains(schema, value);          for (let i = 0; i < length; ++i) { -            const schemaInfo = this._getArrayItemSchemaInfo(i); -            if (schemaInfo === null) { +            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._schemaPush(schemaInfo.schema, schemaInfo.path); +            this._schemaPushMultiple(stack);              this._valuePush(propertyValue, i);              try { -                this._validate(propertyValue); +                this._validate(itemSchema, propertyValue);              } finally {                  this._valuePop(); -                this._schemaPop(); +                this._schemaPopMultiple(stack.length);              }          }      } -    _validateArrayContains(value) { -        const containsSchema = this._schema.contains; -        if (!this._isObject(containsSchema)) { return; } +    /** +     * @param {import('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 { @@ -677,7 +945,7 @@ export class JsonSchema {                  const propertyValue = value[i];                  this._valuePush(propertyValue, i);                  try { -                    this._validate(propertyValue); +                    this._validate(containsSchema, propertyValue);                      return;                  } catch (e) {                      // NOP @@ -691,8 +959,13 @@ export class JsonSchema {          }      } -    _validateObject(value) { -        const {required, minProperties, maxProperties} = this._schema; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {import('json-schema').ValueObject} value +     * @throws {Error} +     */ +    _validateObject(schema, value) { +        const {required, minProperties, maxProperties} = schema;          const properties = Object.getOwnPropertyNames(value);          const {length} = properties; @@ -714,27 +987,32 @@ export class JsonSchema {          for (let i = 0; i < length; ++i) {              const property = properties[i]; -            const schemaInfo = this._getObjectPropertySchemaInfo(property); -            if (schemaInfo === null) { +            const {schema: propertySchema, stack} = this._getObjectPropertySchemaInfo(schema, property); +            if (propertySchema === false) {                  throw this._createError(`No schema found for ${property}`);              }              const propertyValue = value[property]; -            this._schemaPush(schemaInfo.schema, schemaInfo.path); +            this._schemaPushMultiple(stack);              this._valuePush(propertyValue, property);              try { -                this._validate(propertyValue); +                this._validate(propertySchema, propertyValue);              } finally {                  this._valuePop(); -                this._schemaPop(); +                this._schemaPopMultiple(stack.length);              }          }      }      // Creation +    /** +     * @param {import('json-schema').Type|import('json-schema').Type[]|undefined} type +     * @returns {import('json-schema').Value} +     */      _getDefaultTypeValue(type) { +        if (Array.isArray(type)) { type = type[0]; }          if (typeof type === 'string') {              switch (type) {                  case 'null': @@ -755,95 +1033,122 @@ export class JsonSchema {          return null;      } -    _getDefaultSchemaValue() { -        const {type: schemaType, default: schemaDefault} = this._schema; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @returns {import('json-schema').Value} +     */ +    _getDefaultSchemaValue(schema) { +        const {type: schemaType, default: schemaDefault} = schema;          return (              typeof schemaDefault !== 'undefined' &&              this._isValueTypeAny(schemaDefault, this._getValueType(schemaDefault), schemaType) ? -            clone(schemaDefault) : +            JsonSchema.clone(schemaDefault) :              this._getDefaultTypeValue(schemaType)          );      } -    _getValidValueOrDefault(path, value, schemaInfo) { -        schemaInfo = this._getResolveSchemaInfo(schemaInfo); -        this._schemaPush(schemaInfo.schema, schemaInfo.path); +    /** +     * @param {import('json-schema').Schema} schema +     * @param {string|number|null} path +     * @param {unknown} value +     * @param {import('json-schema').SchemaStackItem[]} stack +     * @returns {import('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(value); +            return this._getValidValueOrDefaultInner(schema, value);          } finally {              this._valuePop(); -            this._schemaPop(); +            this._schemaPopMultiple(stack.length);          }      } -    _getValidValueOrDefaultInner(value) { +    /** +     * @param {import('json-schema').Schema} schema +     * @param {unknown} value +     * @returns {import('json-schema').Value} +     */ +    _getValidValueOrDefaultInner(schema, value) {          let type = this._getValueType(value); -        if (typeof value === 'undefined' || !this._isValueTypeAny(value, type, this._schema.type)) { -            value = this._getDefaultSchemaValue(); +        if (typeof schema === 'boolean') { +            return type !== null ? /** @type {import('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': -                value = this._populateObjectDefaults(value); -                break; +                return this._populateObjectDefaults(schema, /** @type {import('json-schema').ValueObject} */ (value));              case 'array': -                value = this._populateArrayDefaults(value); -                break; +                return this._populateArrayDefaults(schema, /** @type {import('json-schema').Value[]} */ (value));              default: -                if (!this._isValidCurrent(value)) { -                    const schemaDefault = this._getDefaultSchemaValue(); -                    if (this._isValidCurrent(schemaDefault)) { -                        value = schemaDefault; +                if (!this._isValidCurrent(schema, value)) { +                    const schemaDefault = this._getDefaultSchemaValue(schema); +                    if (this._isValidCurrent(schema, schemaDefault)) { +                        return schemaDefault;                      }                  }                  break;          } -        return value; +        return /** @type {import('json-schema').ValueObject} */ (value);      } -    _populateObjectDefaults(value) { +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {import('json-schema').ValueObject} value +     * @returns {import('json-schema').ValueObject} +     */ +    _populateObjectDefaults(schema, value) {          const properties = new Set(Object.getOwnPropertyNames(value)); -        const {required} = this._schema; +        const {required} = schema;          if (Array.isArray(required)) {              for (const property of required) {                  properties.delete(property); -                const schemaInfo = this._getObjectPropertySchemaInfo(property); -                if (schemaInfo === null) { continue; } +                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(property, propertyValue, schemaInfo); +                value[property] = this._getValidValueOrDefault(propertySchema, property, propertyValue, stack);              }          }          for (const property of properties) { -            const schemaInfo = this._getObjectPropertySchemaInfo(property); -            if (schemaInfo === null) { +            const {schema: propertySchema, stack} = this._getObjectPropertySchemaInfo(schema, property); +            if (propertySchema === false) {                  Reflect.deleteProperty(value, property);              } else { -                value[property] = this._getValidValueOrDefault(property, value[property], schemaInfo); +                value[property] = this._getValidValueOrDefault(propertySchema, property, value[property], stack);              }          }          return value;      } -    _populateArrayDefaults(value) { +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {import('json-schema').Value[]} value +     * @returns {import('json-schema').Value[]} +     */ +    _populateArrayDefaults(schema, value) {          for (let i = 0, ii = value.length; i < ii; ++i) { -            const schemaInfo = this._getArrayItemSchemaInfo(i); -            if (schemaInfo === null) { continue; } +            const {schema: itemSchema, stack} = this._getArrayItemSchemaInfo(schema, i); +            if (itemSchema === false) { continue; }              const propertyValue = value[i]; -            value[i] = this._getValidValueOrDefault(i, propertyValue, schemaInfo); +            value[i] = this._getValidValueOrDefault(itemSchema, i, propertyValue, stack);          } -        const {minItems, maxItems} = this._schema; +        const {minItems, maxItems} = schema;          if (typeof minItems === 'number' && value.length < minItems) {              for (let i = value.length; i < minItems; ++i) { -                const schemaInfo = this._getArrayItemSchemaInfo(i); -                if (schemaInfo === null) { break; } -                const item = this._getValidValueOrDefault(i, void 0, schemaInfo); +                const {schema: itemSchema, stack} = this._getArrayItemSchemaInfo(schema, i); +                if (itemSchema === false) { break; } +                const item = this._getValidValueOrDefault(itemSchema, i, void 0, stack);                  value.push(item);              }          } @@ -856,115 +1161,187 @@ export class JsonSchema {      }  } +/** + * @implements {ProxyHandler<import('json-schema').ValueObjectOrArray>} + */  class JsonSchemaProxyHandler { -    constructor(schema) { -        this._schema = schema; +    /** +     * @param {JsonSchema} schemaValidator +     */ +    constructor(schemaValidator) { +        /** @type {JsonSchema} */ +        this._schemaValidator = schemaValidator; +        /** @type {RegExp} */          this._numberPattern = /^(?:0|[1-9]\d*)$/;      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @returns {?import('core').UnknownObject} +     */      getPrototypeOf(target) {          return Object.getPrototypeOf(target);      } +    /** +     * @type {(target: import('json-schema').ValueObjectOrArray, newPrototype: ?unknown) => boolean} +     */      setPrototypeOf() {          throw new Error('setPrototypeOf not supported');      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @returns {boolean} +     */      isExtensible(target) {          return Object.isExtensible(target);      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @returns {boolean} +     */      preventExtensions(target) {          Object.preventExtensions(target);          return true;      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @param {string|symbol} property +     * @returns {PropertyDescriptor|undefined} +     */      getOwnPropertyDescriptor(target, property) {          return Object.getOwnPropertyDescriptor(target, property);      } +    /** +     * @type {(target: import('json-schema').ValueObjectOrArray, property: string|symbol, attributes: PropertyDescriptor) => boolean} +     */      defineProperty() {          throw new Error('defineProperty not supported');      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @param {string|symbol} property +     * @returns {boolean} +     */      has(target, property) {          return property in target;      } -    get(target, property) { -        if (typeof property === 'symbol') { return target[property]; } +    /** +     * @param {import('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 target[property]; +                return /** @type {import('core').SafeAny} */ (target)[property];              } -            property = index; -            propertySchema = this._schema.getArrayItemSchema(property); +            property = `${index}`; +            propertySchema = this._schemaValidator.getArrayItemSchema(index);          } else { -            propertySchema = this._schema.getObjectPropertySchema(property); +            propertySchema = this._schemaValidator.getObjectPropertySchema(property);          }          if (propertySchema === null) { return void 0; } -        const value = target[property]; -        return value !== null && typeof value === 'object' ? propertySchema.createProxy(value) : value; +        const value = /** @type {import('core').UnknownObject} */ (target)[property]; +        return value !== null && typeof value === 'object' ? propertySchema.createProxy(/** @type {import('json-schema').Value} */ (value)) : value;      } +    /** +     * @param {import('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 ${property}`); } +        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) { -                target[property] = value; +                /** @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._schema.getArrayItemSchema(property); +            propertySchema = this._schemaValidator.getArrayItemSchema(property);          } else { -            propertySchema = this._schema.getObjectPropertySchema(property); +            if (typeof property !== 'string') { +                property = `${property}`; +            } +            propertySchema = this._schemaValidator.getObjectPropertySchema(property);          }          if (propertySchema === null) { throw new Error(`Property ${property} not supported`); } -        value = clone(value); +        value = JsonSchema.clone(value);          propertySchema.validate(value); -        target[property] = value; +        /** @type {import('core').UnknownObject} */ (target)[property] = value;          return true;      } +    /** +     * @param {import('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) && this._schema.isObjectPropertyRequired(property)) : +            (!Array.isArray(target) && typeof property === 'string' && this._schemaValidator.isObjectPropertyRequired(property)) :              true          );          if (required) { -            throw new Error(`${property} cannot be deleted`); +            throw new Error(`${typeof property === 'symbol' ? '<symbol>' : property} cannot be deleted`);          }          return Reflect.deleteProperty(target, property);      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @returns {ArrayLike<string|symbol>} +     */      ownKeys(target) {          return Reflect.ownKeys(target);      } +    /** +     * @type {(target: import('json-schema').ValueObjectOrArray, thisArg: import('core').SafeAny, argArray: import('core').SafeAny[]) => import('core').SafeAny} +     */      apply() {          throw new Error('apply not supported');      } +    /** +     * @type {(target: import('json-schema').ValueObjectOrArray, argArray: import('core').SafeAny[], newTarget: import('core').SafeFunction) => import('json-schema').ValueObjectOrArray} +     */      construct() {          throw new Error('construct not supported');      }      // Private +    /** +     * @param {string|symbol|number} property +     * @returns {?number} +     */      _getArrayIndex(property) {          if (typeof property === 'string' && this._numberPattern.test(property)) {              return Number.parseInt(property, 10);  |