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,8 +413,10 @@ function testValidate2() {
function testGetValidValueOrDefault1() { function testGetValidValueOrDefault1() {
const data = [
// Test value defaulting on objects with additionalProperties=false // Test value defaulting on objects with additionalProperties=false
const schema = { {
schema: {
type: 'object', type: 'object',
required: ['test'], required: ['test'],
properties: { properties: {
@ -218,9 +426,8 @@ function testGetValidValueOrDefault1() {
} }
}, },
additionalProperties: false additionalProperties: false
}; },
inputs: [
const testData = [
[ [
void 0, void 0,
{test: 'default'} {test: 'default'}
@ -257,17 +464,12 @@ function testGetValidValueOrDefault1() {
{test: 'value', test2: 'value2'}, {test: 'value', test2: 'value2'},
{test: 'value'} {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 // Test value defaulting on objects with additionalProperties=true
const schema = { {
schema: {
type: 'object', type: 'object',
required: ['test'], required: ['test'],
properties: { properties: {
@ -277,9 +479,8 @@ function testGetValidValueOrDefault2() {
} }
}, },
additionalProperties: true additionalProperties: true
}; },
inputs: [
const testData = [
[ [
{}, {},
{test: 'default'} {test: 'default'}
@ -296,17 +497,12 @@ function testGetValidValueOrDefault2() {
{test: 'value', test2: 'value2'}, {test: 'value', 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} // Test value defaulting on objects with additionalProperties={schema}
const schema = { {
schema: {
type: 'object', type: 'object',
required: ['test'], required: ['test'],
properties: { properties: {
@ -319,9 +515,8 @@ function testGetValidValueOrDefault3() {
type: 'number', type: 'number',
default: 10 default: 10
} }
}; },
inputs: [
const testData = [
[ [
{}, {},
{test: 'default'} {test: 'default'}
@ -354,12 +549,16 @@ function testGetValidValueOrDefault3() {
{test: 'value', test2: 2, test3: void 0}, {test: 'value', test2: 2, test3: void 0},
{test: 'value', test2: 2, test3: 10} {test: 'value', test2: 2, test3: 10}
] ]
]
}
]; ];
for (const [value, expected] of testData) { for (const {schema, inputs} of data) {
for (const [value, expected] of inputs) {
const actual = JsonSchema.getValidValueOrDefault(schema, value); const actual = JsonSchema.getValidValueOrDefault(schema, value);
vm.assert.deepStrictEqual(actual, expected); vm.assert.deepStrictEqual(actual, expected);
} }
}
} }
@ -367,8 +566,6 @@ function main() {
testValidate1(); testValidate1();
testValidate2(); testValidate2();
testGetValidValueOrDefault1(); testGetValidValueOrDefault1();
testGetValidValueOrDefault2();
testGetValidValueOrDefault3();
} }