diff --git a/index.js b/index.js index 694f7843..95911cd9 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,11 @@ const Validator = require('./lib/validator') const RefResolver = require('./lib/ref-resolver') const Location = require('./lib/location') +/** + * Keywords + */ +const constKeyword = require('./lib/keywords/const').keyword + let largeArraySize = 2e4 let largeArrayMechanism = 'default' @@ -78,6 +83,7 @@ function build (schema, options) { options, refResolver: new RefResolver(), rootSchemaId: schema.$id || randomUUID(), + strict: false, validatorSchemasIds: new Set() } @@ -116,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') @@ -321,27 +334,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) { @@ -782,33 +794,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 @@ -868,7 +853,7 @@ function buildValue (context, location, input) { return code } - const nullable = schema.nullable === true + const nullable = schema.nullable === true && !('const' in schema) if (nullable) { code += ` if (${input} === null) { @@ -878,7 +863,7 @@ function buildValue (context, location, input) { } if (schema.const !== undefined) { - code += buildConstSerializer(location, 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 new file mode 100644 index 00000000..75c051e2 --- /dev/null +++ b/lib/keywords/const.js @@ -0,0 +1,141 @@ +'use strict' + +function validator (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) + } +} + +function keyword (context, location, input) { + const schema = location.schema + let schemaRef = location.getSchemaRef() + if (schemaRef.startsWith(context.rootSchemaId)) { + schemaRef = schemaRef.replace(context.rootSchemaId, '') + } + + if (context.strict) { + 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 { + return `json += JSON.stringify(${input})` + } +} + +module.exports.validator = validator +module.exports.keyword = keyword diff --git a/test/const.test.js b/test/const.test.js index c0b0b0aa..fc241ed3 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,37 +14,52 @@ test('schema with const string', (t) => { } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: 'bar' - }) + const input = { foo: 'bar' } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - 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) => { +test('schema with const string and different input, strict: false', (t) => { t.plan(2) const schema = { type: 'object', properties: { - foo: { const: 'bar' } + foo: { type: 'string', const: 'bar' } } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: 'baz' - }) + const input = { foo: 'baz' } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - t.equal(output, '{"foo":"bar"}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), JSON.stringify(input)) + t.not(validate(input)) }) -test('schema with const string and different type input', (t) => { +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, strict: false', (t) => { t.plan(2) const schema = { @@ -54,17 +69,15 @@ test('schema with const string and different type input', (t) => { } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: 1 - }) + const input = { foo: 1 } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - t.equal(output, '{"foo":"bar"}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), JSON.stringify(input)) + t.not(validate(input)) }) -test('schema with const string and no input', (t) => { +test('schema with const string and different type input, strict: true', (t) => { t.plan(2) const schema = { @@ -74,15 +87,53 @@ test('schema with const string and no input', (t) => { } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({}) + const input = { foo: 1 } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: true }) - t.equal(output, '{}') - 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, strict: false', (t) => { + t.plan(3) + + const schema = { + type: 'object', + properties: { + foo: { const: 'bar' } + } + } + + const input = {} + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) + + t.equal(stringify(input), '{}') + t.ok(validate((input))) + t.equal(stringify(input), JSON.stringify(input)) }) test('schema with const number', (t) => { + t.plan(3) + + const schema = { + type: 'object', + properties: { + foo: { const: 1 } + } + } + + const input = { foo: 1 } + const validate = ajv.compile(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, strict: false', (t) => { t.plan(2) const schema = { @@ -92,17 +143,15 @@ test('schema with const number', (t) => { } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: 1 - }) + const input = { foo: 2 } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - t.equal(output, '{"foo":1}') - t.ok(validate(JSON.parse(output)), 'valid schema') + t.equal(stringify(input), JSON.stringify(input)) + t.not(validate(input)) }) -test('schema with const number and different input', (t) => { +test('schema with const number and different input, strict: true', (t) => { t.plan(2) const schema = { @@ -112,18 +161,16 @@ test('schema with const number and different input', (t) => { } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: 2 - }) + const input = { foo: 2 } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: true }) - 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 +179,17 @@ test('schema with const bool', (t) => { } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: true - }) + const input = { foo: true } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - 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 +198,17 @@ test('schema with const number', (t) => { } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: 1 - }) + const input = { foo: 1 } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - 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 +217,17 @@ test('schema with const null', (t) => { } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: null - }) + const input = { foo: null } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - 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 +236,17 @@ test('schema with const array', (t) => { } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: [1, 2, 3] - }) + const input = { foo: [1, 2, 3] } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - 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 +255,17 @@ test('schema with const object', (t) => { } } - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: { bar: 'baz' } - }) + const input = { foo: { bar: 'baz' } } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - 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) +test('schema with const and null as type, strict: false', (t) => { + t.plan(5) const schema = { type: 'object', @@ -231,48 +273,99 @@ test('schema with const and null as type', (t) => { foo: { type: ['string', 'null'], const: 'baz' } } } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({ - foo: null - }) + 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' })) +}) - t.equal(output, '{"foo":null}') - t.ok(validate(JSON.parse(output)), 'valid schema') +test('schema with const and null as type, strict: true', (t) => { + t.plan(5) - const output2 = stringify({ foo: 'baz' }) - t.equal(output2, '{"foo":"baz"}') - t.ok(validate(JSON.parse(output2)), 'valid schema') + 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 })) + + 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) +test('schema with const as nullable, strict: false', (t) => { + 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 stringify = build(schema) - const output = stringify({ - foo: null - }) + const validate = ajv.compile(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 })) + + 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, strict: false', (t) => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { const: { foo: 'bar' } } + }, + required: ['foo'] + } - t.equal(output, '{"foo":null}') - t.ok(validate(JSON.parse(output)), 'valid schema') + const input = { foo: { foo: 'baz' } } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: false }) - const output2 = stringify({ - foo: 'baz' - }) - t.equal(output2, '{"foo":"baz"}') - t.ok(validate(JSON.parse(output2)), 'valid schema') + t.equal(stringify(input), JSON.stringify({ foo: { foo: 'baz' } })) + t.not(validate(input)) }) -test('schema with const and invalid object', (t) => { +test('schema with const and invalid object, strict: true', (t) => { t.plan(2) const schema = { @@ -283,12 +376,10 @@ test('schema with const and invalid object', (t) => { required: ['foo'] } - const validate = validator(schema) - const stringify = build(schema) - const result = stringify({ - foo: { foo: 'baz' } - }) + const input = { foo: { foo: 'baz' } } + const validate = ajv.compile(schema) + const stringify = build(schema, { strict: true }) - 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)) }) diff --git a/test/if-then-else.test.js b/test/if-then-else.test.js index bab3be7c..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,10 +398,26 @@ 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.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/keywords/const.test.js b/test/keywords/const.test.js new file mode 100644 index 00000000..27422717 --- /dev/null +++ b/test/keywords/const.test.js @@ -0,0 +1,152 @@ +'use strict' + +const test = require('tap').test +const validator = require('../../lib/keywords/const').validator + +test('string', (t) => { + t.plan(2) + + const validateConst = validator('stringValue') + + t.equal(validateConst('stringValue'), true) + t.equal(validateConst('b'), false) +}) + +test('number', (t) => { + t.plan(2) + + const validateConst = validator(42) + + t.equal(validateConst(42), true) + t.equal(validateConst(43), false) +}) + +test('bigint', (t) => { + t.plan(2) + + const validateConst = validator(42n) + + t.equal(validateConst(42n), true) + t.equal(validateConst(43n), false) +}) + +test('boolean', (t) => { + t.plan(2) + + const validateConst = validator(true) + + t.equal(validateConst(true), true) + t.equal(validateConst(false), false) +}) + +test('null', (t) => { + t.plan(2) + + const validateConst = validator(null) + + t.equal(validateConst(null), true) + t.equal(validateConst('null'), false) +}) + +test('array, basic', (t) => { + t.plan(3) + + const validateConst = validator([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 = validator([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 = validator([[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 = validator({ 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 = validator(NaN) + + t.equal(validateConst(NaN), true) + t.equal(validateConst(Infinity), false) +}) + +test('Infinity', (t) => { + t.plan(2) + + const validateConst = validator(Infinity) + + t.equal(validateConst(Infinity), true) + t.equal(validateConst(-Infinity), false) +}) + +test('Infinity', (t) => { + t.plan(2) + + const validateConst = validator(Infinity) + + t.equal(validateConst(Infinity), true) + t.equal(validateConst(-Infinity), false) +}) + +test('RegExp', (t) => { + t.plan(3) + + const validateConst = validator(/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 = 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') + +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(validator(testCase.value1)(testCase.value2), testCase.equal) + }) + t.test(testCase.description + ' (reverse arguments)', function (t) { + t.plan(1) + t.equal(validator(testCase.value2)(testCase.value1), testCase.equal) + }) + }) + }) +}) 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 () {} 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 }))