From 587822c16e3f573362fdfe291c9afc37ca31bb15 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 11 Aug 2020 19:21:26 -0400 Subject: More JSON schema improvements (#729) * Add support for constant values * Add contains check for arrays * Add tests * Simplify getValidValueOrDefault testing --- ext/bg/js/json-schema.js | 27 +++ test/test-schema.js | 491 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 371 insertions(+), 147 deletions(-) diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index b777ac09..7cc87bb0 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -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)); diff --git a/test/test-schema.js b/test/test-schema.js index b976d81f..b3544b28 100644 --- a/test/test-schema.js +++ b/test/test-schema.js @@ -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(); } -- cgit v1.2.3