diff options
| -rw-r--r-- | ext/js/data/json-schema.js | 89 | ||||
| -rw-r--r-- | test/test-json-schema.js | 178 | 
2 files changed, 265 insertions, 2 deletions
| diff --git a/ext/js/data/json-schema.js b/ext/js/data/json-schema.js index bd6d9022..a6306c3a 100644 --- a/ext/js/data/json-schema.js +++ b/ext/js/data/json-schema.js @@ -25,6 +25,7 @@ class JsonSchema {          this._startSchema = schema;          this._rootSchema = typeof rootSchema !== 'undefined' ? rootSchema : schema;          this._regexCache = null; +        this._refCache = null;          this._valueStack = [];          this._schemaStack = []; @@ -73,7 +74,8 @@ class JsonSchema {      }      getObjectPropertySchema(property) { -        this._schemaPush(this._startSchema, null); +        const startSchemaInfo = this._getResolveSchemaInfo({schema: this._startSchema, path: null}); +        this._schemaPush(startSchemaInfo.schema, startSchemaInfo.path);          try {              const schemaInfo = this._getObjectPropertySchemaInfo(property);              return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null; @@ -83,7 +85,8 @@ class JsonSchema {      }      getArrayItemSchema(index) { -        this._schemaPush(this._startSchema, null); +        const startSchemaInfo = this._getResolveSchemaInfo({schema: this._startSchema, path: null}); +        this._schemaPush(startSchemaInfo.schema, startSchemaInfo.path);          try {              const schemaInfo = this._getArrayItemSchemaInfo(index);              return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null; @@ -267,6 +270,71 @@ class JsonSchema {          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); +        } +        return {schema, path}; +    } + +    _getReference(ref) { +        if (!ref.startsWith('#/')) { +            throw this._createError(`Unsupported reference path: ${ref}`); +        } + +        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); +        } + +        return {schema: info.schema, path: [...info.path]}; +    } + +    _getReferenceUncached(ref) { +        const visited = new Set(); +        const path = []; +        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; +            try { +                for (const pathPart of pathParts) { +                    schema = schema[pathPart]; +                } +            } catch (e) { +                throw this._createError(`Invalid reference: ${ref}`); +            } +            if (!this._isObject(schema)) { +                throw this._createError(`Invalid reference: ${ref}`); +            } + +            path.push(null, ...pathParts); + +            ref = schema.$ref; +            if (typeof ref !== 'string') { +                return {schema, path}; +            } +        } +    } +      // Validation      _isValidCurrent(value) { @@ -279,6 +347,22 @@ class JsonSchema {      }      _validate(value) { +        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); @@ -638,6 +722,7 @@ class JsonSchema {      }      _getValidValueOrDefault(path, value, schemaInfo) { +        schemaInfo = this._getResolveSchemaInfo(schemaInfo);          this._schemaPush(schemaInfo.schema, schemaInfo.path);          this._valuePush(value, path);          try { diff --git a/test/test-json-schema.js b/test/test-json-schema.js index ba0131bf..c5ff830a 100644 --- a/test/test-json-schema.js +++ b/test/test-json-schema.js @@ -413,6 +413,80 @@ function testValidate2() {                  {expected: false, value: []},                  {expected: false, value: {}}              ] +        }, + +        // Reference tests +        { +            schema: { +                definitions: { +                    example: { +                        type: 'number' +                    } +                }, +                $ref: '#/definitions/example' +            }, +            inputs: [ +                {expected: true,  value: 0}, +                {expected: true,  value: 0.5}, +                {expected: true,  value: 1}, +                {expected: false, value: '0'}, +                {expected: false, value: null}, +                {expected: false, value: []}, +                {expected: false, value: {}} +            ] +        }, +        { +            schema: { +                definitions: { +                    example: { +                        type: 'integer' +                    } +                }, +                $ref: '#/definitions/example' +            }, +            inputs: [ +                {expected: true,  value: 0}, +                {expected: false, value: 0.5}, +                {expected: true,  value: 1}, +                {expected: false, value: '0'}, +                {expected: false, value: null}, +                {expected: false, value: []}, +                {expected: false, value: {}} +            ] +        }, +        { +            schema: { +                definitions: { +                    example: { +                        type: 'object', +                        additionalProperties: false, +                        properties: { +                            test: { +                                $ref: '#/definitions/example' +                            } +                        } +                    } +                }, +                $ref: '#/definitions/example' +            }, +            inputs: [ +                {expected: false, value: 0}, +                {expected: false, value: 0.5}, +                {expected: false, value: 1}, +                {expected: false, value: '0'}, +                {expected: false, value: null}, +                {expected: false, value: []}, +                {expected: true,  value: {}}, +                {expected: false, value: {test: 0}}, +                {expected: false, value: {test: 0.5}}, +                {expected: false, value: {test: 1}}, +                {expected: false, value: {test: '0'}}, +                {expected: false, value: {test: null}}, +                {expected: false, value: {test: []}}, +                {expected: true,  value: {test: {}}}, +                {expected: true,  value: {test: {test: {}}}}, +                {expected: true,  value: {test: {test: {test: {}}}}} +            ]          }      ]; @@ -690,6 +764,83 @@ function testGetValidValueOrDefault1() {                      {test: -1}                  ]              ] +        }, + +        // Test references +        { +            schema: { +                definitions: { +                    example: { +                        type: 'number', +                        default: 0 +                    } +                }, +                $ref: '#/definitions/example' +            }, +            inputs: [ +                [ +                    1, +                    1 +                ], +                [ +                    null, +                    0 +                ], +                [ +                    'test', +                    0 +                ], +                [ +                    {test: 'value'}, +                    0 +                ] +            ] +        }, +        { +            schema: { +                definitions: { +                    example: { +                        type: 'object', +                        additionalProperties: false, +                        properties: { +                            test: { +                                $ref: '#/definitions/example' +                            } +                        } +                    } +                }, +                $ref: '#/definitions/example' +            }, +            inputs: [ +                [ +                    1, +                    {} +                ], +                [ +                    null, +                    {} +                ], +                [ +                    'test', +                    {} +                ], +                [ +                    {}, +                    {} +                ], +                [ +                    {test: {}}, +                    {test: {}} +                ], +                [ +                    {test: 'value'}, +                    {test: {}} +                ], +                [ +                    {test: {test: {}}}, +                    {test: {test: {}}} +                ] +            ]          }      ]; @@ -797,6 +948,33 @@ function testProxy1() {                  {error: true,  value: ['default'], action: (value) => { delete value[0]; }},                  {error: false, value: ['default'], action: (value) => { value[1] = 'string'; }}              ] +        }, + +        // Reference tests +        { +            schema: { +                definitions: { +                    example: { +                        type: 'object', +                        additionalProperties: false, +                        properties: { +                            test: { +                                $ref: '#/definitions/example' +                            } +                        } +                    } +                }, +                $ref: '#/definitions/example' +            }, +            tests: [ +                {error: false, value: {}, action: (value) => { value.test = {}; }}, +                {error: false, value: {}, action: (value) => { value.test = {}; value.test.test = {}; }}, +                {error: false, value: {}, action: (value) => { value.test = {test: {}}; }}, +                {error: true,  value: {}, action: (value) => { value.test = null; }}, +                {error: true,  value: {}, action: (value) => { value.test = 'string'; }}, +                {error: true,  value: {}, action: (value) => { value.test = {}; value.test.test = 'string'; }}, +                {error: true,  value: {}, action: (value) => { value.test = {test: 'string'}; }} +            ]          }      ]; |