More JSON schema improvements (#729)

* Add support for constant values

* Add contains check for arrays

* Add tests

* Simplify getValidValueOrDefault testing
This commit is contained in:
toasted-nutbread 2020-08-11 19:21:26 -04:00 committed by GitHub
parent abfa0362dd
commit 587822c16e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 371 additions and 147 deletions

View File

@ -326,6 +326,11 @@ class JsonSchemaValidator {
throw new JsonSchemaValidationError(`Value type ${type} does not match schema type ${schemaType}`, value, schema, info);
}
const schemaConst = schema.const;
if (typeof schemaConst !== 'undefined' && !this.valuesAreEqual(value, schemaConst)) {
throw new JsonSchemaValidationError('Invalid constant value', value, schema, info);
}
const schemaEnum = schema.enum;
if (Array.isArray(schemaEnum) && !this.valuesAreEqualAny(value, schemaEnum)) {
throw new JsonSchemaValidationError('Invalid enum value', value, schema, info);
@ -414,6 +419,8 @@ class JsonSchemaValidator {
throw new JsonSchemaValidationError('Array length too long', value, schema, info);
}
this.validateArrayContains(value, schema, info);
for (let i = 0, ii = value.length; i < ii; ++i) {
const schemaPath = [];
const propertySchema = this.getPropertySchema(schema, i, value, schemaPath);
@ -431,6 +438,26 @@ class JsonSchemaValidator {
}
}
validateArrayContains(value, schema, info) {
const containsSchema = schema.contains;
if (!this.isObject(containsSchema)) { return; }
info.schemaPush('contains', containsSchema);
for (let i = 0, ii = value.length; i < ii; ++i) {
const propertyValue = value[i];
info.valuePush(i, propertyValue);
try {
this.validate(propertyValue, containsSchema, info);
info.schemaPop();
return;
} catch (e) {
// NOP
}
info.valuePop();
}
throw new JsonSchemaValidationError('contains schema didn\'t match', value, schema, info);
}
validateObject(value, schema, info) {
const properties = new Set(Object.getOwnPropertyNames(value));

View File

@ -185,6 +185,212 @@ function testValidate2() {
inputs: [
{expected: false, value: ''}
]
},
// Const tests
{
schema: {
const: 32
},
inputs: [
{expected: true, value: 32},
{expected: false, value: 0},
{expected: false, value: '32'},
{expected: false, value: null},
{expected: false, value: {a: 'b'}},
{expected: false, value: [1, 2, 3]}
]
},
{
schema: {
const: '32'
},
inputs: [
{expected: false, value: 32},
{expected: false, value: 0},
{expected: true, value: '32'},
{expected: false, value: null},
{expected: false, value: {a: 'b'}},
{expected: false, value: [1, 2, 3]}
]
},
{
schema: {
const: null
},
inputs: [
{expected: false, value: 32},
{expected: false, value: 0},
{expected: false, value: '32'},
{expected: true, value: null},
{expected: false, value: {a: 'b'}},
{expected: false, value: [1, 2, 3]}
]
},
{
schema: {
const: {a: 'b'}
},
inputs: [
{expected: false, value: 32},
{expected: false, value: 0},
{expected: false, value: '32'},
{expected: false, value: null},
{expected: false, value: {a: 'b'}},
{expected: false, value: [1, 2, 3]}
]
},
{
schema: {
const: [1, 2, 3]
},
inputs: [
{expected: false, value: 32},
{expected: false, value: 0},
{expected: false, value: '32'},
{expected: false, value: null},
{expected: false, value: {a: 'b'}},
{expected: false, value: [1, 2, 3]}
]
},
// Array contains tests
{
schema: {
type: 'array',
contains: {const: 32}
},
inputs: [
{expected: false, value: []},
{expected: true, value: [32]},
{expected: true, value: [1, 32]},
{expected: true, value: [1, 32, 1]},
{expected: false, value: [33]},
{expected: false, value: [1, 33]},
{expected: false, value: [1, 33, 1]}
]
},
// Number limits tests
{
schema: {
type: 'number',
minimum: 0
},
inputs: [
{expected: false, value: -1},
{expected: true, value: 0},
{expected: true, value: 1}
]
},
{
schema: {
type: 'number',
exclusiveMinimum: 0
},
inputs: [
{expected: false, value: -1},
{expected: false, value: 0},
{expected: true, value: 1}
]
},
{
schema: {
type: 'number',
maximum: 0
},
inputs: [
{expected: true, value: -1},
{expected: true, value: 0},
{expected: false, value: 1}
]
},
{
schema: {
type: 'number',
exclusiveMaximum: 0
},
inputs: [
{expected: true, value: -1},
{expected: false, value: 0},
{expected: false, value: 1}
]
},
// Integer limits tests
{
schema: {
type: 'integer',
minimum: 0
},
inputs: [
{expected: false, value: -1},
{expected: true, value: 0},
{expected: true, value: 1}
]
},
{
schema: {
type: 'integer',
exclusiveMinimum: 0
},
inputs: [
{expected: false, value: -1},
{expected: false, value: 0},
{expected: true, value: 1}
]
},
{
schema: {
type: 'integer',
maximum: 0
},
inputs: [
{expected: true, value: -1},
{expected: true, value: 0},
{expected: false, value: 1}
]
},
{
schema: {
type: 'integer',
exclusiveMaximum: 0
},
inputs: [
{expected: true, value: -1},
{expected: false, value: 0},
{expected: false, value: 1}
]
},
// Numeric type tests
{
schema: {
type: 'number'
},
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: {
type: 'integer'
},
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: {}}
]
}
];
@ -207,158 +413,151 @@ function testValidate2() {
function testGetValidValueOrDefault1() {
// Test value defaulting on objects with additionalProperties=false
const schema = {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'default'
}
const data = [
// Test value defaulting on objects with additionalProperties=false
{
schema: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'default'
}
},
additionalProperties: false
},
inputs: [
[
void 0,
{test: 'default'}
],
[
null,
{test: 'default'}
],
[
0,
{test: 'default'}
],
[
'',
{test: 'default'}
],
[
[],
{test: 'default'}
],
[
{},
{test: 'default'}
],
[
{test: 'value'},
{test: 'value'}
],
[
{test2: 'value2'},
{test: 'default'}
],
[
{test: 'value', test2: 'value2'},
{test: 'value'}
]
]
},
additionalProperties: false
};
const testData = [
[
void 0,
{test: 'default'}
],
[
null,
{test: 'default'}
],
[
0,
{test: 'default'}
],
[
'',
{test: 'default'}
],
[
[],
{test: 'default'}
],
[
{},
{test: 'default'}
],
[
{test: 'value'},
{test: 'value'}
],
[
{test2: 'value2'},
{test: 'default'}
],
[
{test: 'value', test2: 'value2'},
{test: 'value'}
]
];
for (const [value, expected] of testData) {
const actual = JsonSchema.getValidValueOrDefault(schema, value);
vm.assert.deepStrictEqual(actual, expected);
}
}
function testGetValidValueOrDefault2() {
// Test value defaulting on objects with additionalProperties=true
const schema = {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'default'
}
// Test value defaulting on objects with additionalProperties=true
{
schema: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'default'
}
},
additionalProperties: true
},
inputs: [
[
{},
{test: 'default'}
],
[
{test: 'value'},
{test: 'value'}
],
[
{test2: 'value2'},
{test: 'default', test2: 'value2'}
],
[
{test: 'value', test2: 'value2'},
{test: 'value', test2: 'value2'}
]
]
},
additionalProperties: true
};
const testData = [
[
{},
{test: 'default'}
],
[
{test: 'value'},
{test: 'value'}
],
[
{test2: 'value2'},
{test: 'default', test2: 'value2'}
],
[
{test: 'value', test2: 'value2'},
{test: 'value', test2: 'value2'}
]
];
for (const [value, expected] of testData) {
const actual = JsonSchema.getValidValueOrDefault(schema, value);
vm.assert.deepStrictEqual(actual, expected);
}
}
function testGetValidValueOrDefault3() {
// Test value defaulting on objects with additionalProperties={schema}
const schema = {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'default'
}
},
additionalProperties: {
type: 'number',
default: 10
// Test value defaulting on objects with additionalProperties={schema}
{
schema: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'string',
default: 'default'
}
},
additionalProperties: {
type: 'number',
default: 10
}
},
inputs: [
[
{},
{test: 'default'}
],
[
{test: 'value'},
{test: 'value'}
],
[
{test2: 'value2'},
{test: 'default', test2: 10}
],
[
{test: 'value', test2: 'value2'},
{test: 'value', test2: 10}
],
[
{test2: 2},
{test: 'default', test2: 2}
],
[
{test: 'value', test2: 2},
{test: 'value', test2: 2}
],
[
{test: 'value', test2: 2, test3: null},
{test: 'value', test2: 2, test3: 10}
],
[
{test: 'value', test2: 2, test3: void 0},
{test: 'value', test2: 2, test3: 10}
]
]
}
};
const testData = [
[
{},
{test: 'default'}
],
[
{test: 'value'},
{test: 'value'}
],
[
{test2: 'value2'},
{test: 'default', test2: 10}
],
[
{test: 'value', test2: 'value2'},
{test: 'value', test2: 10}
],
[
{test2: 2},
{test: 'default', test2: 2}
],
[
{test: 'value', test2: 2},
{test: 'value', test2: 2}
],
[
{test: 'value', test2: 2, test3: null},
{test: 'value', test2: 2, test3: 10}
],
[
{test: 'value', test2: 2, test3: void 0},
{test: 'value', test2: 2, test3: 10}
]
];
for (const [value, expected] of testData) {
const actual = JsonSchema.getValidValueOrDefault(schema, value);
vm.assert.deepStrictEqual(actual, expected);
for (const {schema, inputs} of data) {
for (const [value, expected] of inputs) {
const actual = JsonSchema.getValidValueOrDefault(schema, value);
vm.assert.deepStrictEqual(actual, expected);
}
}
}
@ -367,8 +566,6 @@ function main() {
testValidate1();
testValidate2();
testGetValidValueOrDefault1();
testGetValidValueOrDefault2();
testGetValidValueOrDefault3();
}