Json schema ref support (#1708)

* Add basic support for JSON schema $ref

* Add tests
This commit is contained in:
toasted-nutbread 2021-05-23 15:49:25 -04:00 committed by GitHub
parent 8e330d54d6
commit 54e102f343
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 265 additions and 2 deletions

View File

@ -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 {

View File

@ -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'}; }}
]
}
];