From 68ff81cc29979413b1db7ed9b13bd5cbfac8db32 Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Tue, 16 Aug 2022 11:03:08 +0200 Subject: [PATCH 01/12] Improved performance for const schema --- benchmark/bench.js | 42 ++++++++++++++++++++++++++++++++ index.js | 5 ++++ test/const.test.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/benchmark/bench.js b/benchmark/bench.js index 4526f250..4bb58a72 100644 --- a/benchmark/bench.js +++ b/benchmark/bench.js @@ -213,6 +213,48 @@ const benchmarks = [ n5: 42, b5: true } + }, + { + name: 'object with const string property', + schema: { + type: 'object', + properties: { + a: { const: 'const string' } + } + }, + input: { a: 'const string' } + }, + { + name: 'object with const number property', + schema: { + type: 'object', + properties: { + a: { const: 1 } + } + }, + input: { a: 1 } + }, + { + name: 'object with const bool property', + schema: { + type: 'object', + properties: { + a: { const: true } + } + }, + input: { a: true } + }, + { + name: 'object with const object property', + schema: { + type: 'object', + properties: { + foo: { const: { bar: 'baz' } } + } + }, + input: { + foo: { bar: 'baz' } + } } ] diff --git a/index.js b/index.js index f26ee30b..bf04a0b7 100644 --- a/index.js +++ b/index.js @@ -240,6 +240,11 @@ function inferTypeByKeyword (schema) { for (var keyword of numberKeywords) { if (keyword in schema) return 'number' } + + if (schema.const && typeof schema.const !== 'object') { + return typeof schema.const + } + return schema.type } diff --git a/test/const.test.js b/test/const.test.js index 72def12f..c2d776b7 100644 --- a/test/const.test.js +++ b/test/const.test.js @@ -24,6 +24,66 @@ test('schema with const string', (t) => { t.ok(validate(JSON.parse(output)), 'valid schema') }) +test('schema with const bool', t => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { const: true } + } + } + + const validate = validator(schema) + const stringify = build(schema) + const output = stringify({ + foo: true + }) + + t.equal(output, '{"foo":true}') + t.ok(validate(JSON.parse(output)), 'valid schema') +}) + +test('schema with const number', t => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { const: 1 } + } + } + + const validate = validator(schema) + const stringify = build(schema) + const output = stringify({ + foo: 1 + }) + + t.equal(output, '{"foo":1}') + t.ok(validate(JSON.parse(output)), 'valid schema') +}) + +test('schema with const null', t => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { const: null } + } + } + + const validate = validator(schema) + const stringify = build(schema) + const output = stringify({ + foo: null + }) + + t.equal(output, '{"foo":null}') + t.ok(validate(JSON.parse(output)), 'valid schema') +}) + test('schema with const object', (t) => { t.plan(2) From 5fa6e25a685693873fbf8c533ac7a19ca906aa20 Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Tue, 16 Aug 2022 12:41:24 +0200 Subject: [PATCH 02/12] Improve const object schema performance --- index.js | 21 +++++++++++++++++++-- test/const.test.js | 23 ++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index bf04a0b7..a953208f 100644 --- a/index.js +++ b/index.js @@ -241,8 +241,16 @@ function inferTypeByKeyword (schema) { if (keyword in schema) return 'number' } - if (schema.const && typeof schema.const !== 'object') { - return typeof schema.const + if (schema.const) { + if (typeof schema.const !== 'object') { + return typeof schema.const + } else if (schema.const === null) { + return 'null' + } else if (Array.isArray(schema.const)) { + return 'array' + } else { + return 'object' + } } return schema.type @@ -838,6 +846,15 @@ function buildValue (location, input) { funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)' } else if (schema.format === 'time') { funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)' + } else if ('const' in schema) { + const stringifiedSchema = JSON.stringify(schema.const) + code += ` + if('${stringifiedSchema}' === JSON.stringify(${input})) + json += '${stringifiedSchema}' + else + throw new Error(\`Item $\{JSON.stringify(${input})} does not match schema definition.\`) + ` + break } else { funcName = buildObject(location) } diff --git a/test/const.test.js b/test/const.test.js index c2d776b7..16208526 100644 --- a/test/const.test.js +++ b/test/const.test.js @@ -84,6 +84,26 @@ test('schema with const null', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) +test('schema with const array', t => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { const: [1, 2, 3] } + } + } + + const validate = validator(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') +}) + test('schema with const object', (t) => { t.plan(2) @@ -117,9 +137,10 @@ test('schema with const and invalid object', (t) => { const stringify = build(schema) try { - stringify({ + const result = stringify({ foo: { foo: 'baz' } }) + console.log({ result }) } catch (err) { t.match(err.message, /^Item .* does not match schema definition/, 'Given object has invalid const value') t.ok(err) From ca55edf1581ac1d9e40530fb2d95b167638ae5ae Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Tue, 16 Aug 2022 13:49:33 +0200 Subject: [PATCH 03/12] Improve string and object performance --- benchmark/bench.js | 16 ++- index.js | 253 ++++++++++++++++++++++++++------------------- test/const.test.js | 53 +++++++++- 3 files changed, 210 insertions(+), 112 deletions(-) diff --git a/benchmark/bench.js b/benchmark/bench.js index 4bb58a72..17f2fe44 100644 --- a/benchmark/bench.js +++ b/benchmark/bench.js @@ -255,6 +255,18 @@ const benchmarks = [ input: { foo: { bar: 'baz' } } + }, + { + name: 'object with const null property', + schema: { + type: 'object', + properties: { + foo: { const: null } + } + }, + input: { + foo: null + } } ] @@ -264,10 +276,10 @@ async function runBenchmark (benchmark) { return new Promise((resolve, reject) => { let result = null worker.on('error', reject) - worker.on('message', (benchResult) => { + worker.on('message', benchResult => { result = benchResult }) - worker.on('exit', (code) => { + worker.on('exit', code => { if (code === 0) { resolve(result) } else { diff --git a/index.js b/index.js index a953208f..502d11e8 100644 --- a/index.js +++ b/index.js @@ -818,168 +818,205 @@ function buildValue (location, input) { type = 'string' } - switch (type) { - case 'null': - code += 'json += serializer.asNull()' - break - case 'string': { - funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)' - code += `json += ${funcName}(${input})` - break + if ('const' in schema) { + const stringifiedSchema = JSON.stringify(schema.const) + let compareFn = '' + switch (type) { + case 'boolean': + compareFn = `${schema.const} === ${input}` + break + case 'string': + compareFn = `'${schema.const}' === ${input}` + break + case 'number': + compareFn = `${schema.const} === ${input}` + break + case 'null': + compareFn = `${schema.const} === ${input}` + break + default: + compareFn = `'${stringifiedSchema}' === JSON.stringify(${input})` + break } - case 'integer': - funcName = nullable ? 'serializer.asIntegerNullable.bind(serializer)' : 'serializer.asInteger.bind(serializer)' - code += `json += ${funcName}(${input})` - break - case 'number': - funcName = nullable ? 'serializer.asNumberNullable.bind(serializer)' : 'serializer.asNumber.bind(serializer)' - code += `json += ${funcName}(${input})` - break - case 'boolean': - funcName = nullable ? 'serializer.asBooleanNullable.bind(serializer)' : 'serializer.asBoolean.bind(serializer)' - code += `json += ${funcName}(${input})` - break - case 'object': - if (schema.format === 'date-time') { - funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)' - } else if (schema.format === 'date') { - funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)' - } else if (schema.format === 'time') { - funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)' - } else if ('const' in schema) { - const stringifiedSchema = JSON.stringify(schema.const) - code += ` - if('${stringifiedSchema}' === JSON.stringify(${input})) + + code += ` + if(${compareFn}) json += '${stringifiedSchema}' else throw new Error(\`Item $\{JSON.stringify(${input})} does not match schema definition.\`) ` + } else switchTypeSchema() + + return code + + function switchTypeSchema () { + switch (type) { + case 'null': + code += 'json += serializer.asNull()' + break + case 'string': { + funcName = nullable + ? 'serializer.asStringNullable.bind(serializer)' + : 'serializer.asString.bind(serializer)' + code += `json += ${funcName}(${input})` break - } else { - funcName = buildObject(location) } - code += `json += ${funcName}(${input})` - break - case 'array': - funcName = buildArray(location) - code += `json += ${funcName}(${input})` - break - case undefined: - if (schema.anyOf || schema.oneOf) { - // beware: dereferenceOfRefs has side effects and changes schema.anyOf - const type = schema.anyOf ? 'anyOf' : 'oneOf' - const anyOfLocation = mergeLocation(location, type) - - for (let index = 0; index < location.schema[type].length; index++) { - const optionLocation = mergeLocation(anyOfLocation, index) - const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer - const nestedResult = buildValue(optionLocation, input) - code += ` + case 'integer': + funcName = nullable + ? 'serializer.asIntegerNullable.bind(serializer)' + : 'serializer.asInteger.bind(serializer)' + code += `json += ${funcName}(${input})` + break + case 'number': + funcName = nullable + ? 'serializer.asNumberNullable.bind(serializer)' + : 'serializer.asNumber.bind(serializer)' + code += `json += ${funcName}(${input})` + break + case 'boolean': + funcName = nullable + ? 'serializer.asBooleanNullable.bind(serializer)' + : 'serializer.asBoolean.bind(serializer)' + code += `json += ${funcName}(${input})` + break + case 'object': + if (schema.format === 'date-time') { + funcName = nullable + ? 'serializer.asDateTimeNullable.bind(serializer)' + : 'serializer.asDateTime.bind(serializer)' + } else if (schema.format === 'date') { + funcName = nullable + ? 'serializer.asDateNullable.bind(serializer)' + : 'serializer.asDate.bind(serializer)' + } else if (schema.format === 'time') { + funcName = nullable + ? 'serializer.asTimeNullable.bind(serializer)' + : 'serializer.asTime.bind(serializer)' + } else { + funcName = buildObject(location) + } + code += `json += ${funcName}(${input})` + break + case 'array': + funcName = buildArray(location) + code += `json += ${funcName}(${input})` + break + case undefined: + if (schema.anyOf || schema.oneOf) { + // beware: dereferenceOfRefs has side effects and changes schema.anyOf + const type = schema.anyOf ? 'anyOf' : 'oneOf' + const anyOfLocation = mergeLocation(location, type) + + for (let index = 0; index < location.schema[type].length; index++) { + const optionLocation = mergeLocation(anyOfLocation, index) + const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer + const nestedResult = buildValue(optionLocation, input) + code += ` ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaRef}", ${input})) ${nestedResult} ` - } + } - code += ` + code += ` else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`) ` - } else if (isEmpty(schema)) { - code += ` + } else if (isEmpty(schema)) { + code += ` json += JSON.stringify(${input}) ` - } else if ('const' in schema) { - code += ` + } else if ('const' in schema) { + code += ` if(ajv.validate(${JSON.stringify(schema)}, ${input})) json += '${JSON.stringify(schema.const)}' else throw new Error(\`Item $\{JSON.stringify(${input})} does not match schema definition.\`) ` - } else if (schema.type === undefined) { - code += ` + } else if (schema.type === undefined) { + code += ` json += JSON.stringify(${input}) ` - } else { - throw new Error(`${schema.type} unsupported`) - } - break - default: - if (Array.isArray(type)) { - let sortedTypes = type - const nullable = schema.nullable === true || type.includes('null') + } else { + throw new Error(`${schema.type} unsupported`) + } + break - if (nullable) { - sortedTypes = sortedTypes.filter(type => type !== 'null') - code += ` + default: + if (Array.isArray(type)) { + let sortedTypes = type + const nullable = schema.nullable === true || type.includes('null') + + if (nullable) { + sortedTypes = sortedTypes.filter(type => type !== 'null') + code += ` if (${input} === null) { json += null } else {` - } + } - const locationClone = clone(location) - sortedTypes.forEach((type, index) => { - const statement = index === 0 ? 'if' : 'else if' - locationClone.schema.type = type - const nestedResult = buildValue(locationClone, input) - switch (type) { - case 'string': { - code += ` + const locationClone = clone(location) + sortedTypes.forEach((type, index) => { + const statement = index === 0 ? 'if' : 'else if' + locationClone.schema.type = type + const nestedResult = buildValue(locationClone, input) + switch (type) { + case 'string': { + code += ` ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString"))) ${nestedResult} ` - break - } - case 'array': { - code += ` + break + } + case 'array': { + code += ` ${statement}(Array.isArray(${input})) ${nestedResult} ` - break - } - case 'integer': { - code += ` + break + } + case 'integer': { + code += ` ${statement}(Number.isInteger(${input}) || ${input} === null) ${nestedResult} ` - break - } - case 'object': { - if (schema.fjs_type) { - code += ` + break + } + case 'object': { + if (schema.fjs_type) { + code += ` ${statement}(${input} instanceof Date || ${input} === null) ${nestedResult} ` - } else { - code += ` + } else { + code += ` ${statement}(typeof ${input} === "object" || ${input} === null) ${nestedResult} ` + } + break } - break - } - default: { - code += ` + default: { + code += ` ${statement}(typeof ${input} === "${type}" || ${input} === null) ${nestedResult} ` - break + break + } } - } - }) - code += ` + }) + code += ` else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`) ` - if (nullable) { - code += ` + if (nullable) { + code += ` } ` + } + } else { + throw new Error(`${type} unsupported`) } - } else { - throw new Error(`${type} unsupported`) - } + } } - - return code } // Ajv does not support js date format. In order to properly validate objects containing a date, diff --git a/test/const.test.js b/test/const.test.js index 16208526..576cc5e0 100644 --- a/test/const.test.js +++ b/test/const.test.js @@ -137,12 +137,61 @@ test('schema with const and invalid object', (t) => { const stringify = build(schema) try { - const result = stringify({ + stringify({ foo: { foo: 'baz' } }) - console.log({ result }) } catch (err) { t.match(err.message, /^Item .* does not match schema definition/, 'Given object has invalid const value') t.ok(err) } }) + +test('schema with const and invalid number', t => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { const: 1 } + } + } + + const stringify = build(schema) + try { + stringify({ + foo: 2 + }) + } catch (err) { + t.match( + err.message, + /^Item .* does not match schema definition/, + 'Given object has invalid const value' + ) + t.ok(err) + } +}) + +test('schema with const and invalid string', t => { + t.plan(2) + + const schema = { + type: 'object', + properties: { + foo: { const: 'hello' } + } + } + + const stringify = build(schema) + try { + stringify({ + foo: 'hell' + }) + } catch (err) { + t.match( + err.message, + /^Item .* does not match schema definition/, + 'Given object has invalid const value' + ) + t.ok(err) + } +}) From 05c92cfa5ad57ee1edc4cebae563c921410f51e7 Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Thu, 25 Aug 2022 18:54:37 +0200 Subject: [PATCH 04/12] Restored example file --- example.js | 79 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/example.js b/example.js index e9108e66..889232d9 100644 --- a/example.js +++ b/example.js @@ -3,12 +3,79 @@ const stringify = fastJson({ title: 'Example Schema', type: 'object', properties: { - noRequired: { const: 'hello world' } + firstName: { + type: 'string' + }, + lastName: { + type: 'string' + }, + age: { + description: 'Age in years', + type: 'integer' + }, + now: { + type: 'string' + }, + birthdate: { + type: ['string'], + format: 'date-time' + }, + reg: { + type: 'string' + }, + obj: { + type: 'object', + properties: { + bool: { + type: 'boolean' + } + } + }, + arr: { + type: 'array', + items: { + type: 'object', + properties: { + str: { + type: 'string' + } + } + } + } }, - required: ['noRequired'] -}) - -const result = stringify({ + required: ['now'], + patternProperties: { + '.*foo$': { + type: 'string' + }, + test: { + type: 'number' + }, + date: { + type: 'string', + format: 'date-time' + } + }, + additionalProperties: { + type: 'string' + } }) -console.log({ result }) +console.log( + stringify({ + firstName: 'Matteo', + lastName: 'Collina', + age: 32, + now: new Date(), + reg: /"([^"]|\\")*"/, + foo: 'hello', + numfoo: 42, + test: 42, + strtest: '23', + arr: [{ str: 'stark' }, { str: 'lannister' }], + obj: { bool: true }, + notmatch: 'valar morghulis', + notmatchobj: { a: true }, + notmatchnum: 42 + }) +) From f73da5ad7d4d823c718fa6d633abe44fb32d8d70 Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Thu, 25 Aug 2022 19:11:55 +0200 Subject: [PATCH 05/12] Changed findIndex with indexOf --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index ad9218dd..e0c56c2e 100644 --- a/index.js +++ b/index.js @@ -359,7 +359,7 @@ function buildCode (location) { const sanitized = JSON.stringify(key) const asString = JSON.stringify(sanitized) - const isRequired = schema.required !== undefined && schema.required.findIndex(item => item === key) !== -1 + const isRequired = schema.required !== undefined && schema.required.indexOf(key) !== -1 const isConst = schema.properties[key].const !== undefined // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons, // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion. From e2c3bd6cf18e6e59d44b8e5f8838dc89e20c49dd Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Thu, 25 Aug 2022 20:47:04 +0200 Subject: [PATCH 06/12] Removed useless inference when schema is const --- index.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/index.js b/index.js index e0c56c2e..1dc3e879 100644 --- a/index.js +++ b/index.js @@ -241,16 +241,6 @@ function inferTypeByKeyword (schema) { if (keyword in schema) return 'number' } - if (schema.const) { - if (typeof schema.const !== 'object') { - return typeof schema.const - } else if (schema.const === null) { - return 'null' - } else if (Array.isArray(schema.const)) { - return 'array' - } - } - return schema.type } @@ -827,6 +817,7 @@ function buildValue (location, input) { code += ` json += '${stringifiedSchema}' ` + return code } else switchTypeSchema() return code From ca1357da5d4154526e88697daa2c3aaad5c0d29e Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Fri, 26 Aug 2022 16:50:58 +0200 Subject: [PATCH 07/12] Review requests - Removed switchTypeSchema - Use of variable isRequired where defined - Rebase to PR #510 - Remove use of default value for const --- index.js | 333 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 167 insertions(+), 166 deletions(-) diff --git a/index.js b/index.js index 1dc3e879..571df6cc 100644 --- a/index.js +++ b/index.js @@ -13,10 +13,7 @@ const buildAjv = require('./ajv') let largeArraySize = 2e4 let largeArrayMechanism = 'default' -const validLargeArrayMechanisms = [ - 'default', - 'json-stringify' -] +const validLargeArrayMechanisms = ['default', 'json-stringify'] const addComma = ` if (addComma) { @@ -34,7 +31,9 @@ function isValidSchema (schema, name) { name = '' } const first = validate.errors[0] - const err = new Error(`${name}schema is invalid: data${first.instancePath} ${first.message}`) + const err = new Error( + `${name}schema is invalid: data${first.instancePath} ${first.message}` + ) err.errors = isValidSchema.errors throw err } @@ -122,7 +121,9 @@ function build (schema, options) { if (options.rounding) { if (!['floor', 'ceil', 'round'].includes(options.rounding)) { - throw new Error(`Unsupported integer rounding method ${options.rounding}`) + throw new Error( + `Unsupported integer rounding method ${options.rounding}` + ) } } @@ -138,7 +139,9 @@ function build (schema, options) { if (!Number.isNaN(Number.parseInt(options.largeArraySize, 10))) { largeArraySize = options.largeArraySize } else { - throw new Error(`Unsupported large array size. Expected integer-like, got ${options.largeArraySize}`) + throw new Error( + `Unsupported large array size. Expected integer-like, got ${options.largeArraySize}` + ) } } @@ -205,11 +208,7 @@ const arrayKeywords = [ 'contains' ] -const stringKeywords = [ - 'maxLength', - 'minLength', - 'pattern' -] +const stringKeywords = ['maxLength', 'minLength', 'pattern'] const numberKeywords = [ 'multipleOf', @@ -254,8 +253,11 @@ function addPatternProperties (location) { if (properties[keys[i]]) continue ` - const patternPropertiesLocation = mergeLocation(location, 'patternProperties') - Object.keys(pp).forEach((regex) => { + const patternPropertiesLocation = mergeLocation( + location, + 'patternProperties' + ) + Object.keys(pp).forEach(regex => { let ppLocation = mergeLocation(patternPropertiesLocation, regex) if (pp[regex].$ref) { ppLocation = resolveRef(ppLocation, pp[regex].$ref) @@ -265,7 +267,11 @@ function addPatternProperties (location) { try { RegExp(regex) } catch (err) { - throw new Error(`${err.message}. Found at ${regex} matching ${JSON.stringify(pp[regex])}`) + throw new Error( + `${err.message}. Found at ${regex} matching ${JSON.stringify( + pp[regex] + )}` + ) } const valueCode = buildValue(ppLocation, 'obj[keys[i]]') @@ -340,19 +346,20 @@ function buildCode (location) { let code = '' const propertiesLocation = mergeLocation(location, 'properties') - Object.keys(schema.properties || {}).forEach((key) => { + Object.keys(schema.properties || {}).forEach(key => { let propertyLocation = mergeLocation(propertiesLocation, key) if (schema.properties[key].$ref) { propertyLocation = resolveRef(location, schema.properties[key].$ref) schema.properties[key] = propertyLocation.schema } + // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons, + // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion. + const sanitized = JSON.stringify(key) const asString = JSON.stringify(sanitized) const isRequired = schema.required !== undefined && schema.required.indexOf(key) !== -1 const isConst = schema.properties[key].const !== undefined - // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons, - // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion. code += ` if (obj[${sanitized}] !== undefined) { @@ -362,18 +369,25 @@ function buildCode (location) { code += buildValue(propertyLocation, `obj[${JSON.stringify(key)}]`) - let defaultValue = schema.properties[key].default - if (isRequired) { - defaultValue = defaultValue !== undefined ? defaultValue : schema.properties[key].const - } + const defaultValue = schema.properties[key].default + const constValue = schema.properties[key].const if (defaultValue !== undefined) { code += ` } else { ${addComma} - json += ${asString} + ':' + ${JSON.stringify(JSON.stringify(defaultValue))} + json += ${asString} + ':' + ${JSON.stringify( + JSON.stringify(defaultValue) + )} + ` + } else if (isRequired && isConst) { + code += ` + } else { + json += ${asString} + ':' + ${JSON.stringify( + JSON.stringify(constValue) + )} ` - } else if (required.includes(key) && !isConst) { + } else if (required.includes(key)) { code += ` } else { throw new Error('${sanitized} is required!') @@ -386,7 +400,7 @@ function buildCode (location) { }) for (const requiredProperty of required) { - if (schema.properties && schema.properties[requiredProperty] !== undefined) continue + if (schema.properties && schema.properties[requiredProperty] !== undefined) { continue } code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n` } @@ -450,14 +464,20 @@ function mergeAllOfSchema (location, schema, mergedSchema) { if (mergedSchema.additionalProperties === undefined) { mergedSchema.additionalProperties = {} } - Object.assign(mergedSchema.additionalProperties, allOfSchema.additionalProperties) + Object.assign( + mergedSchema.additionalProperties, + allOfSchema.additionalProperties + ) } if (allOfSchema.patternProperties !== undefined) { if (mergedSchema.patternProperties === undefined) { mergedSchema.patternProperties = {} } - Object.assign(mergedSchema.patternProperties, allOfSchema.patternProperties) + Object.assign( + mergedSchema.patternProperties, + allOfSchema.patternProperties + ) } if (allOfSchema.required !== undefined) { @@ -674,7 +694,9 @@ function buildArray (location) { if (largeArrayMechanism === 'json-stringify') { functionCode += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n` } else { - throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`) + throw new Error( + `Unsupported large array mechanism ${largeArrayMechanism}` + ) } } @@ -759,7 +781,7 @@ function buildArrayTypeCondition (type, accessor) { break default: if (Array.isArray(type)) { - const conditions = type.map((subType) => { + const conditions = type.map(subType => { return buildArrayTypeCondition(subType, accessor) }) condition = `(${conditions.join(' || ')})` @@ -807,189 +829,166 @@ function buildValue (location, input) { let code = '' let funcName - if (schema.fjs_type === 'string' && schema.format === undefined && Array.isArray(schema.type) && schema.type.length === 2) { + if ( + schema.fjs_type === 'string' && + schema.format === undefined && + Array.isArray(schema.type) && + schema.type.length === 2 + ) { type = 'string' } if ('const' in schema) { - const stringifiedSchema = JSON.stringify(schema.const) - - code += ` - json += '${stringifiedSchema}' - ` + code += `json += '${JSON.stringify(schema.const)}'` return code - } else switchTypeSchema() - - return code + } - function switchTypeSchema () { - switch (type) { - case 'null': - code += 'json += serializer.asNull()' - break - case 'string': { - funcName = nullable - ? 'serializer.asStringNullable.bind(serializer)' - : 'serializer.asString.bind(serializer)' - code += `json += ${funcName}(${input})` - break + switch (type) { + case 'null': + code += 'json += serializer.asNull()' + break + case 'string': { + funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)' + code += `json += ${funcName}(${input})` + break + } + case 'integer': + funcName = nullable ? 'serializer.asIntegerNullable.bind(serializer)' : 'serializer.asInteger.bind(serializer)' + code += `json += ${funcName}(${input})` + break + case 'number': + funcName = nullable ? 'serializer.asNumberNullable.bind(serializer)' : 'serializer.asNumber.bind(serializer)' + code += `json += ${funcName}(${input})` + break + case 'boolean': + funcName = nullable ? 'serializer.asBooleanNullable.bind(serializer)' : 'serializer.asBoolean.bind(serializer)' + code += `json += ${funcName}(${input})` + break + case 'object': + if (schema.format === 'date-time') { + funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)' + } else if (schema.format === 'date') { + funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)' + } else if (schema.format === 'time') { + funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)' + } else { + funcName = buildObject(location) } - case 'integer': - funcName = nullable - ? 'serializer.asIntegerNullable.bind(serializer)' - : 'serializer.asInteger.bind(serializer)' - code += `json += ${funcName}(${input})` - break - case 'number': - funcName = nullable - ? 'serializer.asNumberNullable.bind(serializer)' - : 'serializer.asNumber.bind(serializer)' - code += `json += ${funcName}(${input})` - break - case 'boolean': - funcName = nullable - ? 'serializer.asBooleanNullable.bind(serializer)' - : 'serializer.asBoolean.bind(serializer)' - code += `json += ${funcName}(${input})` - break - case 'object': - if (schema.format === 'date-time') { - funcName = nullable - ? 'serializer.asDateTimeNullable.bind(serializer)' - : 'serializer.asDateTime.bind(serializer)' - } else if (schema.format === 'date') { - funcName = nullable - ? 'serializer.asDateNullable.bind(serializer)' - : 'serializer.asDate.bind(serializer)' - } else if (schema.format === 'time') { - funcName = nullable - ? 'serializer.asTimeNullable.bind(serializer)' - : 'serializer.asTime.bind(serializer)' - } else { - funcName = buildObject(location) - } - code += `json += ${funcName}(${input})` - break - case 'array': - funcName = buildArray(location) - code += `json += ${funcName}(${input})` - break - case undefined: - if (schema.anyOf || schema.oneOf) { - // beware: dereferenceOfRefs has side effects and changes schema.anyOf - const type = schema.anyOf ? 'anyOf' : 'oneOf' - const anyOfLocation = mergeLocation(location, type) - - for (let index = 0; index < location.schema[type].length; index++) { - const optionLocation = mergeLocation(anyOfLocation, index) - const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer - const nestedResult = buildValue(optionLocation, input) - code += ` + code += `json += ${funcName}(${input})` + break + case 'array': + funcName = buildArray(location) + code += `json += ${funcName}(${input})` + break + case undefined: + if (schema.anyOf || schema.oneOf) { + // beware: dereferenceOfRefs has side effects and changes schema.anyOf + const type = schema.anyOf ? 'anyOf' : 'oneOf' + const anyOfLocation = mergeLocation(location, type) + + for (let index = 0; index < location.schema[type].length; index++) { + const optionLocation = mergeLocation(anyOfLocation, index) + const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer + const nestedResult = buildValue(optionLocation, input) + code += ` ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaRef}", ${input})) ${nestedResult} ` - } + } - code += ` + code += ` else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`) ` - } else if (isEmpty(schema)) { - code += ` + } else if (isEmpty(schema)) { + code += ` json += JSON.stringify(${input}) ` - } else if ('const' in schema) { - code += ` - if(ajv.validate(${JSON.stringify(schema)}, ${input})) - json += '${JSON.stringify(schema.const)}' - else - throw new Error(\`Item $\{JSON.stringify(${input})} does not match schema definition.\`) - ` - } else if (schema.type === undefined) { - code += ` + } else if (schema.type === undefined) { + code += ` json += JSON.stringify(${input}) ` - } else { - throw new Error(`${schema.type} unsupported`) - } - break - - default: - if (Array.isArray(type)) { - let sortedTypes = type - const nullable = schema.nullable === true || type.includes('null') + } else { + throw new Error(`${schema.type} unsupported`) + } + break + default: + if (Array.isArray(type)) { + let sortedTypes = type + const nullable = schema.nullable === true || type.includes('null') - if (nullable) { - sortedTypes = sortedTypes.filter(type => type !== 'null') - code += ` + if (nullable) { + sortedTypes = sortedTypes.filter(type => type !== 'null') + code += ` if (${input} === null) { json += null } else {` - } + } - const locationClone = clone(location) - sortedTypes.forEach((type, index) => { - const statement = index === 0 ? 'if' : 'else if' - locationClone.schema.type = type - const nestedResult = buildValue(locationClone, input) - switch (type) { - case 'string': { - code += ` + const locationClone = clone(location) + sortedTypes.forEach((type, index) => { + const statement = index === 0 ? 'if' : 'else if' + locationClone.schema.type = type + const nestedResult = buildValue(locationClone, input) + switch (type) { + case 'string': { + code += ` ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString"))) ${nestedResult} ` - break - } - case 'array': { - code += ` + break + } + case 'array': { + code += ` ${statement}(Array.isArray(${input})) ${nestedResult} ` - break - } - case 'integer': { - code += ` + break + } + case 'integer': { + code += ` ${statement}(Number.isInteger(${input}) || ${input} === null) ${nestedResult} ` - break - } - case 'object': { - if (schema.fjs_type) { - code += ` + break + } + case 'object': { + if (schema.fjs_type) { + code += ` ${statement}(${input} instanceof Date || ${input} === null) ${nestedResult} ` - } else { - code += ` + } else { + code += ` ${statement}(typeof ${input} === "object" || ${input} === null) ${nestedResult} ` - } - break } - default: { - code += ` + break + } + default: { + code += ` ${statement}(typeof ${input} === "${type}" || ${input} === null) ${nestedResult} ` - break - } + break } - }) - code += ` + } + }) + code += ` else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`) ` - if (nullable) { - code += ` + if (nullable) { + code += ` } ` - } - } else { - throw new Error(`${type} unsupported`) } - } + } else { + throw new Error(`${type} unsupported`) + } } + + return code } // Ajv does not support js date format. In order to properly validate objects containing a date, @@ -1019,7 +1018,7 @@ function extendDateTimeType (schema) { function isEmpty (schema) { // eslint-disable-next-line for (var key in schema) { - if (Object.prototype.hasOwnProperty.call(schema, key) && schema[key] !== undefined) { + if (schema.hasOwnProperty(key) && schema[key] !== undefined) { return false } } @@ -1033,6 +1032,8 @@ module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms module.exports.restore = function ({ code, ajv }) { const serializer = new Serializer() // eslint-disable-next-line - return (Function.apply(null, ['ajv', 'serializer', code]) - .apply(null, [ajv, serializer])) + return Function.apply(null, ["ajv", "serializer", code]).apply(null, [ + ajv, + serializer + ]) } From 844eba5f4956230e1ec64bfaea18d0e595d066d4 Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Fri, 26 Aug 2022 16:55:41 +0200 Subject: [PATCH 08/12] Rebase to #510 (use hasOwnProperty from Object.prototype) --- index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 571df6cc..c1ccd7d5 100644 --- a/index.js +++ b/index.js @@ -932,7 +932,7 @@ function buildValue (location, input) { switch (type) { case 'string': { code += ` - ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString"))) + ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.prototype.hasOwnProperty.call(${input}, "toString"))) ${nestedResult} ` break @@ -1018,7 +1018,10 @@ function extendDateTimeType (schema) { function isEmpty (schema) { // eslint-disable-next-line for (var key in schema) { - if (schema.hasOwnProperty(key) && schema[key] !== undefined) { + if ( + Object.prototype.hasOwnProperty.call(schema, key) && + schema[key] !== undefined + ) { return false } } From 49301dc60a6fc4e66d7830b3b343aed95d78d901 Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Fri, 26 Aug 2022 17:01:12 +0200 Subject: [PATCH 09/12] Use isRequired when available --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index c1ccd7d5..2cf475c4 100644 --- a/index.js +++ b/index.js @@ -387,7 +387,7 @@ function buildCode (location) { JSON.stringify(constValue) )} ` - } else if (required.includes(key)) { + } else if (isRequired) { code += ` } else { throw new Error('${sanitized} is required!') From 0eb362a3eb41239d8188550d6ae6ba34dae258bc Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Sat, 27 Aug 2022 17:14:11 +0200 Subject: [PATCH 10/12] Restored FJS format style --- benchmark/bench.js | 4 +-- example.js | 34 ++++++++++---------- index.js | 80 ++++++++++++++++------------------------------ 3 files changed, 46 insertions(+), 72 deletions(-) diff --git a/benchmark/bench.js b/benchmark/bench.js index 17f2fe44..e655cdb9 100644 --- a/benchmark/bench.js +++ b/benchmark/bench.js @@ -276,10 +276,10 @@ async function runBenchmark (benchmark) { return new Promise((resolve, reject) => { let result = null worker.on('error', reject) - worker.on('message', benchResult => { + worker.on('message', (benchResult) => { result = benchResult }) - worker.on('exit', code => { + worker.on('exit', (code) => { if (code === 0) { resolve(result) } else { diff --git a/example.js b/example.js index 889232d9..15cb7d9a 100644 --- a/example.js +++ b/example.js @@ -61,21 +61,19 @@ const stringify = fastJson({ } }) -console.log( - stringify({ - firstName: 'Matteo', - lastName: 'Collina', - age: 32, - now: new Date(), - reg: /"([^"]|\\")*"/, - foo: 'hello', - numfoo: 42, - test: 42, - strtest: '23', - arr: [{ str: 'stark' }, { str: 'lannister' }], - obj: { bool: true }, - notmatch: 'valar morghulis', - notmatchobj: { a: true }, - notmatchnum: 42 - }) -) +console.log(stringify({ + firstName: 'Matteo', + lastName: 'Collina', + age: 32, + now: new Date(), + reg: /"([^"]|\\")*"/, + foo: 'hello', + numfoo: 42, + test: 42, + strtest: '23', + arr: [{ str: 'stark' }, { str: 'lannister' }], + obj: { bool: true }, + notmatch: 'valar morghulis', + notmatchobj: { a: true }, + notmatchnum: 42 +})) diff --git a/index.js b/index.js index 2cf475c4..c4cbef21 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,10 @@ const buildAjv = require('./ajv') let largeArraySize = 2e4 let largeArrayMechanism = 'default' -const validLargeArrayMechanisms = ['default', 'json-stringify'] +const validLargeArrayMechanisms = [ + 'default', + 'json-stringify' +] const addComma = ` if (addComma) { @@ -31,9 +34,7 @@ function isValidSchema (schema, name) { name = '' } const first = validate.errors[0] - const err = new Error( - `${name}schema is invalid: data${first.instancePath} ${first.message}` - ) + const err = new Error(`${name}schema is invalid: data${first.instancePath} ${first.message}`) err.errors = isValidSchema.errors throw err } @@ -121,9 +122,7 @@ function build (schema, options) { if (options.rounding) { if (!['floor', 'ceil', 'round'].includes(options.rounding)) { - throw new Error( - `Unsupported integer rounding method ${options.rounding}` - ) + throw new Error(`Unsupported integer rounding method ${options.rounding}`) } } @@ -139,9 +138,7 @@ function build (schema, options) { if (!Number.isNaN(Number.parseInt(options.largeArraySize, 10))) { largeArraySize = options.largeArraySize } else { - throw new Error( - `Unsupported large array size. Expected integer-like, got ${options.largeArraySize}` - ) + throw new Error(`Unsupported large array size. Expected integer-like, got ${options.largeArraySize}`) } } @@ -208,7 +205,11 @@ const arrayKeywords = [ 'contains' ] -const stringKeywords = ['maxLength', 'minLength', 'pattern'] +const stringKeywords = [ + 'maxLength', + 'minLength', + 'pattern' +] const numberKeywords = [ 'multipleOf', @@ -239,7 +240,6 @@ function inferTypeByKeyword (schema) { for (var keyword of numberKeywords) { if (keyword in schema) return 'number' } - return schema.type } @@ -253,11 +253,8 @@ function addPatternProperties (location) { if (properties[keys[i]]) continue ` - const patternPropertiesLocation = mergeLocation( - location, - 'patternProperties' - ) - Object.keys(pp).forEach(regex => { + const patternPropertiesLocation = mergeLocation(location, 'patternProperties') + Object.keys(pp).forEach((regex) => { let ppLocation = mergeLocation(patternPropertiesLocation, regex) if (pp[regex].$ref) { ppLocation = resolveRef(ppLocation, pp[regex].$ref) @@ -267,11 +264,7 @@ function addPatternProperties (location) { try { RegExp(regex) } catch (err) { - throw new Error( - `${err.message}. Found at ${regex} matching ${JSON.stringify( - pp[regex] - )}` - ) + throw new Error(`${err.message}. Found at ${regex} matching ${JSON.stringify(pp[regex])}`) } const valueCode = buildValue(ppLocation, 'obj[keys[i]]') @@ -346,18 +339,19 @@ function buildCode (location) { let code = '' const propertiesLocation = mergeLocation(location, 'properties') - Object.keys(schema.properties || {}).forEach(key => { + Object.keys(schema.properties || {}).forEach((key) => { let propertyLocation = mergeLocation(propertiesLocation, key) if (schema.properties[key].$ref) { propertyLocation = resolveRef(location, schema.properties[key].$ref) schema.properties[key] = propertyLocation.schema } + const sanitized = JSON.stringify(key) + const asString = JSON.stringify(sanitized) + // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons, // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion. - const sanitized = JSON.stringify(key) - const asString = JSON.stringify(sanitized) const isRequired = schema.required !== undefined && schema.required.indexOf(key) !== -1 const isConst = schema.properties[key].const !== undefined @@ -400,7 +394,7 @@ function buildCode (location) { }) for (const requiredProperty of required) { - if (schema.properties && schema.properties[requiredProperty] !== undefined) { continue } + if (schema.properties && schema.properties[requiredProperty] !== undefined) continue code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n` } @@ -464,20 +458,14 @@ function mergeAllOfSchema (location, schema, mergedSchema) { if (mergedSchema.additionalProperties === undefined) { mergedSchema.additionalProperties = {} } - Object.assign( - mergedSchema.additionalProperties, - allOfSchema.additionalProperties - ) + Object.assign(mergedSchema.additionalProperties, allOfSchema.additionalProperties) } if (allOfSchema.patternProperties !== undefined) { if (mergedSchema.patternProperties === undefined) { mergedSchema.patternProperties = {} } - Object.assign( - mergedSchema.patternProperties, - allOfSchema.patternProperties - ) + Object.assign(mergedSchema.patternProperties, allOfSchema.patternProperties) } if (allOfSchema.required !== undefined) { @@ -694,9 +682,7 @@ function buildArray (location) { if (largeArrayMechanism === 'json-stringify') { functionCode += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n` } else { - throw new Error( - `Unsupported large array mechanism ${largeArrayMechanism}` - ) + throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`) } } @@ -781,7 +767,7 @@ function buildArrayTypeCondition (type, accessor) { break default: if (Array.isArray(type)) { - const conditions = type.map(subType => { + const conditions = type.map((subType) => { return buildArrayTypeCondition(subType, accessor) }) condition = `(${conditions.join(' || ')})` @@ -829,12 +815,7 @@ function buildValue (location, input) { let code = '' let funcName - if ( - schema.fjs_type === 'string' && - schema.format === undefined && - Array.isArray(schema.type) && - schema.type.length === 2 - ) { + if (schema.fjs_type === 'string' && schema.format === undefined && Array.isArray(schema.type) && schema.type.length === 2) { type = 'string' } @@ -1018,10 +999,7 @@ function extendDateTimeType (schema) { function isEmpty (schema) { // eslint-disable-next-line for (var key in schema) { - if ( - Object.prototype.hasOwnProperty.call(schema, key) && - schema[key] !== undefined - ) { + if (Object.prototype.hasOwnProperty.call(schema, key) && schema[key] !== undefined) { return false } } @@ -1035,8 +1013,6 @@ module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms module.exports.restore = function ({ code, ajv }) { const serializer = new Serializer() // eslint-disable-next-line - return Function.apply(null, ["ajv", "serializer", code]).apply(null, [ - ajv, - serializer - ]) + return Function.apply(null, ["ajv", "serializer", code]) + .apply(null, [ajv, serializer]) } From b80bc4a5caded5a7f9092425f14b910a5131d535 Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Sat, 27 Aug 2022 17:22:44 +0200 Subject: [PATCH 11/12] Restore FJS format style --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index c4cbef21..52a3eea8 100644 --- a/index.js +++ b/index.js @@ -1013,6 +1013,6 @@ module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms module.exports.restore = function ({ code, ajv }) { const serializer = new Serializer() // eslint-disable-next-line - return Function.apply(null, ["ajv", "serializer", code]) + return Function.apply(null, ['ajv', 'serializer', code]) .apply(null, [ajv, serializer]) } From 3879db91f1c594bcc3554dbe1259cb217acdd889 Mon Sep 17 00:00:00 2001 From: Daniele Fedeli Date: Sun, 28 Aug 2022 10:05:29 +0200 Subject: [PATCH 12/12] Added support for nullable / type: [..., 'null'] --- index.js | 29 ++++++--------- test/const.test.js | 93 ++++++++++++++++++++++++++++++---------------- 2 files changed, 73 insertions(+), 49 deletions(-) diff --git a/index.js b/index.js index 52a3eea8..d372094b 100644 --- a/index.js +++ b/index.js @@ -352,9 +352,6 @@ function buildCode (location) { // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons, // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion. - const isRequired = schema.required !== undefined && schema.required.indexOf(key) !== -1 - const isConst = schema.properties[key].const !== undefined - code += ` if (obj[${sanitized}] !== undefined) { ${addComma} @@ -364,24 +361,14 @@ function buildCode (location) { code += buildValue(propertyLocation, `obj[${JSON.stringify(key)}]`) const defaultValue = schema.properties[key].default - const constValue = schema.properties[key].const if (defaultValue !== undefined) { code += ` } else { ${addComma} - json += ${asString} + ':' + ${JSON.stringify( - JSON.stringify(defaultValue) - )} - ` - } else if (isRequired && isConst) { - code += ` - } else { - json += ${asString} + ':' + ${JSON.stringify( - JSON.stringify(constValue) - )} + json += ${asString} + ':' + ${JSON.stringify(JSON.stringify(defaultValue))} ` - } else if (isRequired) { + } else if (required.includes(key)) { code += ` } else { throw new Error('${sanitized} is required!') @@ -810,7 +797,7 @@ function buildValue (location, input) { } let type = schema.type - const nullable = schema.nullable === true + const nullable = schema.nullable === true || (Array.isArray(type) && type.includes('null')) let code = '' let funcName @@ -820,6 +807,12 @@ function buildValue (location, input) { } if ('const' in schema) { + if (nullable) { + code += ` + json += ${input} === null ? 'null' : '${JSON.stringify(schema.const)}' + ` + return code + } code += `json += '${JSON.stringify(schema.const)}'` return code } @@ -1013,6 +1006,6 @@ module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms module.exports.restore = function ({ code, ajv }) { const serializer = new Serializer() // eslint-disable-next-line - return Function.apply(null, ['ajv', 'serializer', code]) - .apply(null, [ajv, serializer]) + return (Function.apply(null, ['ajv', 'serializer', code]) + .apply(null, [ajv, serializer])) } diff --git a/test/const.test.js b/test/const.test.js index 886db48a..c0b0b0aa 100644 --- a/test/const.test.js +++ b/test/const.test.js @@ -4,7 +4,7 @@ const test = require('tap').test const validator = require('is-my-json-valid') const build = require('..') -test('schema with const string', t => { +test('schema with const string', (t) => { t.plan(2) const schema = { @@ -24,7 +24,7 @@ test('schema with const string', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const string and different input', t => { +test('schema with const string and different input', (t) => { t.plan(2) const schema = { @@ -44,7 +44,7 @@ test('schema with const string and different input', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const string and different type input', t => { +test('schema with const string and different type input', (t) => { t.plan(2) const schema = { @@ -64,7 +64,7 @@ test('schema with const string and different type input', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const string and no input', t => { +test('schema with const string and no input', (t) => { t.plan(2) const schema = { @@ -82,26 +82,7 @@ test('schema with const string and no input', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const string and no input but required property', t => { - t.plan(2) - - const schema = { - type: 'object', - properties: { - foo: { const: 'bar' } - }, - required: ['foo'] - } - - const validate = validator(schema) - const stringify = build(schema) - const output = stringify({}) - - t.equal(output, '{"foo":"bar"}') - t.ok(validate(JSON.parse(output)), 'valid schema') -}) - -test('schema with const number', t => { +test('schema with const number', (t) => { t.plan(2) const schema = { @@ -121,7 +102,7 @@ test('schema with const number', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const number and different input', t => { +test('schema with const number and different input', (t) => { t.plan(2) const schema = { @@ -141,7 +122,7 @@ test('schema with const number and different input', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const bool', t => { +test('schema with const bool', (t) => { t.plan(2) const schema = { @@ -161,7 +142,7 @@ test('schema with const bool', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const number', t => { +test('schema with const number', (t) => { t.plan(2) const schema = { @@ -181,7 +162,7 @@ test('schema with const number', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const null', t => { +test('schema with const null', (t) => { t.plan(2) const schema = { @@ -201,7 +182,7 @@ test('schema with const null', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const array', t => { +test('schema with const array', (t) => { t.plan(2) const schema = { @@ -221,7 +202,7 @@ test('schema with const array', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const object', t => { +test('schema with const object', (t) => { t.plan(2) const schema = { @@ -241,7 +222,57 @@ test('schema with const object', t => { t.ok(validate(JSON.parse(output)), 'valid schema') }) -test('schema with const and invalid object', t => { +test('schema with const and null as type', (t) => { + t.plan(4) + + const schema = { + type: 'object', + properties: { + foo: { type: ['string', 'null'], const: 'baz' } + } + } + + const validate = validator(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') +}) + +test('schema with const as nullable', (t) => { + t.plan(4) + + const schema = { + type: 'object', + properties: { + foo: { nullable: true, const: 'baz' } + } + } + + const validate = validator(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') +}) + +test('schema with const and invalid object', (t) => { t.plan(2) const schema = {