summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-05-23 15:49:25 -0400
committerGitHub <noreply@github.com>2021-05-23 15:49:25 -0400
commit54e102f343b651ac41b2ce34d38a3a4638192d4a (patch)
treeffd0c0473807e0ff5299d32942290cb2d2d411c1
parent8e330d54d6bca67b06a6afda3119b0e4bded41e6 (diff)
Json schema ref support (#1708)
* Add basic support for JSON schema $ref * Add tests
-rw-r--r--ext/js/data/json-schema.js89
-rw-r--r--test/test-json-schema.js178
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'}; }}
+ ]
}
];