Skip to content

Commit b2d55a0

Browse files
refactor: create separate schema for Ajv validation
1 parent c723e18 commit b2d55a0

File tree

7 files changed

+143
-152
lines changed

7 files changed

+143
-152
lines changed

ajv.js

-30
This file was deleted.

index.js

+41-107
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 }
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
// Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
@@ -364,7 +343,7 @@ function buildCode (location) {
364343

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

367-
const defaultValue = schema.properties[key].default
346+
const defaultValue = propertyLocation.schema.default
368347
if (defaultValue !== undefined) {
369348
code += `
370349
} else {
@@ -479,24 +458,14 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
479458
mergedSchema.anyOf.push(...allOfSchema.anyOf)
480459
}
481460

482-
if (allOfSchema.fjs_type !== undefined) {
483-
if (
484-
mergedSchema.fjs_type !== undefined &&
485-
mergedSchema.fjs_type !== allOfSchema.fjs_type
486-
) {
487-
throw new Error('allOf schemas have different fjs_type values')
488-
}
489-
mergedSchema.fjs_type = allOfSchema.fjs_type
490-
}
491-
492461
if (allOfSchema.allOf !== undefined) {
493462
mergeAllOfSchema(location, allOfSchema, mergedSchema)
494463
}
495464
}
496465
delete mergedSchema.allOf
497466

498467
mergedSchema.$id = `merged_${randomUUID()}`
499-
ajvInstance.addSchema(mergedSchema)
468+
validator.addSchema(mergedSchema)
500469
refResolver.addSchema(mergedSchema)
501470
location.schemaId = mergedSchema.$id
502471
location.jsonPointer = '#'
@@ -526,7 +495,7 @@ function addIfThenElse (location) {
526495
const ifSchemaRef = ifLocation.schemaId + ifLocation.jsonPointer
527496

528497
let code = `
529-
if (ajv.validate("${ifSchemaRef}", obj)) {
498+
if (validator.validate("${ifSchemaRef}", obj)) {
530499
`
531500

532501
const thenLocation = mergeLocation(location, 'then')
@@ -800,22 +769,26 @@ function buildValue (location, input) {
800769
location.schema = mergedSchema
801770
}
802771

803-
let type = schema.type
772+
const type = schema.type
804773
const nullable = schema.nullable === true
805774

806775
let code = ''
807776
let funcName
808777

809-
if (schema.fjs_type === 'string' && schema.format === undefined && Array.isArray(schema.type) && schema.type.length === 2) {
810-
type = 'string'
811-
}
812-
813778
switch (type) {
814779
case 'null':
815780
code += 'json += serializer.asNull()'
816781
break
817782
case 'string': {
818-
funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
783+
if (schema.format === 'date-time') {
784+
funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)'
785+
} else if (schema.format === 'date') {
786+
funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
787+
} else if (schema.format === 'time') {
788+
funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
789+
} else {
790+
funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
791+
}
819792
code += `json += ${funcName}(${input})`
820793
break
821794
}
@@ -832,15 +805,7 @@ function buildValue (location, input) {
832805
code += `json += ${funcName}(${input})`
833806
break
834807
case 'object':
835-
if (schema.format === 'date-time') {
836-
funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)'
837-
} else if (schema.format === 'date') {
838-
funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
839-
} else if (schema.format === 'time') {
840-
funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
841-
} else {
842-
funcName = buildObject(location)
843-
}
808+
funcName = buildObject(location)
844809
code += `json += ${funcName}(${input})`
845810
break
846811
case 'array':
@@ -858,7 +823,7 @@ function buildValue (location, input) {
858823
const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer
859824
const nestedResult = buildValue(optionLocation, input)
860825
code += `
861-
${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaRef}", ${input}))
826+
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
862827
${nestedResult}
863828
`
864829
}
@@ -872,7 +837,7 @@ function buildValue (location, input) {
872837
`
873838
} else if ('const' in schema) {
874839
code += `
875-
if(ajv.validate(${JSON.stringify(schema)}, ${input}))
840+
if(validator.validate(${JSON.stringify(schema)}, ${input}))
876841
json += '${JSON.stringify(schema.const)}'
877842
else
878843
throw new Error(\`Item $\{JSON.stringify(${input})} does not match schema definition.\`)
@@ -906,7 +871,7 @@ function buildValue (location, input) {
906871
switch (type) {
907872
case 'string': {
908873
code += `
909-
${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
874+
${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof RegExp || ${input} instanceof Date || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
910875
${nestedResult}
911876
`
912877
break
@@ -926,17 +891,10 @@ function buildValue (location, input) {
926891
break
927892
}
928893
case 'object': {
929-
if (schema.fjs_type) {
930-
code += `
931-
${statement}(${input} instanceof Date || ${input} === null)
932-
${nestedResult}
933-
`
934-
} else {
935-
code += `
936-
${statement}(typeof ${input} === "object" || ${input} === null)
937-
${nestedResult}
938-
`
939-
}
894+
code += `
895+
${statement}(typeof ${input} === "object" || ${input} === null)
896+
${nestedResult}
897+
`
940898
break
941899
}
942900
default: {
@@ -965,30 +923,6 @@ function buildValue (location, input) {
965923
return code
966924
}
967925

968-
// Ajv does not support js date format. In order to properly validate objects containing a date,
969-
// it needs to replace all occurrences of the string date format with a custom keyword fjs_type.
970-
// (see https://github.com/fastify/fast-json-stringify/pull/441)
971-
function extendDateTimeType (schema) {
972-
if (schema === null) return
973-
974-
if (schema.type === 'string') {
975-
schema.fjs_type = 'string'
976-
schema.type = ['string', 'object']
977-
} else if (
978-
Array.isArray(schema.type) &&
979-
schema.type.includes('string') &&
980-
!schema.type.includes('object')
981-
) {
982-
schema.fjs_type = 'string'
983-
schema.type.push('object')
984-
}
985-
for (const property in schema) {
986-
if (typeof schema[property] === 'object') {
987-
extendDateTimeType(schema[property])
988-
}
989-
}
990-
}
991-
992926
function isEmpty (schema) {
993927
// eslint-disable-next-line
994928
for (var key in schema) {
@@ -1003,9 +937,9 @@ module.exports = build
1003937

1004938
module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms
1005939

1006-
module.exports.restore = function ({ code, ajv }) {
940+
module.exports.restore = function ({ code, validator }) {
1007941
const serializer = new Serializer()
1008942
// eslint-disable-next-line
1009-
return (Function.apply(null, ['ajv', 'serializer', code])
1010-
.apply(null, [ajv, serializer]))
943+
return (Function.apply(null, ['validator', 'serializer', code])
944+
.apply(null, [validator, serializer]))
1011945
}

ref-resolver.js

+4-2
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

+9
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)