Skip to content

Commit 8997bf1

Browse files
refactor: create separate schema for Ajv validation (#504)
1 parent b0bb1a6 commit 8997bf1

File tree

7 files changed

+153
-150
lines changed

7 files changed

+153
-150
lines changed

ajv.js

Lines changed: 0 additions & 30 deletions
This file was deleted.

index.js

Lines changed: 47 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ const { randomUUID } = require('crypto')
99

1010
const validate = require('./schema-validator')
1111
const Serializer = require('./serializer')
12+
const Validator = require('./validator')
1213
const RefResolver = require('./ref-resolver')
13-
const buildAjv = require('./ajv')
1414

1515
let largeArraySize = 2e4
1616
let largeArrayMechanism = 'default'
@@ -75,51 +75,31 @@ const arrayItemsReferenceSerializersMap = new Map()
7575
const objectReferenceSerializersMap = new Map()
7676

7777
let rootSchemaId = null
78-
let ajvInstance = null
7978
let refResolver = null
79+
let validator = null
8080
let contextFunctions = null
8181

8282
function build (schema, options) {
83-
schema = clone(schema)
84-
8583
arrayItemsReferenceSerializersMap.clear()
8684
objectReferenceSerializersMap.clear()
8785

8886
contextFunctions = []
8987
options = options || {}
9088

91-
ajvInstance = buildAjv(options.ajv)
9289
refResolver = new RefResolver()
90+
validator = new Validator(options.ajv)
91+
9392
rootSchemaId = schema.$id || randomUUID()
9493

9594
isValidSchema(schema)
96-
extendDateTimeType(schema)
97-
ajvInstance.addSchema(schema, rootSchemaId)
95+
validator.addSchema(schema, rootSchemaId)
9896
refResolver.addSchema(schema, rootSchemaId)
9997

10098
if (options.schema) {
101-
const externalSchemas = clone(options.schema)
102-
103-
for (const key of Object.keys(externalSchemas)) {
104-
const externalSchema = externalSchemas[key]
105-
isValidSchema(externalSchema, key)
106-
extendDateTimeType(externalSchema)
107-
108-
let schemaKey = externalSchema.$id || key
109-
if (externalSchema.$id !== undefined && externalSchema.$id[0] === '#') {
110-
schemaKey = key + externalSchema.$id // relative URI
111-
}
112-
113-
if (refResolver.getSchema(schemaKey) === undefined) {
114-
refResolver.addSchema(externalSchema, key)
115-
}
116-
117-
if (
118-
ajvInstance.refs[schemaKey] === undefined &&
119-
ajvInstance.schemas[schemaKey] === undefined
120-
) {
121-
ajvInstance.addSchema(externalSchema, schemaKey)
122-
}
99+
for (const key of Object.keys(options.schema)) {
100+
isValidSchema(options.schema[key], key)
101+
validator.addSchema(options.schema[key], key)
102+
refResolver.addSchema(options.schema[key], key)
123103
}
124104
}
125105

@@ -160,28 +140,28 @@ function build (schema, options) {
160140
return main
161141
`
162142

163-
const dependenciesName = ['ajv', 'serializer', contextFunctionCode]
143+
const dependenciesName = ['validator', 'serializer', contextFunctionCode]
164144

165145
if (options.debugMode) {
166146
options.mode = 'debug'
167147
}
168148

169149
if (options.mode === 'debug') {
170-
return { code: dependenciesName.join('\n'), ajv: ajvInstance }
150+
return { code: dependenciesName.join('\n'), validator, ajv: validator.ajv }
171151
}
172152

173153
if (options.mode === 'standalone') {
174154
// lazy load
175155
const buildStandaloneCode = require('./standalone')
176-
return buildStandaloneCode(options, ajvInstance, contextFunctionCode)
156+
return buildStandaloneCode(options, validator, contextFunctionCode)
177157
}
178158

179159
/* eslint no-new-func: "off" */
180-
const contextFunc = new Function('ajv', 'serializer', contextFunctionCode)
181-
const stringifyFunc = contextFunc(ajvInstance, serializer)
160+
const contextFunc = new Function('validator', 'serializer', contextFunctionCode)
161+
const stringifyFunc = contextFunc(validator, serializer)
182162

183-
ajvInstance = null
184163
refResolver = null
164+
validator = null
185165
rootSchemaId = null
186166
contextFunctions = null
187167
arrayItemsReferenceSerializersMap.clear()
@@ -345,9 +325,8 @@ function buildCode (location) {
345325
const propertiesLocation = mergeLocation(location, 'properties')
346326
Object.keys(schema.properties || {}).forEach((key) => {
347327
let propertyLocation = mergeLocation(propertiesLocation, key)
348-
if (schema.properties[key].$ref) {
349-
propertyLocation = resolveRef(location, schema.properties[key].$ref)
350-
schema.properties[key] = propertyLocation.schema
328+
if (propertyLocation.$ref) {
329+
propertyLocation = resolveRef(location, propertyLocation.$ref)
351330
}
352331

353332
const sanitized = JSON.stringify(key)
@@ -364,8 +343,7 @@ function buildCode (location) {
364343

365344
code += buildValue(propertyLocation, `obj[${JSON.stringify(key)}]`)
366345

367-
const defaultValue = schema.properties[key].default
368-
346+
const defaultValue = propertyLocation.schema.default
369347
if (defaultValue !== undefined) {
370348
code += `
371349
} else {
@@ -480,24 +458,14 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
480458
mergedSchema.anyOf.push(...allOfSchema.anyOf)
481459
}
482460

483-
if (allOfSchema.fjs_type !== undefined) {
484-
if (
485-
mergedSchema.fjs_type !== undefined &&
486-
mergedSchema.fjs_type !== allOfSchema.fjs_type
487-
) {
488-
throw new Error('allOf schemas have different fjs_type values')
489-
}
490-
mergedSchema.fjs_type = allOfSchema.fjs_type
491-
}
492-
493461
if (allOfSchema.allOf !== undefined) {
494462
mergeAllOfSchema(location, allOfSchema, mergedSchema)
495463
}
496464
}
497465
delete mergedSchema.allOf
498466

499467
mergedSchema.$id = `merged_${randomUUID()}`
500-
ajvInstance.addSchema(mergedSchema)
468+
validator.addSchema(mergedSchema)
501469
refResolver.addSchema(mergedSchema)
502470
location.schemaId = mergedSchema.$id
503471
location.jsonPointer = '#'
@@ -527,7 +495,7 @@ function addIfThenElse (location) {
527495
const ifSchemaRef = ifLocation.schemaId + ifLocation.jsonPointer
528496

529497
let code = `
530-
if (ajv.validate("${ifSchemaRef}", obj)) {
498+
if (validator.validate("${ifSchemaRef}", obj)) {
531499
`
532500

533501
const thenLocation = mergeLocation(location, 'then')
@@ -801,16 +769,12 @@ function buildValue (location, input) {
801769
location.schema = mergedSchema
802770
}
803771

804-
let type = schema.type
772+
const type = schema.type
805773
const nullable = schema.nullable === true || (Array.isArray(type) && type.includes('null'))
806774

807775
let code = ''
808776
let funcName
809777

810-
if (schema.fjs_type === 'string' && schema.format === undefined && Array.isArray(schema.type) && schema.type.length === 2) {
811-
type = 'string'
812-
}
813-
814778
if ('const' in schema) {
815779
if (nullable) {
816780
code += `
@@ -827,7 +791,15 @@ function buildValue (location, input) {
827791
code += 'json += serializer.asNull()'
828792
break
829793
case 'string': {
830-
funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
794+
if (schema.format === 'date-time') {
795+
funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)'
796+
} else if (schema.format === 'date') {
797+
funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
798+
} else if (schema.format === 'time') {
799+
funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
800+
} else {
801+
funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
802+
}
831803
code += `json += ${funcName}(${input})`
832804
break
833805
}
@@ -844,15 +816,7 @@ function buildValue (location, input) {
844816
code += `json += ${funcName}(${input})`
845817
break
846818
case 'object':
847-
if (schema.format === 'date-time') {
848-
funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)'
849-
} else if (schema.format === 'date') {
850-
funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
851-
} else if (schema.format === 'time') {
852-
funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
853-
} else {
854-
funcName = buildObject(location)
855-
}
819+
funcName = buildObject(location)
856820
code += `json += ${funcName}(${input})`
857821
break
858822
case 'array':
@@ -870,7 +834,7 @@ function buildValue (location, input) {
870834
const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer
871835
const nestedResult = buildValue(optionLocation, input)
872836
code += `
873-
${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaRef}", ${input}))
837+
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
874838
${nestedResult}
875839
`
876840
}
@@ -882,6 +846,13 @@ function buildValue (location, input) {
882846
code += `
883847
json += JSON.stringify(${input})
884848
`
849+
} else if ('const' in schema) {
850+
code += `
851+
if(validator.validate(${JSON.stringify(schema)}, ${input}))
852+
json += '${JSON.stringify(schema.const)}'
853+
else
854+
throw new Error(\`Item $\{JSON.stringify(${input})} does not match schema definition.\`)
855+
`
885856
} else if (schema.type === undefined) {
886857
code += `
887858
json += JSON.stringify(${input})
@@ -914,6 +885,7 @@ function buildValue (location, input) {
914885
${statement}(
915886
typeof ${input} === "string" ||
916887
${input} === null ||
888+
${input} instanceof Date ||
917889
${input} instanceof RegExp ||
918890
(
919891
typeof ${input} === "object" &&
@@ -941,17 +913,10 @@ function buildValue (location, input) {
941913
break
942914
}
943915
case 'object': {
944-
if (schema.fjs_type) {
945-
code += `
946-
${statement}(${input} instanceof Date || ${input} === null)
947-
${nestedResult}
948-
`
949-
} else {
950-
code += `
951-
${statement}(typeof ${input} === "object" || ${input} === null)
952-
${nestedResult}
953-
`
954-
}
916+
code += `
917+
${statement}(typeof ${input} === "object" || ${input} === null)
918+
${nestedResult}
919+
`
955920
break
956921
}
957922
default: {
@@ -980,30 +945,6 @@ function buildValue (location, input) {
980945
return code
981946
}
982947

983-
// Ajv does not support js date format. In order to properly validate objects containing a date,
984-
// it needs to replace all occurrences of the string date format with a custom keyword fjs_type.
985-
// (see https://github.com/fastify/fast-json-stringify/pull/441)
986-
function extendDateTimeType (schema) {
987-
if (schema === null) return
988-
989-
if (schema.type === 'string') {
990-
schema.fjs_type = 'string'
991-
schema.type = ['string', 'object']
992-
} else if (
993-
Array.isArray(schema.type) &&
994-
schema.type.includes('string') &&
995-
!schema.type.includes('object')
996-
) {
997-
schema.fjs_type = 'string'
998-
schema.type.push('object')
999-
}
1000-
for (const property in schema) {
1001-
if (typeof schema[property] === 'object') {
1002-
extendDateTimeType(schema[property])
1003-
}
1004-
}
1005-
}
1006-
1007948
function isEmpty (schema) {
1008949
// eslint-disable-next-line
1009950
for (var key in schema) {
@@ -1018,9 +959,9 @@ module.exports = build
1018959

1019960
module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms
1020961

1021-
module.exports.restore = function ({ code, ajv }) {
962+
module.exports.restore = function ({ code, validator }) {
1022963
const serializer = new Serializer()
1023964
// eslint-disable-next-line
1024-
return (Function.apply(null, ['ajv', 'serializer', code])
1025-
.apply(null, [ajv, serializer]))
965+
return (Function.apply(null, ['validator', 'serializer', code])
966+
.apply(null, [validator, serializer]))
1026967
}

ref-resolver.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ class RefResolver {
1111
if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {
1212
schemaId = schema.$id
1313
}
14-
this.insertSchemaBySchemaId(schema, schemaId)
15-
this.insertSchemaSubschemas(schema, schemaId)
14+
if (this.getSchema(schemaId) === undefined) {
15+
this.insertSchemaBySchemaId(schema, schemaId)
16+
this.insertSchemaSubschemas(schema, schemaId)
17+
}
1618
}
1719

1820
getSchema (schemaId, jsonPointer = '#') {

serializer.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ module.exports = class Serializer {
7474
if (date instanceof Date) {
7575
return '"' + date.toISOString() + '"'
7676
}
77+
if (typeof date === 'string') {
78+
return '"' + date + '"'
79+
}
7780
throw new Error(`The value "${date}" cannot be converted to a date-time.`)
7881
}
7982

@@ -86,6 +89,9 @@ module.exports = class Serializer {
8689
if (date instanceof Date) {
8790
return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"'
8891
}
92+
if (typeof date === 'string') {
93+
return '"' + date + '"'
94+
}
8995
throw new Error(`The value "${date}" cannot be converted to a date.`)
9096
}
9197

@@ -98,6 +104,9 @@ module.exports = class Serializer {
98104
if (date instanceof Date) {
99105
return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"'
100106
}
107+
if (typeof date === 'string') {
108+
return '"' + date + '"'
109+
}
101110
throw new Error(`The value "${date}" cannot be converted to a time.`)
102111
}
103112

0 commit comments

Comments
 (0)