From 9c2d6fb95702c51bdde72626b3f5cdc097d2d310 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Fri, 20 Jan 2023 15:19:15 +0100 Subject: [PATCH 1/9] const is validated by fjs --- index.js | 64 +++++++++++---- test/const.test.js | 189 ++++++++++++++++++++------------------------- 2 files changed, 130 insertions(+), 123 deletions(-) diff --git a/index.js b/index.js index 198fe163..431ab048 100644 --- a/index.js +++ b/index.js @@ -785,26 +785,56 @@ function buildSingleTypeSerializer (location, input) { function buildConstSerializer (location, input) { const schema = location.schema - const type = schema.type - - const hasNullType = Array.isArray(type) && type.includes('null') - + let schemaRef = location.getSchemaRef() + if (schemaRef.startsWith(rootSchemaId)) { + schemaRef = schemaRef.replace(rootSchemaId, '') + } let code = '' - if (hasNullType) { - code += ` - if (${input} === null) { - json += 'null' + switch (typeof schema.const) { + case 'bigint': + code += ` + if (${input} === ${schema.const}n) { + json += '${Number(schema.const)}' } else { - ` - } - - code += `json += '${JSON.stringify(schema.const)}'` - - if (hasNullType) { - code += ` + throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) + }` + break + case 'number': + case 'boolean': + code += ` + if (${input} === ${schema.const}) { + json += ${JSON.stringify(JSON.stringify(schema.const))} + } else { + throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) + }` + break + case 'object': + if (schema.const === null) { + code += ` + if (${input} === null) { + json += 'null' + } else { + throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) + }` + } else { + code += ` + if (JSON.stringify(${input}) === '${JSON.stringify(schema.const)}') { + json += ${JSON.stringify(JSON.stringify(schema.const))} + } else { + throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) + }` } - ` + break + case 'string': + case 'undefined': + code += ` + if (${input} === '${schema.const}') { + json += ${JSON.stringify(JSON.stringify(schema.const))} + } else { + throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) + }` + break } return code @@ -869,7 +899,7 @@ function buildValue (location, input) { return code } - const nullable = schema.nullable === true + const nullable = schema.nullable === true && !('const' in schema) if (nullable) { code += ` if (${input} === null) { diff --git a/test/const.test.js b/test/const.test.js index c0b0b0aa..04b72fae 100644 --- a/test/const.test.js +++ b/test/const.test.js @@ -1,11 +1,11 @@ 'use strict' const test = require('tap').test -const validator = require('is-my-json-valid') +const ajv = new (require('ajv'))() const build = require('..') test('schema with const string', (t) => { - t.plan(2) + t.plan(3) const schema = { type: 'object', @@ -14,14 +14,13 @@ test('schema with const string', (t) => { } } - const validate = validator(schema) + const input = { foo: 'bar' } + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: 'bar' - }) - t.equal(output, '{"foo":"bar"}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), '{"foo":"bar"}') + t.ok(validate((input))) + t.equal(stringify(input), JSON.stringify(input)) }) test('schema with const string and different input', (t) => { @@ -30,18 +29,16 @@ test('schema with const string and different input', (t) => { const schema = { type: 'object', properties: { - foo: { const: 'bar' } + foo: { type: 'string', const: 'bar' } } } - const validate = validator(schema) + const input = { foo: 'baz' } + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: 'baz' - }) - t.equal(output, '{"foo":"bar"}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.throws(() => stringify(input), new Error("The value of '#/properties/foo' does not match schema definition.")) + t.not(validate(input)) }) test('schema with const string and different type input', (t) => { @@ -54,18 +51,16 @@ test('schema with const string and different type input', (t) => { } } - const validate = validator(schema) + const input = { foo: 1 } + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: 1 - }) - t.equal(output, '{"foo":"bar"}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.throws(() => stringify(input), new Error("The value of '#/properties/foo' does not match schema definition.")) + t.not(validate(input)) }) test('schema with const string and no input', (t) => { - t.plan(2) + t.plan(3) const schema = { type: 'object', @@ -74,16 +69,17 @@ test('schema with const string and no input', (t) => { } } - const validate = validator(schema) + const input = {} + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({}) - t.equal(output, '{}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), '{}') + t.ok(validate((input))) + t.equal(stringify(input), JSON.stringify(input)) }) test('schema with const number', (t) => { - t.plan(2) + t.plan(3) const schema = { type: 'object', @@ -92,14 +88,13 @@ test('schema with const number', (t) => { } } - const validate = validator(schema) + const input = { foo: 1 } + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: 1 - }) - t.equal(output, '{"foo":1}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), '{"foo":1}') + t.ok(validate((input))) + t.equal(stringify(input), JSON.stringify(input)) }) test('schema with const number and different input', (t) => { @@ -112,18 +107,16 @@ test('schema with const number and different input', (t) => { } } - const validate = validator(schema) + const input = { foo: 2 } + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: 2 - }) - t.equal(output, '{"foo":1}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.throws(() => stringify(input), new Error("The value of '#/properties/foo' does not match schema definition.")) + t.not(validate(input)) }) test('schema with const bool', (t) => { - t.plan(2) + t.plan(3) const schema = { type: 'object', @@ -132,18 +125,17 @@ test('schema with const bool', (t) => { } } - const validate = validator(schema) + const input = { foo: true } + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: true - }) - t.equal(output, '{"foo":true}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), '{"foo":true}') + t.ok(validate((input))) + t.equal(stringify(input), JSON.stringify(input)) }) test('schema with const number', (t) => { - t.plan(2) + t.plan(3) const schema = { type: 'object', @@ -152,18 +144,17 @@ test('schema with const number', (t) => { } } - const validate = validator(schema) + const input = { foo: 1 } + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: 1 - }) - t.equal(output, '{"foo":1}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), '{"foo":1}') + t.ok(validate((input))) + t.equal(stringify(input), JSON.stringify(input)) }) test('schema with const null', (t) => { - t.plan(2) + t.plan(3) const schema = { type: 'object', @@ -172,18 +163,17 @@ test('schema with const null', (t) => { } } - const validate = validator(schema) + const input = { foo: null } + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: null - }) - t.equal(output, '{"foo":null}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), '{"foo":null}') + t.ok(validate((input))) + t.equal(stringify(input), JSON.stringify(input)) }) test('schema with const array', (t) => { - t.plan(2) + t.plan(3) const schema = { type: 'object', @@ -192,18 +182,17 @@ test('schema with const array', (t) => { } } - const validate = validator(schema) + const input = { foo: [1, 2, 3] } + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: [1, 2, 3] - }) - t.equal(output, '{"foo":[1,2,3]}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), '{"foo":[1,2,3]}') + t.ok(validate((input))) + t.equal(stringify(input), JSON.stringify(input)) }) test('schema with const object', (t) => { - t.plan(2) + t.plan(3) const schema = { type: 'object', @@ -212,18 +201,17 @@ test('schema with const object', (t) => { } } - const validate = validator(schema) + const input = { foo: { bar: 'baz' } } + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: { bar: 'baz' } - }) - t.equal(output, '{"foo":{"bar":"baz"}}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), '{"foo":{"bar":"baz"}}') + t.ok(validate((input))) + t.equal(stringify(input), JSON.stringify(input)) }) test('schema with const and null as type', (t) => { - t.plan(4) + t.plan(5) const schema = { type: 'object', @@ -231,45 +219,36 @@ test('schema with const and null as type', (t) => { foo: { type: ['string', 'null'], const: 'baz' } } } - - const validate = validator(schema) + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: null - }) - t.equal(output, '{"foo":null}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.throws(() => stringify({ foo: null }), new Error("The value of '\#/properties/foo' does not match schema definition.")) + t.not(validate({ foo: null })) - const output2 = stringify({ foo: 'baz' }) - t.equal(output2, '{"foo":"baz"}') - t.ok(validate(JSON.parse(output2)), 'valid schema') + t.equal(stringify({ foo: 'baz' }), '{"foo":"baz"}') + t.ok(validate({ foo: 'baz' })) + t.equal(stringify({ foo: 'baz' }), JSON.stringify({ foo: 'baz' })) }) test('schema with const as nullable', (t) => { - t.plan(4) + t.plan(5) const schema = { type: 'object', properties: { - foo: { nullable: true, const: 'baz' } + foo: { type: 'string', nullable: true, const: 'baz' } } } - const validate = validator(schema) + const validate = ajv.compile(schema) const stringify = build(schema) - const output = stringify({ - foo: null - }) - - t.equal(output, '{"foo":null}') - t.ok(validate(JSON.parse(output)), 'valid schema') - - const output2 = stringify({ - foo: 'baz' - }) - t.equal(output2, '{"foo":"baz"}') - t.ok(validate(JSON.parse(output2)), 'valid schema') + + t.throws(() => stringify({ foo: null }), new Error("The value of '\#/properties/foo' does not match schema definition.")) + t.not(validate({ foo: null })) + + t.equal(stringify({ foo: 'baz' }), '{"foo":"baz"}') + t.ok(validate({ foo: 'baz' })) + t.equal(stringify({ foo: 'baz' }), JSON.stringify({ foo: 'baz' })) }) test('schema with const and invalid object', (t) => { @@ -283,12 +262,10 @@ test('schema with const and invalid object', (t) => { required: ['foo'] } - const validate = validator(schema) + const input = { foo: { foo: 'baz' } } + const validate = ajv.compile(schema) const stringify = build(schema) - const result = stringify({ - foo: { foo: 'baz' } - }) - t.equal(result, '{"foo":{"foo":"bar"}}') - t.ok(validate(JSON.parse(result)), 'valid schema') + t.throws(() => stringify(input), new Error("The value of '\#/properties/foo' does not match schema definition.")) + t.not(validate(input)) }) From f00533a5279d650bf88079214ceba0148cc61c94 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 24 Jan 2023 05:00:06 +0100 Subject: [PATCH 2/9] fix linting error --- test/const.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/const.test.js b/test/const.test.js index 04b72fae..a9fab963 100644 --- a/test/const.test.js +++ b/test/const.test.js @@ -222,7 +222,7 @@ test('schema with const and null as type', (t) => { const validate = ajv.compile(schema) const stringify = build(schema) - t.throws(() => stringify({ foo: null }), new Error("The value of '\#/properties/foo' does not match schema definition.")) + t.throws(() => stringify({ foo: null }), new Error("The value of '#/properties/foo' does not match schema definition.")) t.not(validate({ foo: null })) t.equal(stringify({ foo: 'baz' }), '{"foo":"baz"}') @@ -243,7 +243,7 @@ test('schema with const as nullable', (t) => { const validate = ajv.compile(schema) const stringify = build(schema) - t.throws(() => stringify({ foo: null }), new Error("The value of '\#/properties/foo' does not match schema definition.")) + t.throws(() => stringify({ foo: null }), new Error("The value of '#/properties/foo' does not match schema definition.")) t.not(validate({ foo: null })) t.equal(stringify({ foo: 'baz' }), '{"foo":"baz"}') @@ -266,6 +266,6 @@ test('schema with const and invalid object', (t) => { const validate = ajv.compile(schema) const stringify = build(schema) - t.throws(() => stringify(input), new Error("The value of '\#/properties/foo' does not match schema definition.")) + t.throws(() => stringify(input), new Error("The value of '#/properties/foo' does not match schema definition.")) t.not(validate(input)) }) From a7c9ac7c839ae538fc63d7cc0c2f0d6c22401042 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 24 Jan 2023 05:24:35 +0100 Subject: [PATCH 3/9] add const validator --- index.js | 3 +- lib/const-validator.js | 120 +++++++++ test/const-validator.test.js | 152 +++++++++++ test/if-then-else.test.js | 4 +- test/spec/fast-deep-equal.spec.js | 406 ++++++++++++++++++++++++++++++ 5 files changed, 682 insertions(+), 3 deletions(-) create mode 100644 lib/const-validator.js create mode 100644 test/const-validator.test.js create mode 100644 test/spec/fast-deep-equal.spec.js diff --git a/index.js b/index.js index 431ab048..683d5137 100644 --- a/index.js +++ b/index.js @@ -11,6 +11,7 @@ const Serializer = require('./lib/serializer') const Validator = require('./lib/validator') const RefResolver = require('./lib/ref-resolver') const Location = require('./lib/location') +const constValidator = require('./lib/const-validator') let largeArraySize = 2e4 let largeArrayMechanism = 'default' @@ -819,7 +820,7 @@ function buildConstSerializer (location, input) { }` } else { code += ` - if (JSON.stringify(${input}) === '${JSON.stringify(schema.const)}') { + if (${constValidator(schema.const, input, 'integration')}) { json += ${JSON.stringify(JSON.stringify(schema.const))} } else { throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) diff --git a/lib/const-validator.js b/lib/const-validator.js new file mode 100644 index 00000000..f8baee2e --- /dev/null +++ b/lib/const-validator.js @@ -0,0 +1,120 @@ +'use strict' + +function constValidator (input, accessPath = 'value', mode = 'function') { + if (mode === 'integration') { + return `${_const(input, accessPath)} true\n` + } else { + return new Function('value', `return (\n${_const(input, accessPath)} true\n)`) // eslint-disable-line no-new-func + } +} + +function _const (input, accessPath) { + let functionCode = '' + switch (typeof input) { + case 'undefined': + functionCode += _constUndefined(input, accessPath) + break + case 'bigint': + functionCode += _constBigInt(input, accessPath) + break + case 'boolean': + functionCode += _constBoolean(input, accessPath) + break + case 'number': + functionCode += _constNumber(input, accessPath) + break + case 'string': + functionCode += _constString(input, accessPath) + break + case 'object': + functionCode += _constObject(input, accessPath) + break + } + return functionCode +} + +function _constUndefined (input, accessPath) { + return `typeof ${accessPath} === 'undefined' &&\n` +} + +function _constBigInt (input, accessPath) { + return `typeof ${accessPath} === 'bigint' && ${accessPath} === ${input.toString()}n &&\n` +} + +function _constNumber (input, accessPath) { + if (input !== input) { // eslint-disable-line no-self-compare + return `typeof ${accessPath} === 'number' && ${accessPath} !== ${accessPath} &&\n` + } else { + return `typeof ${accessPath} === 'number' && ${accessPath} === ${input.toString()} &&\n` + } +} + +function _constBoolean (input, accessPath) { + return `typeof ${accessPath} === 'boolean' && ${accessPath} === ${input ? 'true' : 'false'} &&\n` +} + +function _constString (input, accessPath) { + return `typeof ${accessPath} === 'string' && ${accessPath} === ${JSON.stringify(input)} &&\n` +} + +function _constNull (input, accessPath) { + return `typeof ${accessPath} === 'object' && ${accessPath} === null &&\n` +} + +function _constArray (input, accessPath) { + let functionCode = `Array.isArray(${accessPath}) && ${accessPath}.length === ${input.length} &&\n` + for (let i = 0; i < input.length; ++i) { + functionCode += _const(input[i], `${accessPath}[${i}]`) + } + return functionCode +} + +function _constPOJO (input, accessPath) { + const keys = Object.keys(input) + + let functionCode = `typeof ${accessPath} === 'object' && ${accessPath} !== null &&\n` + + functionCode += `Object.keys(${accessPath}).length === ${keys.length} &&\n` + + if (typeof input.valueOf === 'function') { + functionCode += `(${accessPath}.valueOf === Object.prototype.valueOf || ${accessPath}.valueOf() === ${JSON.stringify(input.valueOf())}) &&\n` + } + + if (typeof input.toString === 'function') { + functionCode += `(${accessPath}.toString === Object.prototype.toString || ${accessPath}.toString() === ${JSON.stringify(input.toString())}) &&\n` + } + // check keys + for (const key of keys) { + functionCode += `${JSON.stringify(key)} in ${accessPath} &&\n` + } + // check values + for (const key of keys) { + functionCode += `${_const(input[key], `${accessPath}[${JSON.stringify(key)}]`)}` + } + + return functionCode +} + +function _constRegExp (input, accessPath) { + return `typeof ${accessPath} === 'object' && ${accessPath} !== null && ${accessPath}.constructor === RegExp && ${accessPath}.source === ${JSON.stringify(input.source)} && ${accessPath}.flags === ${JSON.stringify(input.flags)} &&\n` +} + +function _constDate (input, accessPath) { + return `typeof ${accessPath} === 'object' && ${accessPath} !== null && ${accessPath}.constructor === Date && ${accessPath}.getTime() === ${input.getTime()} &&\n` +} + +function _constObject (input, accessPath) { + if (input === null) { + return _constNull(input, accessPath) + } else if (Array.isArray(input)) { + return _constArray(input, accessPath) + } else if (input.constructor === RegExp) { + return _constRegExp(input, accessPath) + } else if (input.constructor === Date) { + return _constDate(input, accessPath) + } else { + return _constPOJO(input, accessPath) + } +} + +module.exports = constValidator diff --git a/test/const-validator.test.js b/test/const-validator.test.js new file mode 100644 index 00000000..8ec2db65 --- /dev/null +++ b/test/const-validator.test.js @@ -0,0 +1,152 @@ +'use strict' + +const test = require('tap').test +const constValidator = require('../lib/const-validator') + +test('string', (t) => { + t.plan(2) + + const validateConst = constValidator('stringValue') + + t.equal(validateConst('stringValue'), true) + t.equal(validateConst('b'), false) +}) + +test('number', (t) => { + t.plan(2) + + const validateConst = constValidator(42) + + t.equal(validateConst(42), true) + t.equal(validateConst(43), false) +}) + +test('bigint', (t) => { + t.plan(2) + + const validateConst = constValidator(42n) + + t.equal(validateConst(42n), true) + t.equal(validateConst(43n), false) +}) + +test('boolean', (t) => { + t.plan(2) + + const validateConst = constValidator(true) + + t.equal(validateConst(true), true) + t.equal(validateConst(false), false) +}) + +test('null', (t) => { + t.plan(2) + + const validateConst = constValidator(null) + + t.equal(validateConst(null), true) + t.equal(validateConst('null'), false) +}) + +test('array, basic', (t) => { + t.plan(3) + + const validateConst = constValidator([1, 2, 3]) + + t.equal(validateConst([1, 2, 3]), true) + t.equal(validateConst([1, 2]), false) + t.equal(validateConst([1, 2, 3, 4]), false) +}) + +test('array, only numbers', (t) => { + t.plan(2) + + const validateConst = constValidator([1, 2, 3]) + + t.equal(validateConst([1, 2, 3]), true) + t.equal(validateConst([1, 2, 4]), false) +}) + +test('array, sub arrays with numbers', (t) => { + t.plan(3) + + const validateConst = constValidator([[1, 2], 3]) + + t.equal(validateConst([[1, 2], 3]), true) + t.equal(validateConst([[1, 2], 4]), false) + t.equal(validateConst([[1, 3], 4]), false) +}) + +test('object, two properties', (t) => { + t.plan(3) + + const validateConst = constValidator({ a: 1, b: 2 }) + + t.equal(validateConst({ a: 1, b: 2 }), true) + t.equal(validateConst({ b: 2, a: 1 }), true) + t.equal(validateConst({ a: 1, b: 3 }), false) +}) + +test('NaN', (t) => { + t.plan(2) + + const validateConst = constValidator(NaN) + + t.equal(validateConst(NaN), true) + t.equal(validateConst(Infinity), false) +}) + +test('Infinity', (t) => { + t.plan(2) + + const validateConst = constValidator(Infinity) + + t.equal(validateConst(Infinity), true) + t.equal(validateConst(-Infinity), false) +}) + +test('Infinity', (t) => { + t.plan(2) + + const validateConst = constValidator(Infinity) + + t.equal(validateConst(Infinity), true) + t.equal(validateConst(-Infinity), false) +}) + +test('RegExp', (t) => { + t.plan(3) + + const validateConst = constValidator(/a-z/g) + + t.equal(validateConst(/a-z/g), true) + t.equal(validateConst(/a-z/gm), false) + t.equal(validateConst(/a-Z/gm), false) +}) + +test('Date', (t) => { + t.plan(2) + + const validateConst = constValidator(new Date(123)) + + t.equal(validateConst(new Date(123)), true) + t.equal(validateConst(new Date(124)), false) +}) + +const spec = require('./spec/fast-deep-equal.spec') + +spec.forEach(function (suite) { + test(suite.description, function (t) { + t.plan(suite.tests.length * 2) + suite.tests.forEach(function (testCase) { + t.test(testCase.description, function (t) { + t.plan(1) + t.equal(constValidator(testCase.value1)(testCase.value2), testCase.equal) + }) + t.test(testCase.description + ' (reverse arguments)', function (t) { + t.plan(1) + t.equal(constValidator(testCase.value2)(testCase.value1), testCase.equal) + }) + }) + }) +}) diff --git a/test/if-then-else.test.js b/test/if-then-else.test.js index 33a4b192..b2b301f8 100644 --- a/test/if-then-else.test.js +++ b/test/if-then-else.test.js @@ -399,8 +399,8 @@ t.test('if/else with const integers', (t) => { const stringify = build(schema) - t.equal(stringify(100.32), '66') - t.equal(stringify(10.12), '33') + t.throws(() => stringify(100.32), new Error('The value of \'#/then\' does not match schema definition.')) + t.throws(() => stringify(10 - 12), new Error('The value of \'#/else\' does not match schema definition.')) }) t.test('if/else with array', (t) => { diff --git a/test/spec/fast-deep-equal.spec.js b/test/spec/fast-deep-equal.spec.js new file mode 100644 index 00000000..c95e6f09 --- /dev/null +++ b/test/spec/fast-deep-equal.spec.js @@ -0,0 +1,406 @@ +'use strict' + +module.exports = [ + { + description: 'scalars', + tests: [ + { + description: 'equal numbers', + value1: 1, + value2: 1, + equal: true + }, + { + description: 'not equal numbers', + value1: 1, + value2: 2, + equal: false + }, + { + description: 'number and array are not equal', + value1: 1, + value2: [], + equal: false + }, + { + description: '0 and null are not equal', + value1: 0, + value2: null, + equal: false + }, + { + description: 'equal strings', + value1: 'a', + value2: 'a', + equal: true + }, + { + description: 'not equal strings', + value1: 'a', + value2: 'b', + equal: false + }, + { + description: 'empty string and null are not equal', + value1: '', + value2: null, + equal: false + }, + { + description: 'null is equal to null', + value1: null, + value2: null, + equal: true + }, + { + description: 'equal booleans (true)', + value1: true, + value2: true, + equal: true + }, + { + description: 'equal booleans (false)', + value1: false, + value2: false, + equal: true + }, + { + description: 'not equal booleans', + value1: true, + value2: false, + equal: false + }, + { + description: '1 and true are not equal', + value1: 1, + value2: true, + equal: false + }, + { + description: '0 and false are not equal', + value1: 0, + value2: false, + equal: false + }, + { + description: 'NaN and NaN are equal', + value1: NaN, + value2: NaN, + equal: true + }, + { + description: '0 and -0 are equal', + value1: 0, + value2: -0, + equal: true + }, + { + description: 'Infinity and Infinity are equal', + value1: Infinity, + value2: Infinity, + equal: true + }, + { + description: 'Infinity and -Infinity are not equal', + value1: Infinity, + value2: -Infinity, + equal: false + } + ] + }, + + { + description: 'objects', + tests: [ + { + description: 'empty objects are equal', + value1: {}, + value2: {}, + equal: true + }, + { + description: 'equal objects (same properties "order")', + value1: { a: 1, b: '2' }, + value2: { a: 1, b: '2' }, + equal: true + }, + { + description: 'equal objects (different properties "order")', + value1: { a: 1, b: '2' }, + value2: { b: '2', a: 1 }, + equal: true + }, + { + description: 'not equal objects (extra property)', + value1: { a: 1, b: '2' }, + value2: { a: 1, b: '2', c: [] }, + equal: false + }, + { + description: 'not equal objects (different property values)', + value1: { a: 1, b: '2', c: 3 }, + value2: { a: 1, b: '2', c: 4 }, + equal: false + }, + { + description: 'not equal objects (different properties)', + value1: { a: 1, b: '2', c: 3 }, + value2: { a: 1, b: '2', d: 3 }, + equal: false + }, + { + description: 'equal objects (same sub-properties)', + value1: { a: [{ b: 'c' }] }, + value2: { a: [{ b: 'c' }] }, + equal: true + }, + { + description: 'not equal objects (different sub-property value)', + value1: { a: [{ b: 'c' }] }, + value2: { a: [{ b: 'd' }] }, + equal: false + }, + { + description: 'not equal objects (different sub-property)', + value1: { a: [{ b: 'c' }] }, + value2: { a: [{ c: 'c' }] }, + equal: false + }, + { + description: 'empty array and empty object are not equal', + value1: {}, + value2: [], + equal: false + }, + { + description: 'object with extra undefined properties are not equal #1', + value1: {}, + value2: { foo: undefined }, + equal: false + }, + { + description: 'object with extra undefined properties are not equal #2', + value1: { foo: undefined }, + value2: {}, + equal: false + }, + { + description: 'object with extra undefined properties are not equal #3', + value1: { foo: undefined }, + value2: { bar: undefined }, + equal: false + }, + { + description: 'nulls are equal', + value1: null, + value2: null, + equal: true + }, + { + description: 'null and undefined are not equal', + value1: null, + value2: undefined, + equal: false + }, + { + description: 'null and empty object are not equal', + value1: null, + value2: {}, + equal: false + }, + { + description: 'undefined and empty object are not equal', + value1: undefined, + value2: {}, + equal: false + }, + { + description: 'objects with different `toString` functions returning same values are equal', + value1: { toString: () => 'Hello world!' }, + value2: { toString: () => 'Hello world!' }, + equal: true + }, + { + description: 'objects with `toString` functions returning different values are not equal', + value1: { toString: () => 'Hello world!' }, + value2: { toString: () => 'Hi!' }, + equal: false + }, + { + description: 'objects without `valueOf` and `toString` function do not throw error', + value1: Object.assign(Object.create(null), { a: 1 }), + value2: Object.assign(Object.create(null), { a: 1 }), + equal: true + } + ] + }, + + { + description: 'arrays', + tests: [ + { + description: 'two empty arrays are equal', + value1: [], + value2: [], + equal: true + }, + { + description: 'equal arrays', + value1: [1, 2, 3], + value2: [1, 2, 3], + equal: true + }, + { + description: 'not equal arrays (different item)', + value1: [1, 2, 3], + value2: [1, 2, 4], + equal: false + }, + { + description: 'not equal arrays (different length)', + value1: [1, 2, 3], + value2: [1, 2], + equal: false + }, + { + description: 'equal arrays of objects', + value1: [{ a: 'a' }, { b: 'b' }], + value2: [{ a: 'a' }, { b: 'b' }], + equal: true + }, + { + description: 'not equal arrays of objects', + value1: [{ a: 'a' }, { b: 'b' }], + value2: [{ a: 'a' }, { b: 'c' }], + equal: false + }, + { + description: 'pseudo array and equivalent array are not equal', + value1: { 0: 0, 1: 1, length: 2 }, + value2: [0, 1], + equal: false + } + ] + }, + { + description: 'Date objects', + tests: [ + { + description: 'equal date objects', + value1: new Date('2017-06-16T21:36:48.362Z'), + value2: new Date('2017-06-16T21:36:48.362Z'), + equal: true + }, + { + description: 'not equal date objects', + value1: new Date('2017-06-16T21:36:48.362Z'), + value2: new Date('2017-01-01T00:00:00.000Z'), + equal: false + }, + { + description: 'date and string are not equal', + value1: new Date('2017-06-16T21:36:48.362Z'), + value2: '2017-06-16T21:36:48.362Z', + equal: false + }, + { + description: 'date and object are not equal', + value1: new Date('2017-06-16T21:36:48.362Z'), + value2: {}, + equal: false + } + ] + }, + { + description: 'RegExp objects', + tests: [ + { + description: 'equal RegExp objects', + value1: /foo/, + value2: /foo/, + equal: true + }, + { + description: 'not equal RegExp objects (different pattern)', + value1: /foo/, + value2: /bar/, + equal: false + }, + { + description: 'not equal RegExp objects (different flags)', + value1: /foo/, + value2: /foo/i, + equal: false + }, + { + description: 'RegExp and string are not equal', + value1: /foo/, + value2: 'foo', + equal: false + }, + { + description: 'RegExp and object are not equal', + value1: /foo/, + value2: {}, + equal: false + } + ] + }, + // { + // description: 'functions', + // tests: [ + // { + // description: 'same function is equal', + // value1: func1, + // value2: func1, + // equal: true + // }, + // { + // description: 'different functions are not equal', + // value1: func1, + // value2: func2, + // equal: false + // } + // ] + // }, + { + description: 'sample objects', + tests: [ + { + description: 'big object', + value1: { + prop1: 'value1', + prop2: 'value2', + prop3: 'value3', + prop4: { + subProp1: 'sub value1', + subProp2: { + subSubProp1: 'sub sub value1', + subSubProp2: [1, 2, { prop2: 1, prop: 2 }, 4, 5] + } + }, + prop5: 1000, + prop6: new Date(2016, 2, 10) + }, + value2: { + prop5: 1000, + prop3: 'value3', + prop1: 'value1', + prop2: 'value2', + prop6: new Date('2016/03/10'), + prop4: { + subProp2: { + subSubProp1: 'sub sub value1', + subSubProp2: [1, 2, { prop2: 1, prop: 2 }, 4, 5] + }, + subProp1: 'sub value1' + } + }, + equal: true + } + ] + } +] + +// function func1 () {} +// function func2 () {} From 2c7259937d735020c34140ac59ab127724339be0 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 24 Jan 2023 10:37:14 +0100 Subject: [PATCH 4/9] reduce complexity --- index.js | 54 ++++++------------------------------------------------ 1 file changed, 6 insertions(+), 48 deletions(-) diff --git a/index.js b/index.js index 683d5137..7694b9d8 100644 --- a/index.js +++ b/index.js @@ -790,55 +790,13 @@ function buildConstSerializer (location, input) { if (schemaRef.startsWith(rootSchemaId)) { schemaRef = schemaRef.replace(rootSchemaId, '') } - let code = '' - - switch (typeof schema.const) { - case 'bigint': - code += ` - if (${input} === ${schema.const}n) { - json += '${Number(schema.const)}' - } else { - throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) - }` - break - case 'number': - case 'boolean': - code += ` - if (${input} === ${schema.const}) { - json += ${JSON.stringify(JSON.stringify(schema.const))} - } else { - throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) - }` - break - case 'object': - if (schema.const === null) { - code += ` - if (${input} === null) { - json += 'null' - } else { - throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) - }` - } else { - code += ` - if (${constValidator(schema.const, input, 'integration')}) { - json += ${JSON.stringify(JSON.stringify(schema.const))} - } else { - throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) - }` - } - break - case 'string': - case 'undefined': - code += ` - if (${input} === '${schema.const}') { - json += ${JSON.stringify(JSON.stringify(schema.const))} - } else { - throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) - }` - break + return ` + if (${constValidator(schema.const, input, 'integration')}) { + json += ${JSON.stringify(JSON.stringify(schema.const))} + } else { + throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) } - - return code + ` } function buildValue (location, input) { From 911397a075d3ff059dc9379ab8b3d5117919d3cc Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 24 Jan 2023 11:32:01 +0100 Subject: [PATCH 5/9] encapsulate keyword const --- index.js | 23 ++++-------- lib/{const-validator.js => keywords/const.js} | 20 +++++++++-- .../const.test.js} | 36 +++++++++---------- 3 files changed, 42 insertions(+), 37 deletions(-) rename lib/{const-validator.js => keywords/const.js} (86%) rename test/{const-validator.test.js => keywords/const.test.js} (72%) diff --git a/index.js b/index.js index 7694b9d8..e762f6b9 100644 --- a/index.js +++ b/index.js @@ -11,7 +11,11 @@ const Serializer = require('./lib/serializer') const Validator = require('./lib/validator') const RefResolver = require('./lib/ref-resolver') const Location = require('./lib/location') -const constValidator = require('./lib/const-validator') + +/** + * Keywords + */ +const constKeyword = require('./lib/keywords/const').keyword let largeArraySize = 2e4 let largeArrayMechanism = 'default' @@ -784,21 +788,6 @@ function buildSingleTypeSerializer (location, input) { } } -function buildConstSerializer (location, input) { - const schema = location.schema - let schemaRef = location.getSchemaRef() - if (schemaRef.startsWith(rootSchemaId)) { - schemaRef = schemaRef.replace(rootSchemaId, '') - } - return ` - if (${constValidator(schema.const, input, 'integration')}) { - json += ${JSON.stringify(JSON.stringify(schema.const))} - } else { - throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) - } - ` -} - function buildValue (location, input) { let schema = location.schema @@ -868,7 +857,7 @@ function buildValue (location, input) { } if (schema.const !== undefined) { - code += buildConstSerializer(location, input) + code += constKeyword(location, rootSchemaId, input) } else if (Array.isArray(type)) { code += buildMultiTypeSerializer(location, input) } else { diff --git a/lib/const-validator.js b/lib/keywords/const.js similarity index 86% rename from lib/const-validator.js rename to lib/keywords/const.js index f8baee2e..b1dc47d6 100644 --- a/lib/const-validator.js +++ b/lib/keywords/const.js @@ -1,6 +1,6 @@ 'use strict' -function constValidator (input, accessPath = 'value', mode = 'function') { +function validator (input, accessPath = 'value', mode = 'function') { if (mode === 'integration') { return `${_const(input, accessPath)} true\n` } else { @@ -117,4 +117,20 @@ function _constObject (input, accessPath) { } } -module.exports = constValidator +function keyword (location, rootSchemaId, input) { + const schema = location.schema + let schemaRef = location.getSchemaRef() + if (schemaRef.startsWith(rootSchemaId)) { + schemaRef = schemaRef.replace(rootSchemaId, '') + } + return ` + if (${validator(schema.const, input, 'integration')}) { + json += ${JSON.stringify(JSON.stringify(schema.const))} + } else { + throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) + } + ` +} + +module.exports.validator = validator +module.exports.keyword = keyword diff --git a/test/const-validator.test.js b/test/keywords/const.test.js similarity index 72% rename from test/const-validator.test.js rename to test/keywords/const.test.js index 8ec2db65..27422717 100644 --- a/test/const-validator.test.js +++ b/test/keywords/const.test.js @@ -1,12 +1,12 @@ 'use strict' const test = require('tap').test -const constValidator = require('../lib/const-validator') +const validator = require('../../lib/keywords/const').validator test('string', (t) => { t.plan(2) - const validateConst = constValidator('stringValue') + const validateConst = validator('stringValue') t.equal(validateConst('stringValue'), true) t.equal(validateConst('b'), false) @@ -15,7 +15,7 @@ test('string', (t) => { test('number', (t) => { t.plan(2) - const validateConst = constValidator(42) + const validateConst = validator(42) t.equal(validateConst(42), true) t.equal(validateConst(43), false) @@ -24,7 +24,7 @@ test('number', (t) => { test('bigint', (t) => { t.plan(2) - const validateConst = constValidator(42n) + const validateConst = validator(42n) t.equal(validateConst(42n), true) t.equal(validateConst(43n), false) @@ -33,7 +33,7 @@ test('bigint', (t) => { test('boolean', (t) => { t.plan(2) - const validateConst = constValidator(true) + const validateConst = validator(true) t.equal(validateConst(true), true) t.equal(validateConst(false), false) @@ -42,7 +42,7 @@ test('boolean', (t) => { test('null', (t) => { t.plan(2) - const validateConst = constValidator(null) + const validateConst = validator(null) t.equal(validateConst(null), true) t.equal(validateConst('null'), false) @@ -51,7 +51,7 @@ test('null', (t) => { test('array, basic', (t) => { t.plan(3) - const validateConst = constValidator([1, 2, 3]) + const validateConst = validator([1, 2, 3]) t.equal(validateConst([1, 2, 3]), true) t.equal(validateConst([1, 2]), false) @@ -61,7 +61,7 @@ test('array, basic', (t) => { test('array, only numbers', (t) => { t.plan(2) - const validateConst = constValidator([1, 2, 3]) + const validateConst = validator([1, 2, 3]) t.equal(validateConst([1, 2, 3]), true) t.equal(validateConst([1, 2, 4]), false) @@ -70,7 +70,7 @@ test('array, only numbers', (t) => { test('array, sub arrays with numbers', (t) => { t.plan(3) - const validateConst = constValidator([[1, 2], 3]) + const validateConst = validator([[1, 2], 3]) t.equal(validateConst([[1, 2], 3]), true) t.equal(validateConst([[1, 2], 4]), false) @@ -80,7 +80,7 @@ test('array, sub arrays with numbers', (t) => { test('object, two properties', (t) => { t.plan(3) - const validateConst = constValidator({ a: 1, b: 2 }) + const validateConst = validator({ a: 1, b: 2 }) t.equal(validateConst({ a: 1, b: 2 }), true) t.equal(validateConst({ b: 2, a: 1 }), true) @@ -90,7 +90,7 @@ test('object, two properties', (t) => { test('NaN', (t) => { t.plan(2) - const validateConst = constValidator(NaN) + const validateConst = validator(NaN) t.equal(validateConst(NaN), true) t.equal(validateConst(Infinity), false) @@ -99,7 +99,7 @@ test('NaN', (t) => { test('Infinity', (t) => { t.plan(2) - const validateConst = constValidator(Infinity) + const validateConst = validator(Infinity) t.equal(validateConst(Infinity), true) t.equal(validateConst(-Infinity), false) @@ -108,7 +108,7 @@ test('Infinity', (t) => { test('Infinity', (t) => { t.plan(2) - const validateConst = constValidator(Infinity) + const validateConst = validator(Infinity) t.equal(validateConst(Infinity), true) t.equal(validateConst(-Infinity), false) @@ -117,7 +117,7 @@ test('Infinity', (t) => { test('RegExp', (t) => { t.plan(3) - const validateConst = constValidator(/a-z/g) + const validateConst = validator(/a-z/g) t.equal(validateConst(/a-z/g), true) t.equal(validateConst(/a-z/gm), false) @@ -127,13 +127,13 @@ test('RegExp', (t) => { test('Date', (t) => { t.plan(2) - const validateConst = constValidator(new Date(123)) + const validateConst = validator(new Date(123)) t.equal(validateConst(new Date(123)), true) t.equal(validateConst(new Date(124)), false) }) -const spec = require('./spec/fast-deep-equal.spec') +const spec = require('../spec/fast-deep-equal.spec') spec.forEach(function (suite) { test(suite.description, function (t) { @@ -141,11 +141,11 @@ spec.forEach(function (suite) { suite.tests.forEach(function (testCase) { t.test(testCase.description, function (t) { t.plan(1) - t.equal(constValidator(testCase.value1)(testCase.value2), testCase.equal) + t.equal(validator(testCase.value1)(testCase.value2), testCase.equal) }) t.test(testCase.description + ' (reverse arguments)', function (t) { t.plan(1) - t.equal(constValidator(testCase.value2)(testCase.value1), testCase.equal) + t.equal(validator(testCase.value2)(testCase.value1), testCase.equal) }) }) }) From 14731a6a17a76cbe0c2d9a362d9e40fcffb4deba Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 24 Jan 2023 15:29:51 +0100 Subject: [PATCH 6/9] fix merge error --- index.js | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/index.js b/index.js index e6aea399..d5b3755c 100644 --- a/index.js +++ b/index.js @@ -788,33 +788,6 @@ function buildSingleTypeSerializer (context, location, input) { } } -function buildConstSerializer (location, input) { - const schema = location.schema - const type = schema.type - - const hasNullType = Array.isArray(type) && type.includes('null') - - let code = '' - - if (hasNullType) { - code += ` - if (${input} === null) { - json += 'null' - } else { - ` - } - - code += `json += '${JSON.stringify(schema.const)}'` - - if (hasNullType) { - code += ` - } - ` - } - - return code -} - function buildValue (context, location, input) { let schema = location.schema @@ -884,7 +857,7 @@ function buildValue (context, location, input) { } if (schema.const !== undefined) { - code += constKeyword(location, rootSchemaId, input) + code += constKeyword(location, context.rootSchemaId, input) } else if (Array.isArray(type)) { code += buildMultiTypeSerializer(context, location, input) } else { From c0be2b1cc34c08571878697baad7c1a0a6cf7b6c Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 24 Jan 2023 17:03:52 +0100 Subject: [PATCH 7/9] small reorder --- index.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index d5b3755c..fa878fb3 100644 --- a/index.js +++ b/index.js @@ -326,27 +326,26 @@ function buildInnerObject (context, location) { if (obj[${sanitized}] !== undefined) { ${addComma} json += ${JSON.stringify(sanitized + ':')} + ${buildValue(context, propertyLocation, `obj[${sanitized}]`)} + } ` - code += buildValue(context, propertyLocation, `obj[${sanitized}]`) - - const defaultValue = propertyLocation.schema.default - if (defaultValue !== undefined) { + if (propertyLocation.schema.default !== undefined) { code += ` - } else { + else { ${addComma} - json += ${JSON.stringify(sanitized + ':' + JSON.stringify(defaultValue))} + json += ${JSON.stringify(sanitized + ':' + JSON.stringify(propertyLocation.schema.default))} + } ` - } else if (required.includes(key)) { + } + + if (propertyLocation.schema.default === undefined && required.includes(key)) { code += ` - } else { + else { throw new Error('${sanitized} is required!') + } ` } - - code += ` - } - ` }) for (const requiredProperty of required) { From 8c8115da02998c5e7dd6995cd7eff98e3b6705b8 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Thu, 23 Feb 2023 16:46:25 +0100 Subject: [PATCH 8/9] add strict mode --- index.js | 10 ++- lib/keywords/const.js | 21 +++-- test/const.test.js | 156 +++++++++++++++++++++++++++++++++----- test/if-then-else.test.js | 24 +++++- types/index.d.ts | 6 ++ types/index.test-d.ts | 7 +- 6 files changed, 191 insertions(+), 33 deletions(-) diff --git a/index.js b/index.js index a35dad7d..95911cd9 100644 --- a/index.js +++ b/index.js @@ -83,6 +83,7 @@ function build (schema, options) { options, refResolver: new RefResolver(), rootSchemaId: schema.$id || randomUUID(), + strict: false, validatorSchemasIds: new Set() } @@ -121,6 +122,13 @@ function build (schema, options) { } } + if (options.strict) { + if (typeof options.strict !== 'boolean') { + throw new Error('Strict-mode must be a boolean value') + } + context.strict = options.strict + } + const location = new Location(schema, context.rootSchemaId) const code = buildValue(context, location, 'input') @@ -855,7 +863,7 @@ function buildValue (context, location, input) { } if (schema.const !== undefined) { - code += constKeyword(location, context.rootSchemaId, input) + code += constKeyword(context, location, input) } else if (Array.isArray(type)) { code += buildMultiTypeSerializer(context, location, input) } else { diff --git a/lib/keywords/const.js b/lib/keywords/const.js index b1dc47d6..599bc0d8 100644 --- a/lib/keywords/const.js +++ b/lib/keywords/const.js @@ -117,19 +117,28 @@ function _constObject (input, accessPath) { } } -function keyword (location, rootSchemaId, input) { +function keyword (context, location, input) { const schema = location.schema let schemaRef = location.getSchemaRef() - if (schemaRef.startsWith(rootSchemaId)) { - schemaRef = schemaRef.replace(rootSchemaId, '') + if (schemaRef.startsWith(context.rootSchemaId)) { + schemaRef = schemaRef.replace(context.rootSchemaId, '') } - return ` + let code = ` if (${validator(schema.const, input, 'integration')}) { json += ${JSON.stringify(JSON.stringify(schema.const))} + }` + if (context.strict) { + code += ` else { + throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) + } + ` } else { - throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) + code += ` else { + json += JSON.stringify(${input}) + } + ` } - ` + return code } module.exports.validator = validator diff --git a/test/const.test.js b/test/const.test.js index a9fab963..fc241ed3 100644 --- a/test/const.test.js +++ b/test/const.test.js @@ -16,14 +16,14 @@ test('schema with const string', (t) => { const input = { foo: 'bar' } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) t.equal(stringify(input), '{"foo":"bar"}') t.ok(validate((input))) t.equal(stringify(input), JSON.stringify(input)) }) -test('schema with const string and different input', (t) => { +test('schema with const string and different input, strict: false', (t) => { t.plan(2) const schema = { @@ -35,13 +35,49 @@ test('schema with const string and different input', (t) => { const input = { foo: 'baz' } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) + + t.equal(stringify(input), JSON.stringify(input)) + t.not(validate(input)) +}) + +test('schema with const string and different input, strict: true', (t) => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { type: 'string', const: 'bar' } + } + } + + const input = { foo: 'baz' } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: true }) t.throws(() => stringify(input), new Error("The value of '#/properties/foo' does not match schema definition.")) t.not(validate(input)) }) -test('schema with const string and different type input', (t) => { +test('schema with const string and different type input, strict: false', (t) => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { const: 'bar' } + } + } + + const input = { foo: 1 } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) + + t.equal(stringify(input), JSON.stringify(input)) + t.not(validate(input)) +}) + +test('schema with const string and different type input, strict: true', (t) => { t.plan(2) const schema = { @@ -53,13 +89,13 @@ test('schema with const string and different type input', (t) => { const input = { foo: 1 } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: true }) t.throws(() => stringify(input), new Error("The value of '#/properties/foo' does not match schema definition.")) t.not(validate(input)) }) -test('schema with const string and no input', (t) => { +test('schema with const string and no input, strict: false', (t) => { t.plan(3) const schema = { @@ -71,7 +107,7 @@ test('schema with const string and no input', (t) => { const input = {} const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) t.equal(stringify(input), '{}') t.ok(validate((input))) @@ -90,14 +126,14 @@ test('schema with const number', (t) => { const input = { foo: 1 } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) t.equal(stringify(input), '{"foo":1}') t.ok(validate((input))) t.equal(stringify(input), JSON.stringify(input)) }) -test('schema with const number and different input', (t) => { +test('schema with const number and different input, strict: false', (t) => { t.plan(2) const schema = { @@ -109,7 +145,25 @@ test('schema with const number and different input', (t) => { const input = { foo: 2 } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) + + t.equal(stringify(input), JSON.stringify(input)) + t.not(validate(input)) +}) + +test('schema with const number and different input, strict: true', (t) => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { const: 1 } + } + } + + const input = { foo: 2 } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: true }) t.throws(() => stringify(input), new Error("The value of '#/properties/foo' does not match schema definition.")) t.not(validate(input)) @@ -127,7 +181,7 @@ test('schema with const bool', (t) => { const input = { foo: true } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) t.equal(stringify(input), '{"foo":true}') t.ok(validate((input))) @@ -146,7 +200,7 @@ test('schema with const number', (t) => { const input = { foo: 1 } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) t.equal(stringify(input), '{"foo":1}') t.ok(validate((input))) @@ -165,7 +219,7 @@ test('schema with const null', (t) => { const input = { foo: null } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) t.equal(stringify(input), '{"foo":null}') t.ok(validate((input))) @@ -184,7 +238,7 @@ test('schema with const array', (t) => { const input = { foo: [1, 2, 3] } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) t.equal(stringify(input), '{"foo":[1,2,3]}') t.ok(validate((input))) @@ -203,14 +257,14 @@ test('schema with const object', (t) => { const input = { foo: { bar: 'baz' } } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) t.equal(stringify(input), '{"foo":{"bar":"baz"}}') t.ok(validate((input))) t.equal(stringify(input), JSON.stringify(input)) }) -test('schema with const and null as type', (t) => { +test('schema with const and null as type, strict: false', (t) => { t.plan(5) const schema = { @@ -220,7 +274,27 @@ test('schema with const and null as type', (t) => { } } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) + + t.equal(stringify({ foo: null }), JSON.stringify({ foo: null })) + t.not(validate({ foo: null })) + + t.equal(stringify({ foo: 'baz' }), '{"foo":"baz"}') + t.ok(validate({ foo: 'baz' })) + t.equal(stringify({ foo: 'baz' }), JSON.stringify({ foo: 'baz' })) +}) + +test('schema with const and null as type, strict: true', (t) => { + t.plan(5) + + const schema = { + type: 'object', + properties: { + foo: { type: ['string', 'null'], const: 'baz' } + } + } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: true }) t.throws(() => stringify({ foo: null }), new Error("The value of '#/properties/foo' does not match schema definition.")) t.not(validate({ foo: null })) @@ -230,7 +304,7 @@ test('schema with const and null as type', (t) => { t.equal(stringify({ foo: 'baz' }), JSON.stringify({ foo: 'baz' })) }) -test('schema with const as nullable', (t) => { +test('schema with const as nullable, strict: false', (t) => { t.plan(5) const schema = { @@ -241,7 +315,28 @@ test('schema with const as nullable', (t) => { } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: false }) + + t.equal(stringify({ foo: null }), JSON.stringify({ foo: null })) + t.not(validate({ foo: null })) + + t.equal(stringify({ foo: 'baz' }), '{"foo":"baz"}') + t.ok(validate({ foo: 'baz' })) + t.equal(stringify({ foo: 'baz' }), JSON.stringify({ foo: 'baz' })) +}) + +test('schema with const as nullable, strict: true', (t) => { + t.plan(5) + + const schema = { + type: 'object', + properties: { + foo: { type: 'string', nullable: true, const: 'baz' } + } + } + + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: true }) t.throws(() => stringify({ foo: null }), new Error("The value of '#/properties/foo' does not match schema definition.")) t.not(validate({ foo: null })) @@ -251,7 +346,26 @@ test('schema with const as nullable', (t) => { t.equal(stringify({ foo: 'baz' }), JSON.stringify({ foo: 'baz' })) }) -test('schema with const and invalid object', (t) => { +test('schema with const and invalid object, strict: false', (t) => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { const: { foo: 'bar' } } + }, + required: ['foo'] + } + + const input = { foo: { foo: 'baz' } } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) + + t.equal(stringify(input), JSON.stringify({ foo: { foo: 'baz' } })) + t.not(validate(input)) +}) + +test('schema with const and invalid object, strict: true', (t) => { t.plan(2) const schema = { @@ -264,7 +378,7 @@ test('schema with const and invalid object', (t) => { const input = { foo: { foo: 'baz' } } const validate = ajv.compile(schema) - const stringify = build(schema) + const stringify = build(schema, { strict: true }) t.throws(() => stringify(input), new Error("The value of '#/properties/foo' does not match schema definition.")) t.not(validate(input)) diff --git a/test/if-then-else.test.js b/test/if-then-else.test.js index 62a65731..6c586792 100644 --- a/test/if-then-else.test.js +++ b/test/if-then-else.test.js @@ -55,7 +55,7 @@ const schema = { const nestedIfSchema = { type: 'object', - properties: { }, + properties: {}, if: { type: 'object', properties: { @@ -108,7 +108,7 @@ const nestedIfSchema = { const nestedElseSchema = { type: 'object', - properties: { }, + properties: {}, if: { type: 'object', properties: { @@ -388,7 +388,7 @@ t.test('if/else with string format', (t) => { t.equal(stringify('Invalid'), '"Invalid"') }) -t.test('if/else with const integers', (t) => { +t.test('if/else with const integers, strict: false', (t) => { t.plan(2) const schema = { @@ -398,7 +398,23 @@ t.test('if/else with const integers', (t) => { else: { const: 33 } } - const stringify = build(schema) + const stringify = build(schema, { strict: false }) + + t.equal(stringify(100.32), JSON.stringify(100.32)) + t.equal(stringify(10 - 12), JSON.stringify(10 - 12)) +}) + +t.test('if/else with const integers, strict: true', (t) => { + t.plan(2) + + const schema = { + type: 'number', + if: { type: 'number', minimum: 42 }, + then: { const: 66 }, + else: { const: 33 } + } + + const stringify = build(schema, { strict: true }) t.throws(() => stringify(100.32), new Error('The value of \'#/then\' does not match schema definition.')) t.throws(() => stringify(10 - 12), new Error('The value of \'#/else\' does not match schema definition.')) diff --git a/types/index.d.ts b/types/index.d.ts index f12458a1..67dc7f04 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -187,6 +187,12 @@ declare namespace build { * @default 'default' */ largeArrayMechanism?: 'default' | 'json-stringify' + + /** + * In Strict-mode fast-json-stringify will throw errors if the value is + * mismatching the schema. + */ + strict?: boolean } export const validLargeArrayMechanisms: string[] diff --git a/types/index.test-d.ts b/types/index.test-d.ts index 3c85e75c..2ba9cfaf 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -233,4 +233,9 @@ expectError(build({} as Schema, { largeArrayMechanism: 'invalid'} )) build({}, { largeArraySize: 2000 } ) build({}, { largeArraySize: '2e4' } ) build({}, { largeArraySize: 2n } ) -expectError(build({} as Schema, { largeArraySize: ['asdf']} )) \ No newline at end of file +expectError(build({} as Schema, { largeArraySize: ['asdf']} )) + +// strict +build({}, { strict: true }) +build({}, { strict: false }) +expectError(build(schema1, { strict: 1 })) From 655d3c5d3ab7c1c6da42635ebc7577d02f387da8 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Fri, 24 Feb 2023 13:46:07 +0100 Subject: [PATCH 9/9] no need to validate if strict is false --- lib/keywords/const.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/keywords/const.js b/lib/keywords/const.js index 599bc0d8..75c051e2 100644 --- a/lib/keywords/const.js +++ b/lib/keywords/const.js @@ -123,22 +123,18 @@ function keyword (context, location, input) { if (schemaRef.startsWith(context.rootSchemaId)) { schemaRef = schemaRef.replace(context.rootSchemaId, '') } - let code = ` - if (${validator(schema.const, input, 'integration')}) { - json += ${JSON.stringify(JSON.stringify(schema.const))} - }` + if (context.strict) { - code += ` else { + return ` + if (${validator(schema.const, input, 'integration')}) { + json += ${JSON.stringify(JSON.stringify(schema.const))} + } else { throw new Error(\`The value of '${schemaRef}' does not match schema definition.\`) } ` } else { - code += ` else { - json += JSON.stringify(${input}) - } - ` + return `json += JSON.stringify(${input})` } - return code } module.exports.validator = validator