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); 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; const schemaEnum = schema.enum;
if (Array.isArray(schemaEnum) && !this.valuesAreEqualAny(value, schemaEnum)) { if (Array.isArray(schemaEnum) && !this.valuesAreEqualAny(value, schemaEnum)) {
throw new JsonSchemaValidationError('Invalid enum value', value, schema, info); 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); 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) { for (let i = 0, ii = value.length; i < ii; ++i) {
const schemaPath = []; const schemaPath = [];
const propertySchema = this.getPropertySchema(schema, i, value, 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) { validateObject(value, schema, info) {
const properties = new Set(Object.getOwnPropertyNames(value)); const properties = new Set(Object.getOwnPropertyNames(value));

View File

@ -185,6 +185,212 @@ function testValidate2() {
inputs: [ inputs: [
{expected: false, value: ''} {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() { function testGetValidValueOrDefault1() {
// Test value defaulting on objects with additionalProperties=false const data = [
const schema = { // Test value defaulting on objects with additionalProperties=false
type: 'object', {
required: ['test'], schema: {
properties: { type: 'object',
test: { required: ['test'],
type: 'string', properties: {
default: 'default' 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 = [ // Test value defaulting on objects with additionalProperties=true
[ {
void 0, schema: {
{test: 'default'} type: 'object',
], required: ['test'],
[ properties: {
null, test: {
{test: 'default'} type: 'string',
], default: 'default'
[ }
0, },
{test: 'default'} additionalProperties: true
], },
[ inputs: [
'', [
{test: 'default'} {},
], {test: 'default'}
[ ],
[], [
{test: 'default'} {test: 'value'},
], {test: 'value'}
[ ],
{}, [
{test: 'default'} {test2: 'value2'},
], {test: 'default', test2: 'value2'}
[ ],
{test: 'value'}, [
{test: 'value'} {test: 'value', test2: 'value2'},
], {test: 'value', test2: 'value2'}
[ ]
{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'
}
}, },
additionalProperties: true
};
const testData = [ // Test value defaulting on objects with additionalProperties={schema}
[ {
{}, schema: {
{test: 'default'} type: 'object',
], required: ['test'],
[ properties: {
{test: 'value'}, test: {
{test: 'value'} type: 'string',
], default: 'default'
[ }
{test2: 'value2'}, },
{test: 'default', test2: 'value2'} additionalProperties: {
], type: 'number',
[ default: 10
{test: 'value', test2: 'value2'}, }
{test: 'value', test2: 'value2'} },
] inputs: [
]; [
{},
for (const [value, expected] of testData) { {test: 'default'}
const actual = JsonSchema.getValidValueOrDefault(schema, value); ],
vm.assert.deepStrictEqual(actual, expected); [
} {test: 'value'},
} {test: 'value'}
],
function testGetValidValueOrDefault3() { [
// Test value defaulting on objects with additionalProperties={schema} {test2: 'value2'},
const schema = { {test: 'default', test2: 10}
type: 'object', ],
required: ['test'], [
properties: { {test: 'value', test2: 'value2'},
test: { {test: 'value', test2: 10}
type: 'string', ],
default: 'default' [
} {test2: 2},
}, {test: 'default', test2: 2}
additionalProperties: { ],
type: 'number', [
default: 10 {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) { for (const {schema, inputs} of data) {
const actual = JsonSchema.getValidValueOrDefault(schema, value); for (const [value, expected] of inputs) {
vm.assert.deepStrictEqual(actual, expected); const actual = JsonSchema.getValidValueOrDefault(schema, value);
vm.assert.deepStrictEqual(actual, expected);
}
} }
} }
@ -367,8 +566,6 @@ function main() {
testValidate1(); testValidate1();
testValidate2(); testValidate2();
testGetValidValueOrDefault1(); testGetValidValueOrDefault1();
testGetValidValueOrDefault2();
testGetValidValueOrDefault3();
} }