Json schema ref support (#1708)
* Add basic support for JSON schema $ref * Add tests
This commit is contained in:
parent
8e330d54d6
commit
54e102f343
@ -25,6 +25,7 @@ class JsonSchema {
|
|||||||
this._startSchema = schema;
|
this._startSchema = schema;
|
||||||
this._rootSchema = typeof rootSchema !== 'undefined' ? rootSchema : schema;
|
this._rootSchema = typeof rootSchema !== 'undefined' ? rootSchema : schema;
|
||||||
this._regexCache = null;
|
this._regexCache = null;
|
||||||
|
this._refCache = null;
|
||||||
this._valueStack = [];
|
this._valueStack = [];
|
||||||
this._schemaStack = [];
|
this._schemaStack = [];
|
||||||
|
|
||||||
@ -73,7 +74,8 @@ class JsonSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getObjectPropertySchema(property) {
|
getObjectPropertySchema(property) {
|
||||||
this._schemaPush(this._startSchema, null);
|
const startSchemaInfo = this._getResolveSchemaInfo({schema: this._startSchema, path: null});
|
||||||
|
this._schemaPush(startSchemaInfo.schema, startSchemaInfo.path);
|
||||||
try {
|
try {
|
||||||
const schemaInfo = this._getObjectPropertySchemaInfo(property);
|
const schemaInfo = this._getObjectPropertySchemaInfo(property);
|
||||||
return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null;
|
return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null;
|
||||||
@ -83,7 +85,8 @@ class JsonSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getArrayItemSchema(index) {
|
getArrayItemSchema(index) {
|
||||||
this._schemaPush(this._startSchema, null);
|
const startSchemaInfo = this._getResolveSchemaInfo({schema: this._startSchema, path: null});
|
||||||
|
this._schemaPush(startSchemaInfo.schema, startSchemaInfo.path);
|
||||||
try {
|
try {
|
||||||
const schemaInfo = this._getArrayItemSchemaInfo(index);
|
const schemaInfo = this._getArrayItemSchemaInfo(index);
|
||||||
return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null;
|
return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null;
|
||||||
@ -267,6 +270,71 @@ class JsonSchema {
|
|||||||
return 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);
|
||||||
|
}
|
||||||
|
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
|
// Validation
|
||||||
|
|
||||||
_isValidCurrent(value) {
|
_isValidCurrent(value) {
|
||||||
@ -279,6 +347,22 @@ class JsonSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_validate(value) {
|
_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._validateSingleSchema(value);
|
||||||
this._validateConditional(value);
|
this._validateConditional(value);
|
||||||
this._validateAllOf(value);
|
this._validateAllOf(value);
|
||||||
@ -638,6 +722,7 @@ class JsonSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getValidValueOrDefault(path, value, schemaInfo) {
|
_getValidValueOrDefault(path, value, schemaInfo) {
|
||||||
|
schemaInfo = this._getResolveSchemaInfo(schemaInfo);
|
||||||
this._schemaPush(schemaInfo.schema, schemaInfo.path);
|
this._schemaPush(schemaInfo.schema, schemaInfo.path);
|
||||||
this._valuePush(value, path);
|
this._valuePush(value, path);
|
||||||
try {
|
try {
|
||||||
|
@ -413,6 +413,80 @@ function testValidate2() {
|
|||||||
{expected: false, value: []},
|
{expected: false, value: []},
|
||||||
{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: -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: true, value: ['default'], action: (value) => { delete value[0]; }},
|
||||||
{error: false, value: ['default'], action: (value) => { value[1] = 'string'; }}
|
{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'}; }}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user