fast-json-stringify 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -56,6 +56,173 @@ const schemaReferenceMap = new Map()
56
56
 
57
57
  let ajvInstance = null
58
58
 
59
+ class Serializer {
60
+ constructor (options = {}) {
61
+ switch (options.rounding) {
62
+ case 'floor':
63
+ this.parseInteger = Math.floor
64
+ break
65
+ case 'ceil':
66
+ this.parseInteger = Math.ceil
67
+ break
68
+ case 'round':
69
+ this.parseInteger = Math.round
70
+ break
71
+ default:
72
+ this.parseInteger = Math.trunc
73
+ break
74
+ }
75
+ }
76
+
77
+ asAny (i) {
78
+ return JSON.stringify(i)
79
+ }
80
+
81
+ asNull () {
82
+ return 'null'
83
+ }
84
+
85
+ asInteger (i) {
86
+ if (typeof i === 'bigint') {
87
+ return i.toString()
88
+ } else if (Number.isInteger(i)) {
89
+ return '' + i
90
+ } else {
91
+ /* eslint no-undef: "off" */
92
+ const integer = this.parseInteger(i)
93
+ if (Number.isNaN(integer)) {
94
+ throw new Error(`The value "${i}" cannot be converted to an integer.`)
95
+ } else {
96
+ return '' + integer
97
+ }
98
+ }
99
+ }
100
+
101
+ asIntegerNullable (i) {
102
+ return i === null ? 'null' : this.asInteger(i)
103
+ }
104
+
105
+ asNumber (i) {
106
+ const num = Number(i)
107
+ if (Number.isNaN(num)) {
108
+ throw new Error(`The value "${i}" cannot be converted to a number.`)
109
+ } else {
110
+ return '' + num
111
+ }
112
+ }
113
+
114
+ asNumberNullable (i) {
115
+ return i === null ? 'null' : this.asNumber(i)
116
+ }
117
+
118
+ asBoolean (bool) {
119
+ return bool && 'true' || 'false' // eslint-disable-line
120
+ }
121
+
122
+ asBooleanNullable (bool) {
123
+ return bool === null ? 'null' : this.asBoolean(bool)
124
+ }
125
+
126
+ asDatetime (date, skipQuotes) {
127
+ const quotes = skipQuotes === true ? '' : '"'
128
+ if (date instanceof Date) {
129
+ return quotes + date.toISOString() + quotes
130
+ }
131
+ return this.asString(date, skipQuotes)
132
+ }
133
+
134
+ asDatetimeNullable (date, skipQuotes) {
135
+ return date === null ? 'null' : this.asDatetime(date, skipQuotes)
136
+ }
137
+
138
+ asDate (date, skipQuotes) {
139
+ const quotes = skipQuotes === true ? '' : '"'
140
+ if (date instanceof Date) {
141
+ return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + quotes
142
+ }
143
+ return this.asString(date, skipQuotes)
144
+ }
145
+
146
+ asDateNullable (date, skipQuotes) {
147
+ return date === null ? 'null' : this.asDate(date, skipQuotes)
148
+ }
149
+
150
+ asTime (date, skipQuotes) {
151
+ const quotes = skipQuotes === true ? '' : '"'
152
+ if (date instanceof Date) {
153
+ return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + quotes
154
+ }
155
+ return this.asString(date, skipQuotes)
156
+ }
157
+
158
+ asTimeNullable (date, skipQuotes) {
159
+ return date === null ? 'null' : this.asTime(date, skipQuotes)
160
+ }
161
+
162
+ asString (str, skipQuotes) {
163
+ const quotes = skipQuotes === true ? '' : '"'
164
+ if (str instanceof Date) {
165
+ return quotes + str.toISOString() + quotes
166
+ } else if (str === null) {
167
+ return quotes + quotes
168
+ } else if (str instanceof RegExp) {
169
+ str = str.source
170
+ } else if (typeof str !== 'string') {
171
+ str = str.toString()
172
+ }
173
+ // If we skipQuotes it means that we are using it as test
174
+ // no need to test the string length for the render
175
+ if (skipQuotes) {
176
+ return str
177
+ }
178
+
179
+ if (str.length < 42) {
180
+ return this.asStringSmall(str)
181
+ } else {
182
+ return JSON.stringify(str)
183
+ }
184
+ }
185
+
186
+ asStringNullable (str) {
187
+ return str === null ? 'null' : this.asString(str)
188
+ }
189
+
190
+ // magically escape strings for json
191
+ // relying on their charCodeAt
192
+ // everything below 32 needs JSON.stringify()
193
+ // every string that contain surrogate needs JSON.stringify()
194
+ // 34 and 92 happens all the time, so we
195
+ // have a fast case for them
196
+ asStringSmall (str) {
197
+ const l = str.length
198
+ let result = ''
199
+ let last = 0
200
+ let found = false
201
+ let surrogateFound = false
202
+ let point = 255
203
+ // eslint-disable-next-line
204
+ for (var i = 0; i < l && point >= 32; i++) {
205
+ point = str.charCodeAt(i)
206
+ if (point >= 0xD800 && point <= 0xDFFF) {
207
+ // The current character is a surrogate.
208
+ surrogateFound = true
209
+ }
210
+ if (point === 34 || point === 92) {
211
+ result += str.slice(last, i) + '\\'
212
+ last = i
213
+ found = true
214
+ }
215
+ }
216
+
217
+ if (!found) {
218
+ result = str
219
+ } else {
220
+ result += str.slice(last)
221
+ }
222
+ return ((point < 32) || (surrogateFound === true)) ? JSON.stringify(str) : '"' + result + '"'
223
+ }
224
+ }
225
+
59
226
  function build (schema, options) {
60
227
  arrayItemsReferenceSerializersMap.clear()
61
228
  objectReferenceSerializersMap.clear()
@@ -66,6 +233,29 @@ function build (schema, options) {
66
233
  ajvInstance = new Ajv({ ...options.ajv, strictSchema: false, uriResolver: fastUri })
67
234
  ajvFormats(ajvInstance)
68
235
 
236
+ const validateDateTimeFormat = ajvFormats.get('date-time').validate
237
+ const validateDateFormat = ajvFormats.get('date').validate
238
+ const validateTimeFormat = ajvFormats.get('time').validate
239
+
240
+ ajvInstance.addKeyword({
241
+ keyword: 'fjs_date_type',
242
+ validate: (schema, date) => {
243
+ if (date instanceof Date) {
244
+ return true
245
+ }
246
+ if (schema === 'date-time') {
247
+ return validateDateTimeFormat(date)
248
+ }
249
+ if (schema === 'date') {
250
+ return validateDateFormat(date)
251
+ }
252
+ if (schema === 'time') {
253
+ return validateTimeFormat(date)
254
+ }
255
+ return false
256
+ }
257
+ })
258
+
69
259
  isValidSchema(schema)
70
260
  if (options.schema) {
71
261
  // eslint-disable-next-line
@@ -74,11 +264,8 @@ function build (schema, options) {
74
264
  }
75
265
  }
76
266
 
77
- let intParseFunctionName = 'trunc'
78
267
  if (options.rounding) {
79
- if (['floor', 'ceil', 'round'].includes(options.rounding)) {
80
- intParseFunctionName = options.rounding
81
- } else {
268
+ if (!['floor', 'ceil', 'round'].includes(options.rounding)) {
82
269
  throw new Error(`Unsupported integer rounding method ${options.rounding}`)
83
270
  }
84
271
  }
@@ -99,15 +286,7 @@ function build (schema, options) {
99
286
  }
100
287
  }
101
288
 
102
- /* eslint no-new-func: "off" */
103
- let code = `
104
- 'use strict'
105
- `
106
-
107
- code += `
108
- ${asFunctions}
109
- function parseInteger(int) { return Math.${intParseFunctionName}(int) }
110
- `
289
+ const serializer = new Serializer(options)
111
290
 
112
291
  let location = {
113
292
  schema,
@@ -124,62 +303,34 @@ function build (schema, options) {
124
303
  schema.type = inferTypeByKeyword(schema)
125
304
  }
126
305
 
127
- let main
128
-
129
- switch (schema.type) {
130
- case 'object':
131
- main = '$main'
132
- code = buildObject(location, code, main)
133
- break
134
- case 'string':
135
- main = schema.nullable ? '$asStringNullable' : getStringSerializer(schema.format)
136
- break
137
- case 'integer':
138
- main = schema.nullable ? '$asIntegerNullable' : '$asInteger'
139
- break
140
- case 'number':
141
- main = schema.nullable ? '$asNumberNullable' : '$asNumber'
142
- break
143
- case 'boolean':
144
- main = schema.nullable ? '$asBooleanNullable' : '$asBoolean'
145
- break
146
- case 'null':
147
- main = '$asNull'
148
- break
149
- case 'array':
150
- main = '$main'
151
- code = buildArray(location, code, main)
152
- schema = location.schema
153
- break
154
- case undefined:
155
- main = '$asAny'
156
- break
157
- default:
158
- throw new Error(`${schema.type} unsupported`)
159
- }
160
-
161
- code += `
162
- ;
163
- return ${main}
164
- `
165
- const dependencies = [ajvInstance]
166
- const dependenciesName = ['ajv']
167
- ajvInstance = null
306
+ const { code, laterCode } = buildValue('', 'main', 'input', location)
307
+ const contextFunctionCode = `
308
+ 'use strict'
309
+ function main (input) {
310
+ let json = ''
311
+ ${code}
312
+ return json
313
+ }
314
+ ${laterCode}
315
+ return main
316
+ `
168
317
 
169
- dependenciesName.push(code)
318
+ const dependenciesName = ['ajv', 'serializer', contextFunctionCode]
170
319
 
171
320
  if (options.debugMode) {
172
- return {
173
- code: dependenciesName.join('\n'),
174
- ajv: dependencies[0]
175
- }
321
+ return { code: dependenciesName.join('\n'), ajv: ajvInstance }
176
322
  }
177
323
 
324
+ /* eslint no-new-func: "off" */
325
+ const contextFunc = new Function('ajv', 'serializer', contextFunctionCode)
326
+ const stringifyFunc = contextFunc(ajvInstance, serializer)
327
+
328
+ ajvInstance = null
178
329
  arrayItemsReferenceSerializersMap.clear()
179
330
  objectReferenceSerializersMap.clear()
180
331
  schemaReferenceMap.clear()
181
332
 
182
- return (Function.apply(null, dependenciesName).apply(null, dependencies))
333
+ return stringifyFunc
183
334
  }
184
335
 
185
336
  const objectKeywords = [
@@ -239,171 +390,15 @@ function inferTypeByKeyword (schema) {
239
390
  return schema.type
240
391
  }
241
392
 
242
- const stringSerializerMap = {
243
- 'date-time': '$asDatetime',
244
- date: '$asDate',
245
- time: '$asTime'
246
- }
247
-
248
- function getStringSerializer (format) {
249
- return stringSerializerMap[format] ||
250
- '$asString'
251
- }
252
-
253
- function getTestSerializer (format) {
254
- return stringSerializerMap[format]
255
- }
256
-
257
- const asFunctions = `
258
- function $pad2Zeros (num) {
259
- const s = '00' + num
260
- return s[s.length - 2] + s[s.length - 1]
261
- }
262
-
263
- function $asAny (i) {
264
- return JSON.stringify(i)
265
- }
266
-
267
- function $asNull () {
268
- return 'null'
269
- }
270
-
271
- function $asInteger (i) {
272
- if (typeof i === 'bigint') {
273
- return i.toString()
274
- } else if (Number.isInteger(i)) {
275
- return $asNumber(i)
276
- } else {
277
- /* eslint no-undef: "off" */
278
- return $asNumber(parseInteger(i))
279
- }
280
- }
281
-
282
- function $asIntegerNullable (i) {
283
- return i === null ? null : $asInteger(i)
284
- }
285
-
286
- function $asNumber (i) {
287
- const num = Number(i)
288
- if (isNaN(num)) {
289
- return 'null'
290
- } else {
291
- return '' + num
393
+ function getStringSerializer (format, nullable) {
394
+ switch (format) {
395
+ case 'date-time': return nullable ? 'serializer.asDatetimeNullable.bind(serializer)' : 'serializer.asDatetime.bind(serializer)'
396
+ case 'date': return nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
397
+ case 'time': return nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
398
+ default: return nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
292
399
  }
293
400
  }
294
401
 
295
- function $asNumberNullable (i) {
296
- return i === null ? null : $asNumber(i)
297
- }
298
-
299
- function $asBoolean (bool) {
300
- return bool && 'true' || 'false' // eslint-disable-line
301
- }
302
-
303
- function $asBooleanNullable (bool) {
304
- return bool === null ? null : $asBoolean(bool)
305
- }
306
-
307
- function $asDatetime (date, skipQuotes) {
308
- const quotes = skipQuotes === true ? '' : '"'
309
- if (date instanceof Date) {
310
- return quotes + date.toISOString() + quotes
311
- } else if (date && typeof date.toISOString === 'function') {
312
- return quotes + date.toISOString() + quotes
313
- } else {
314
- return $asString(date, skipQuotes)
315
- }
316
- }
317
-
318
- function $asDate (date, skipQuotes) {
319
- const quotes = skipQuotes === true ? '' : '"'
320
- if (date instanceof Date) {
321
- return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000 )).toISOString().slice(0, 10) + quotes
322
- } else if (date && typeof date.format === 'function') {
323
- return quotes + date.format('YYYY-MM-DD') + quotes
324
- } else {
325
- return $asString(date, skipQuotes)
326
- }
327
- }
328
-
329
- function $asTime (date, skipQuotes) {
330
- const quotes = skipQuotes === true ? '' : '"'
331
- if (date instanceof Date) {
332
- const hour = new Intl.DateTimeFormat('en', { hour: 'numeric', hour12: false }).format(date)
333
- const minute = new Intl.DateTimeFormat('en', { minute: 'numeric' }).format(date)
334
- const second = new Intl.DateTimeFormat('en', { second: 'numeric' }).format(date)
335
- return quotes + $pad2Zeros(hour) + ':' + $pad2Zeros(minute) + ':' + $pad2Zeros(second) + quotes
336
- } else if (date && typeof date.format === 'function') {
337
- return quotes + date.format('HH:mm:ss') + quotes
338
- } else {
339
- return $asString(date, skipQuotes)
340
- }
341
- }
342
-
343
- function $asString (str, skipQuotes) {
344
- const quotes = skipQuotes === true ? '' : '"'
345
- if (str instanceof Date) {
346
- return quotes + str.toISOString() + quotes
347
- } else if (str === null) {
348
- return quotes + quotes
349
- } else if (str instanceof RegExp) {
350
- str = str.source
351
- } else if (typeof str !== 'string') {
352
- str = str.toString()
353
- }
354
- // If we skipQuotes it means that we are using it as test
355
- // no need to test the string length for the render
356
- if (skipQuotes) {
357
- return str
358
- }
359
-
360
- if (str.length < 42) {
361
- return $asStringSmall(str)
362
- } else {
363
- return JSON.stringify(str)
364
- }
365
- }
366
-
367
- function $asStringNullable (str) {
368
- return str === null ? null : $asString(str)
369
- }
370
-
371
- // magically escape strings for json
372
- // relying on their charCodeAt
373
- // everything below 32 needs JSON.stringify()
374
- // every string that contain surrogate needs JSON.stringify()
375
- // 34 and 92 happens all the time, so we
376
- // have a fast case for them
377
- function $asStringSmall (str) {
378
- const l = str.length
379
- let result = ''
380
- let last = 0
381
- let found = false
382
- let surrogateFound = false
383
- let point = 255
384
- // eslint-disable-next-line
385
- for (var i = 0; i < l && point >= 32; i++) {
386
- point = str.charCodeAt(i)
387
- if (point >= 0xD800 && point <= 0xDFFF) {
388
- // The current character is a surrogate.
389
- surrogateFound = true
390
- }
391
- if (point === 34 || point === 92) {
392
- result += str.slice(last, i) + '\\\\'
393
- last = i
394
- found = true
395
- }
396
- }
397
-
398
- if (!found) {
399
- result = str
400
- } else {
401
- result += str.slice(last)
402
- }
403
- return ((point < 32) || (surrogateFound === true)) ? JSON.stringify(str) : '"' + result + '"'
404
- }
405
- `
406
-
407
402
  function addPatternProperties (location) {
408
403
  const schema = location.schema
409
404
  const pp = schema.patternProperties
@@ -413,103 +408,56 @@ function addPatternProperties (location) {
413
408
  for (var i = 0; i < keys.length; i++) {
414
409
  if (properties[keys[i]]) continue
415
410
  `
411
+ let laterCode = ''
412
+
416
413
  Object.keys(pp).forEach((regex, index) => {
417
414
  let ppLocation = mergeLocation(location, { schema: pp[regex] })
418
415
  if (pp[regex].$ref) {
419
416
  ppLocation = refFinder(pp[regex].$ref, location)
420
417
  pp[regex] = ppLocation.schema
421
418
  }
422
- const type = pp[regex].type
423
- const format = pp[regex].format
424
- const stringSerializer = getStringSerializer(format)
419
+
425
420
  try {
426
421
  RegExp(regex)
427
422
  } catch (err) {
428
423
  throw new Error(`${err.message}. Found at ${regex} matching ${JSON.stringify(pp[regex])}`)
429
424
  }
430
425
 
431
- const ifPpKeyExists = `if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {`
432
-
433
- if (type === 'object') {
434
- code += `${buildObject(ppLocation, '', 'buildObjectPP' + index)}
435
- ${ifPpKeyExists}
436
- ${addComma}
437
- json += $asString(keys[i]) + ':' + buildObjectPP${index}(obj[keys[i]])
438
- `
439
- } else if (type === 'array') {
440
- code += `${buildArray(ppLocation, '', 'buildArrayPP' + index)}
441
- ${ifPpKeyExists}
442
- ${addComma}
443
- json += $asString(keys[i]) + ':' + buildArrayPP${index}(obj[keys[i]])
444
- `
445
- } else if (type === 'null') {
446
- code += `
447
- ${ifPpKeyExists}
448
- ${addComma}
449
- json += $asString(keys[i]) +':null'
450
- `
451
- } else if (type === 'string') {
452
- code += `
453
- ${ifPpKeyExists}
454
- ${addComma}
455
- json += $asString(keys[i]) + ':' + ${stringSerializer}(obj[keys[i]])
456
- `
457
- } else if (type === 'integer') {
458
- code += `
459
- ${ifPpKeyExists}
460
- ${addComma}
461
- json += $asString(keys[i]) + ':' + $asInteger(obj[keys[i]])
462
- `
463
- } else if (type === 'number') {
464
- code += `
465
- ${ifPpKeyExists}
466
- ${addComma}
467
- json += $asString(keys[i]) + ':' + $asNumber(obj[keys[i]])
468
- `
469
- } else if (type === 'boolean') {
470
- code += `
471
- ${ifPpKeyExists}
472
- ${addComma}
473
- json += $asString(keys[i]) + ':' + $asBoolean(obj[keys[i]])
474
- `
475
- } else if (type === undefined) {
476
- code += `
477
- ${ifPpKeyExists}
478
- ${addComma}
479
- json += $asString(keys[i]) + ':' + $asAny(obj[keys[i]])
480
- `
481
- } else {
482
- code += `
483
- ${ifPpKeyExists}
484
- throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ' + ${JSON.stringify(type)})
485
- `
486
- }
487
-
426
+ const valueCode = buildValue('', '', 'obj[keys[i]]', ppLocation)
427
+ laterCode += valueCode.laterCode
488
428
  code += `
489
- continue
490
- }
429
+ if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {
430
+ ${addComma}
431
+ json += serializer.asString(keys[i]) + ':'
432
+ ${valueCode.code}
433
+ continue
434
+ }
491
435
  `
492
436
  })
493
437
  if (schema.additionalProperties) {
494
- code += additionalProperty(location)
438
+ const additionalPropertyCode = additionalProperty(location)
439
+ code += additionalPropertyCode.code
440
+ laterCode += additionalPropertyCode.laterCode
495
441
  }
496
442
 
497
443
  code += `
498
444
  }
499
445
  `
500
- return code
446
+ return { code, laterCode }
501
447
  }
502
448
 
503
449
  function additionalProperty (location) {
504
450
  let ap = location.schema.additionalProperties
505
451
  let code = ''
506
452
  if (ap === true) {
507
- return `
453
+ code += `
508
454
  if (obj[keys[i]] !== undefined && typeof obj[keys[i]] !== 'function' && typeof obj[keys[i]] !== 'symbol') {
509
455
  ${addComma}
510
- json += $asString(keys[i]) + ':' + JSON.stringify(obj[keys[i]])
456
+ json += serializer.asString(keys[i]) + ':' + JSON.stringify(obj[keys[i]])
511
457
  }
512
458
  `
459
+
460
+ return { code, laterCode: '' }
513
461
  }
514
462
  let apLocation = mergeLocation(location, { schema: ap })
515
463
  if (ap.$ref) {
@@ -517,72 +465,29 @@ function additionalProperty (location) {
517
465
  ap = apLocation.schema
518
466
  }
519
467
 
520
- const type = ap.type
521
- const format = ap.format
522
- const stringSerializer = getStringSerializer(format)
523
- if (type === 'object') {
524
- code += `${buildObject(apLocation, '', 'buildObjectAP')}
525
- ${addComma}
526
- json += $asString(keys[i]) + ':' + buildObjectAP(obj[keys[i]])
527
- `
528
- } else if (type === 'array') {
529
- code += `${buildArray(apLocation, '', 'buildArrayAP')}
530
- ${addComma}
531
- json += $asString(keys[i]) + ':' + buildArrayAP(obj[keys[i]])
532
- `
533
- } else if (type === 'null') {
534
- code += `
535
- ${addComma}
536
- json += $asString(keys[i]) +':null'
537
- `
538
- } else if (type === 'string') {
539
- code += `
540
- ${addComma}
541
- json += $asString(keys[i]) + ':' + ${stringSerializer}(obj[keys[i]])
542
- `
543
- } else if (type === 'integer') {
544
- code += `
545
- var t = Number(obj[keys[i]])
546
- if (!isNaN(t)) {
547
- ${addComma}
548
- json += $asString(keys[i]) + ':' + t
549
- }
550
- `
551
- } else if (type === 'number') {
552
- code += `
553
- var t = Number(obj[keys[i]])
554
- if (!isNaN(t)) {
555
- ${addComma}
556
- json += $asString(keys[i]) + ':' + t
557
- }
558
- `
559
- } else if (type === 'boolean') {
560
- code += `
561
- ${addComma}
562
- json += $asString(keys[i]) + ':' + $asBoolean(obj[keys[i]])
563
- `
564
- } else if (type === undefined) {
565
- code += `
566
- ${addComma}
567
- json += $asString(keys[i]) + ':' + $asAny(obj[keys[i]])
568
- `
569
- } else {
570
- code += `
571
- throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ' + ${JSON.stringify(type)})
572
- `
573
- }
574
- return code
468
+ const valueCode = buildValue('', '', 'obj[keys[i]]', apLocation)
469
+
470
+ code += `
471
+ ${addComma}
472
+ json += serializer.asString(keys[i]) + ':'
473
+ ${valueCode.code}
474
+ `
475
+
476
+ return { code, laterCode: valueCode.laterCode }
575
477
  }
576
478
 
577
479
  function addAdditionalProperties (location) {
578
- return `
480
+ const additionalPropertyCode = additionalProperty(location)
481
+ const code = `
579
482
  var properties = ${JSON.stringify(location.schema.properties)} || {}
580
483
  var keys = Object.keys(obj)
581
484
  for (var i = 0; i < keys.length; i++) {
582
485
  if (properties[keys[i]]) continue
583
- ${additionalProperty(location)}
486
+ ${additionalPropertyCode.code}
584
487
  }
585
488
  `
489
+
490
+ return { code, laterCode: additionalPropertyCode.laterCode }
586
491
  }
587
492
 
588
493
  function idFinder (schema, searchedId) {
@@ -716,15 +621,15 @@ function refFinder (ref, location) {
716
621
  }
717
622
  }
718
623
 
719
- function buildCode (location, code, laterCode, name) {
624
+ function buildCode (location, code, laterCode, locationPath) {
720
625
  if (location.schema.$ref) {
721
626
  location = refFinder(location.schema.$ref, location)
722
627
  }
723
628
 
724
629
  const schema = location.schema
725
- let required = schema.required
630
+ const required = schema.required || []
726
631
 
727
- Object.keys(schema.properties || {}).forEach((key, i, a) => {
632
+ Object.keys(schema.properties || {}).forEach((key) => {
728
633
  let propertyLocation = mergeLocation(location, { schema: schema.properties[key] })
729
634
  if (schema.properties[key].$ref) {
730
635
  propertyLocation = refFinder(schema.properties[key].$ref, location)
@@ -734,61 +639,27 @@ function buildCode (location, code, laterCode, name) {
734
639
  // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
735
640
  // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
736
641
 
737
- const type = schema.properties[key].type
738
- const nullable = schema.properties[key].nullable
739
642
  const sanitized = JSON.stringify(key)
740
643
  const asString = JSON.stringify(sanitized)
741
644
 
742
- if (nullable) {
743
- code += `
744
- if (obj[${sanitized}] === null) {
745
- ${addComma}
746
- json += ${asString} + ':null'
747
- var rendered = true
748
- } else {
749
- `
750
- }
751
-
752
- if (type === 'number') {
753
- code += `
754
- var t = Number(obj[${sanitized}])
755
- if (!isNaN(t)) {
756
- ${addComma}
757
- json += ${asString} + ':' + t
758
- `
759
- } else if (type === 'integer') {
760
- code += `
761
- var rendered = false
762
- var t = $asInteger(obj[${sanitized}])
763
- if (!isNaN(t)) {
764
- ${addComma}
765
- json += ${asString} + ':' + t
766
- rendered = true
767
- }
768
- if (rendered) {
645
+ code += `
646
+ if (obj[${sanitized}] !== undefined) {
647
+ ${addComma}
648
+ json += ${asString} + ':'
769
649
  `
770
- } else {
771
- code += `
772
- if (obj[${sanitized}] !== undefined) {
773
- ${addComma}
774
- json += ${asString} + ':'
775
- `
776
650
 
777
- const result = nested(laterCode, name, key, mergeLocation(propertyLocation, { schema: schema.properties[key] }), undefined, false)
778
- code += result.code
779
- laterCode = result.laterCode
780
- }
651
+ const result = buildValue(laterCode, locationPath + key, `obj[${JSON.stringify(key)}]`, mergeLocation(propertyLocation, { schema: schema.properties[key] }))
652
+ code += result.code
653
+ laterCode = result.laterCode
781
654
 
782
655
  const defaultValue = schema.properties[key].default
783
656
  if (defaultValue !== undefined) {
784
- required = filterRequired(required, key)
785
657
  code += `
786
658
  } else {
787
659
  ${addComma}
788
660
  json += ${asString} + ':' + ${JSON.stringify(JSON.stringify(defaultValue))}
789
661
  `
790
- } else if (required && required.indexOf(key) !== -1) {
791
- required = filterRequired(required, key)
662
+ } else if (required.includes(key)) {
792
663
  code += `
793
664
  } else {
794
665
  throw new Error('${sanitized} is required!')
@@ -798,75 +669,122 @@ function buildCode (location, code, laterCode, name) {
798
669
  code += `
799
670
  }
800
671
  `
672
+ })
801
673
 
802
- if (nullable) {
803
- code += `
804
- }
805
- `
674
+ for (const requiredProperty of required) {
675
+ if (schema.properties && schema.properties[requiredProperty] !== undefined) continue
676
+ code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
677
+ }
678
+
679
+ return { code, laterCode }
680
+ }
681
+
682
+ function mergeAllOfSchema (location, schema, mergedSchema) {
683
+ for (let allOfSchema of schema.allOf) {
684
+ if (allOfSchema.$ref) {
685
+ allOfSchema = refFinder(allOfSchema.$ref, mergeLocation(location, { schema: allOfSchema })).schema
806
686
  }
807
- })
808
687
 
809
- if (required && required.length > 0) {
810
- code += 'var required = ['
811
- // eslint-disable-next-line
812
- for (var i = 0; i < required.length; i++) {
813
- if (i > 0) {
814
- code += ','
688
+ let allOfSchemaType = allOfSchema.type
689
+ if (allOfSchemaType === undefined) {
690
+ allOfSchemaType = inferTypeByKeyword(allOfSchema)
691
+ }
692
+
693
+ if (allOfSchemaType !== undefined) {
694
+ if (
695
+ mergedSchema.type !== undefined &&
696
+ mergedSchema.type !== allOfSchemaType
697
+ ) {
698
+ throw new Error('allOf schemas have different type values')
815
699
  }
816
- code += `${JSON.stringify(required[i])}`
700
+ mergedSchema.type = allOfSchemaType
817
701
  }
818
- code += `]
819
- for (var i = 0; i < required.length; i++) {
820
- if (obj[required[i]] === undefined) throw new Error('"' + required[i] + '" is required!')
702
+
703
+ if (allOfSchema.format !== undefined) {
704
+ if (
705
+ mergedSchema.format !== undefined &&
706
+ mergedSchema.format !== allOfSchema.format
707
+ ) {
708
+ throw new Error('allOf schemas have different format values')
821
709
  }
822
- `
823
- }
710
+ mergedSchema.format = allOfSchema.format
711
+ }
824
712
 
825
- if (schema.allOf) {
826
- const builtCode = buildCodeWithAllOfs(location, code, laterCode, name)
827
- code = builtCode.code
828
- laterCode = builtCode.laterCode
829
- }
713
+ if (allOfSchema.nullable !== undefined) {
714
+ if (
715
+ mergedSchema.nullable !== undefined &&
716
+ mergedSchema.nullable !== allOfSchema.nullable
717
+ ) {
718
+ throw new Error('allOf schemas have different nullable values')
719
+ }
720
+ mergedSchema.nullable = allOfSchema.nullable
721
+ }
830
722
 
831
- return { code, laterCode }
832
- }
723
+ if (allOfSchema.properties !== undefined) {
724
+ if (mergedSchema.properties === undefined) {
725
+ mergedSchema.properties = {}
726
+ }
727
+ Object.assign(mergedSchema.properties, allOfSchema.properties)
728
+ }
833
729
 
834
- function filterRequired (required, key) {
835
- if (!required) {
836
- return required
837
- }
838
- return required.filter(k => k !== key)
839
- }
730
+ if (allOfSchema.additionalProperties !== undefined) {
731
+ if (mergedSchema.additionalProperties === undefined) {
732
+ mergedSchema.additionalProperties = {}
733
+ }
734
+ Object.assign(mergedSchema.additionalProperties, allOfSchema.additionalProperties)
735
+ }
840
736
 
841
- function buildCodeWithAllOfs (location, code, laterCode, name) {
842
- if (location.schema.allOf) {
843
- location.schema.allOf.forEach((ss) => {
844
- const builtCode = buildCodeWithAllOfs(mergeLocation(location, { schema: ss }), code, laterCode, name)
845
- code = builtCode.code
846
- laterCode = builtCode.laterCode
847
- })
848
- } else {
849
- const builtCode = buildCode(location, code, laterCode, name)
737
+ if (allOfSchema.patternProperties !== undefined) {
738
+ if (mergedSchema.patternProperties === undefined) {
739
+ mergedSchema.patternProperties = {}
740
+ }
741
+ Object.assign(mergedSchema.patternProperties, allOfSchema.patternProperties)
742
+ }
850
743
 
851
- code = builtCode.code
852
- laterCode = builtCode.laterCode
853
- }
744
+ if (allOfSchema.required !== undefined) {
745
+ if (mergedSchema.required === undefined) {
746
+ mergedSchema.required = []
747
+ }
748
+ mergedSchema.required.push(...allOfSchema.required)
749
+ }
854
750
 
855
- return { code, laterCode }
751
+ if (allOfSchema.oneOf !== undefined) {
752
+ if (mergedSchema.oneOf === undefined) {
753
+ mergedSchema.oneOf = []
754
+ }
755
+ mergedSchema.oneOf.push(...allOfSchema.oneOf)
756
+ }
757
+
758
+ if (allOfSchema.anyOf !== undefined) {
759
+ if (mergedSchema.anyOf === undefined) {
760
+ mergedSchema.anyOf = []
761
+ }
762
+ mergedSchema.anyOf.push(...allOfSchema.anyOf)
763
+ }
764
+
765
+ if (allOfSchema.allOf !== undefined) {
766
+ mergeAllOfSchema(location, allOfSchema, mergedSchema)
767
+ }
768
+ }
769
+ delete mergedSchema.allOf
856
770
  }
857
771
 
858
- function buildInnerObject (location, name) {
772
+ function buildInnerObject (location, locationPath) {
859
773
  const schema = location.schema
860
- const result = buildCodeWithAllOfs(location, '', '', name)
774
+ const result = buildCode(location, '', '', locationPath)
861
775
  if (schema.patternProperties) {
862
- result.code += addPatternProperties(location)
776
+ const { code, laterCode } = addPatternProperties(location)
777
+ result.code += code
778
+ result.laterCode += laterCode
863
779
  } else if (schema.additionalProperties && !schema.patternProperties) {
864
- result.code += addAdditionalProperties(location)
780
+ const { code, laterCode } = addAdditionalProperties(location)
781
+ result.code += code
782
+ result.laterCode += laterCode
865
783
  }
866
784
  return result
867
785
  }
868
786
 
869
- function addIfThenElse (location, name) {
787
+ function addIfThenElse (location, locationPath) {
870
788
  let code = ''
871
789
  let r
872
790
  let laterCode = ''
@@ -891,12 +809,12 @@ function addIfThenElse (location, name) {
891
809
  if (valid) {
892
810
  `
893
811
  if (merged.if && merged.then) {
894
- innerR = addIfThenElse(mergedLocation, name + 'Then')
812
+ innerR = addIfThenElse(mergedLocation, locationPath + 'Then')
895
813
  code += innerR.code
896
814
  laterCode = innerR.laterCode
897
815
  }
898
816
 
899
- r = buildInnerObject(mergedLocation, name + 'Then')
817
+ r = buildInnerObject(mergedLocation, locationPath + 'Then')
900
818
  code += r.code
901
819
  laterCode += r.laterCode
902
820
 
@@ -911,12 +829,12 @@ function addIfThenElse (location, name) {
911
829
  `
912
830
 
913
831
  if (merged.if && merged.then) {
914
- innerR = addIfThenElse(mergedLocation, name + 'Else')
832
+ innerR = addIfThenElse(mergedLocation, locationPath + 'Else')
915
833
  code += innerR.code
916
834
  laterCode += innerR.laterCode
917
835
  }
918
836
 
919
- r = buildInnerObject(mergedLocation, name + 'Else')
837
+ r = buildInnerObject(mergedLocation, locationPath + 'Else')
920
838
  code += r.code
921
839
  laterCode += r.laterCode
922
840
 
@@ -933,13 +851,14 @@ function toJSON (variableName) {
933
851
  `
934
852
  }
935
853
 
936
- function buildObject (location, code, name) {
854
+ function buildObject (location, code, functionName, locationPath) {
937
855
  const schema = location.schema
938
856
  if (schema.$id !== undefined) {
939
857
  schemaReferenceMap.set(schema.$id, schema)
940
858
  }
941
859
  code += `
942
- function ${name} (input) {
860
+ function ${functionName} (input) {
861
+ // ${locationPath}
943
862
  `
944
863
  if (schema.nullable) {
945
864
  code += `
@@ -949,14 +868,14 @@ function buildObject (location, code, name) {
949
868
  `
950
869
  }
951
870
 
952
- if (objectReferenceSerializersMap.has(schema) && objectReferenceSerializersMap.get(schema) !== name) {
871
+ if (objectReferenceSerializersMap.has(schema) && objectReferenceSerializersMap.get(schema) !== functionName) {
953
872
  code += `
954
873
  return ${objectReferenceSerializersMap.get(schema)}(input)
955
874
  }
956
875
  `
957
876
  return code
958
877
  }
959
- objectReferenceSerializersMap.set(schema, name)
878
+ objectReferenceSerializersMap.set(schema, functionName)
960
879
 
961
880
  code += `
962
881
  var obj = ${toJSON('input')}
@@ -969,9 +888,9 @@ function buildObject (location, code, name) {
969
888
  code += `
970
889
  var valid
971
890
  `
972
- r = addIfThenElse(location, name)
891
+ r = addIfThenElse(location, locationPath)
973
892
  } else {
974
- r = buildInnerObject(location, name)
893
+ r = buildInnerObject(location, locationPath)
975
894
  }
976
895
 
977
896
  // Removes the comma if is the last element of the string (in case there are not properties)
@@ -985,13 +904,14 @@ function buildObject (location, code, name) {
985
904
  return code
986
905
  }
987
906
 
988
- function buildArray (location, code, name, key = null) {
907
+ function buildArray (location, code, functionName, locationPath) {
989
908
  let schema = location.schema
990
909
  if (schema.$id !== undefined) {
991
910
  schemaReferenceMap.set(schema.$id, schema)
992
911
  }
993
912
  code += `
994
- function ${name} (obj) {
913
+ function ${functionName} (obj) {
914
+ // ${locationPath}
995
915
  `
996
916
  if (schema.nullable) {
997
917
  code += `
@@ -1024,14 +944,14 @@ function buildArray (location, code, name, key = null) {
1024
944
  `
1025
945
  return code
1026
946
  }
1027
- arrayItemsReferenceSerializersMap.set(schema.items, name)
947
+ arrayItemsReferenceSerializersMap.set(schema.items, functionName)
1028
948
  }
1029
949
 
1030
950
  let result = { code: '', laterCode: '' }
1031
951
  const accessor = '[i]'
1032
952
  if (Array.isArray(schema.items)) {
1033
953
  result = schema.items.reduce((res, item, i) => {
1034
- const tmpRes = nested(laterCode, name, accessor, mergeLocation(location, { schema: item }), i, true)
954
+ const tmpRes = buildValue(laterCode, locationPath + accessor + i, 'obj[i]', mergeLocation(location, { schema: item }))
1035
955
  const condition = `i === ${i} && ${buildArrayTypeCondition(item.type, accessor)}`
1036
956
  return {
1037
957
  code: `${res.code}
@@ -1044,7 +964,7 @@ function buildArray (location, code, name, key = null) {
1044
964
  }, result)
1045
965
 
1046
966
  if (schema.additionalItems) {
1047
- const tmpRes = nested(laterCode, name, accessor, mergeLocation(location, { schema: schema.items }), undefined, true)
967
+ const tmpRes = buildValue(laterCode, locationPath + accessor, 'obj[i]', mergeLocation(location, { schema: schema.items }))
1048
968
  result.code += `
1049
969
  else if (i >= ${schema.items.length}) {
1050
970
  ${tmpRes.code}
@@ -1058,54 +978,39 @@ function buildArray (location, code, name, key = null) {
1058
978
  }
1059
979
  `
1060
980
  } else {
1061
- result = nested(laterCode, name, accessor, mergeLocation(location, { schema: schema.items }), undefined, true)
1062
- }
1063
-
1064
- if (key) {
1065
- code += `
1066
- if(!Array.isArray(obj)) {
1067
- throw new TypeError(\`Property '${key}' should be of type array, received '$\{obj}' instead.\`)
1068
- }
1069
- `
981
+ result = buildValue(laterCode, locationPath + accessor, 'obj[i]', mergeLocation(location, { schema: schema.items }))
1070
982
  }
1071
983
 
1072
984
  code += `
1073
- var l = obj.length
1074
- if (l && l >= ${largeArraySize}) {`
985
+ if (!Array.isArray(obj)) {
986
+ throw new TypeError(\`The value '$\{obj}' does not match schema definition.\`)
987
+ }
988
+ `
1075
989
 
1076
- const concatSnippet = `
990
+ code += 'const arrayLength = obj.length\n'
991
+ if (largeArrayMechanism !== 'default') {
992
+ if (largeArrayMechanism === 'json-stringify') {
993
+ code += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
994
+ } else {
995
+ throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`)
1077
996
  }
997
+ }
1078
998
 
1079
- var jsonOutput= ''
1080
- for (var i = 0; i < l; i++) {
1081
- var json = ''
999
+ code += `
1000
+ let jsonOutput= ''
1001
+ for (let i = 0; i < arrayLength; i++) {
1002
+ let json = ''
1082
1003
  ${result.code}
1083
1004
  jsonOutput += json
1084
1005
 
1085
- if (json.length > 0 && i < l - 1) {
1006
+ if (json.length > 0 && i < arrayLength - 1) {
1086
1007
  jsonOutput += ','
1087
1008
  }
1088
1009
  }
1089
1010
  return \`[\${jsonOutput}]\`
1090
1011
  }`
1091
1012
 
1092
- switch (largeArrayMechanism) {
1093
- case 'default':
1094
- code += `
1095
- return \`[\${obj.map(${result.mapFnName}).join(',')}]\``
1096
- break
1097
-
1098
- case 'json-stringify':
1099
- code += `
1100
- return JSON.stringify(obj)`
1101
- break
1102
-
1103
- default:
1104
- throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`)
1105
- }
1106
-
1107
1013
  code += `
1108
- ${concatSnippet}
1109
1014
  ${result.laterCode}
1110
1015
  `
1111
1016
 
@@ -1173,26 +1078,12 @@ function dereferenceOfRefs (location, type) {
1173
1078
  return locations
1174
1079
  }
1175
1080
 
1176
- let strNameCounter = 0
1177
- function asFuncName (str) {
1178
- // only allow chars that can work
1179
- let rep = str.replace(/[^a-zA-Z0-9$_]/g, '')
1180
-
1181
- if (rep.length === 0) {
1182
- return 'anan' + strNameCounter++
1183
- } else if (rep !== str) {
1184
- rep += strNameCounter++
1185
- }
1186
-
1187
- return rep
1081
+ let genFuncNameCounter = 0
1082
+ function generateFuncName () {
1083
+ return 'anonymous' + genFuncNameCounter++
1188
1084
  }
1189
1085
 
1190
- function nested (laterCode, name, key, location, subKey, isArray) {
1191
- let code = ''
1192
- let funcName
1193
-
1194
- subKey = subKey || ''
1195
-
1086
+ function buildValue (laterCode, locationPath, input, location) {
1196
1087
  let schema = location.schema
1197
1088
 
1198
1089
  if (schema.$ref) {
@@ -1206,121 +1097,99 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1206
1097
  }
1207
1098
  }
1208
1099
 
1100
+ if (schema.allOf) {
1101
+ const mergedSchema = clone(schema)
1102
+ mergeAllOfSchema(location, schema, mergedSchema)
1103
+ schema = mergedSchema
1104
+ location.schema = mergedSchema
1105
+ }
1106
+
1209
1107
  const type = schema.type
1210
1108
  const nullable = schema.nullable === true
1211
1109
 
1212
- const accessor = isArray ? key : `[${JSON.stringify(key)}]`
1110
+ let code = ''
1111
+ let funcName
1213
1112
 
1214
1113
  switch (type) {
1215
1114
  case 'null':
1216
- funcName = '$asNull'
1217
1115
  code += `
1218
- json += $asNull()
1116
+ json += serializer.asNull()
1219
1117
  `
1220
1118
  break
1221
1119
  case 'string': {
1222
- funcName = '$asString'
1223
- const stringSerializer = getStringSerializer(schema.format)
1224
- code += nullable ? `json += obj${accessor} === null ? null : ${stringSerializer}(obj${accessor})` : `json += ${stringSerializer}(obj${accessor})`
1120
+ funcName = getStringSerializer(schema.format, nullable)
1121
+ code += `json += ${funcName}(${input})`
1225
1122
  break
1226
1123
  }
1227
1124
  case 'integer':
1228
- funcName = '$asInteger'
1229
- code += nullable ? `json += obj${accessor} === null ? null : $asInteger(obj${accessor})` : `json += $asInteger(obj${accessor})`
1125
+ funcName = nullable ? 'serializer.asIntegerNullable.bind(serializer)' : 'serializer.asInteger.bind(serializer)'
1126
+ code += `json += ${funcName}(${input})`
1230
1127
  break
1231
1128
  case 'number':
1232
- funcName = '$asNumber'
1233
- code += nullable ? `json += obj${accessor} === null ? null : $asNumber(obj${accessor})` : `json += $asNumber(obj${accessor})`
1129
+ funcName = nullable ? 'serializer.asNumberNullable.bind(serializer)' : 'serializer.asNumber.bind(serializer)'
1130
+ code += `json += ${funcName}(${input})`
1234
1131
  break
1235
1132
  case 'boolean':
1236
- funcName = '$asBoolean'
1237
- code += nullable ? `json += obj${accessor} === null ? null : $asBoolean(obj${accessor})` : `json += $asBoolean(obj${accessor})`
1133
+ funcName = nullable ? 'serializer.asBooleanNullable.bind(serializer)' : 'serializer.asBoolean.bind(serializer)'
1134
+ code += `json += ${funcName}(${input})`
1238
1135
  break
1239
1136
  case 'object':
1240
- funcName = asFuncName(name + key + subKey)
1241
- laterCode = buildObject(location, laterCode, funcName)
1242
- code += `
1243
- json += ${funcName}(obj${accessor})
1244
- `
1137
+ funcName = generateFuncName()
1138
+ laterCode = buildObject(location, laterCode, funcName, locationPath)
1139
+ code += `json += ${funcName}(${input})`
1245
1140
  break
1246
1141
  case 'array':
1247
- funcName = asFuncName('$arr' + name + key + subKey) // eslint-disable-line
1248
- laterCode = buildArray(location, laterCode, funcName, key)
1249
- code += `
1250
- json += ${funcName}(obj${accessor})
1251
- `
1142
+ funcName = generateFuncName()
1143
+ laterCode = buildArray(location, laterCode, funcName, locationPath)
1144
+ code += `json += ${funcName}(${input})`
1252
1145
  break
1253
1146
  case undefined:
1254
- funcName = '$asNull'
1255
- if ('anyOf' in schema) {
1147
+ if (schema.anyOf || schema.oneOf) {
1256
1148
  // beware: dereferenceOfRefs has side effects and changes schema.anyOf
1257
- const anyOfLocations = dereferenceOfRefs(location, 'anyOf')
1258
- anyOfLocations.forEach((location, index) => {
1259
- const nestedResult = nested(laterCode, name, key, location, subKey !== '' ? subKey : 'i' + index, isArray)
1260
- // We need a test serializer as the String serializer will not work with
1261
- // date/time ajv validations
1262
- // see: https://github.com/fastify/fast-json-stringify/issues/325
1263
- const testSerializer = getTestSerializer(location.schema.format)
1264
- const testValue = testSerializer !== undefined ? `${testSerializer}(obj${accessor}, true)` : `obj${accessor}`
1265
-
1149
+ const locations = dereferenceOfRefs(location, schema.anyOf ? 'anyOf' : 'oneOf')
1150
+ locations.forEach((location, index) => {
1151
+ const nestedResult = buildValue(laterCode, locationPath + 'i' + index, input, location)
1266
1152
  // Since we are only passing the relevant schema to ajv.validate, it needs to be full dereferenced
1267
1153
  // otherwise any $ref pointing to an external schema would result in an error.
1268
1154
  // Full dereference of the schema happens as side effect of two functions:
1269
1155
  // 1. `dereferenceOfRefs` loops through the `schema.anyOf`` array and replaces any top level reference
1270
1156
  // with the actual schema
1271
- // 2. `nested`, through `buildCode`, replaces any reference in object properties with the actual schema
1157
+ // 2. `buildValue`, through `buildCode`, replaces any reference in object properties with the actual schema
1272
1158
  // (see https://github.com/fastify/fast-json-stringify/blob/6da3b3e8ac24b1ca5578223adedb4083b7adf8db/index.js#L631)
1273
1159
 
1274
- const schemaKey = location.schema.$id || randomUUID()
1275
- ajvInstance.addSchema(location.schema, schemaKey)
1276
-
1277
- code += `
1278
- ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${testValue}))
1279
- ${nestedResult.code}
1280
- `
1281
- laterCode = nestedResult.laterCode
1282
- })
1283
- code += `
1284
- else json+= null
1285
- `
1286
- } else if ('oneOf' in schema) {
1287
- // beware: dereferenceOfRefs has side effects and changes schema.oneOf
1288
- const oneOfLocations = dereferenceOfRefs(location, 'oneOf')
1289
- oneOfLocations.forEach((location, index) => {
1290
- const nestedResult = nested(laterCode, name, key, location, subKey !== '' ? subKey : 'i' + index, isArray)
1291
- const testSerializer = getTestSerializer(location.schema.format)
1292
- const testValue = testSerializer !== undefined ? `${testSerializer}(obj${accessor}, true)` : `obj${accessor}`
1293
- // see comment on anyOf about dereferencing the schema before calling ajv.validate
1160
+ // Ajv does not support js date format. In order to properly validate objects containing a date,
1161
+ // it needs to replace all occurrences of the string date format with a custom keyword fjs_date_type.
1162
+ // (see https://github.com/fastify/fast-json-stringify/pull/441)
1163
+ const extendedSchema = clone(location.schema)
1164
+ extendDateTimeType(extendedSchema)
1294
1165
 
1295
1166
  const schemaKey = location.schema.$id || randomUUID()
1296
- ajvInstance.addSchema(location.schema, schemaKey)
1167
+ ajvInstance.addSchema(extendedSchema, schemaKey)
1297
1168
 
1298
1169
  code += `
1299
- ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${testValue}))
1170
+ ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${input}))
1300
1171
  ${nestedResult.code}
1301
1172
  `
1302
1173
  laterCode = nestedResult.laterCode
1303
1174
  })
1304
1175
 
1305
- if (!isArray) {
1306
- code += `
1307
- else json+= null
1308
- `
1309
- }
1176
+ code += `
1177
+ else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
1178
+ `
1310
1179
  } else if (isEmpty(schema)) {
1311
1180
  code += `
1312
- json += JSON.stringify(obj${accessor})
1181
+ json += JSON.stringify(${input})
1313
1182
  `
1314
1183
  } else if ('const' in schema) {
1315
1184
  code += `
1316
- if(ajv.validate(${JSON.stringify(schema)}, obj${accessor}))
1185
+ if(ajv.validate(${JSON.stringify(schema)}, ${input}))
1317
1186
  json += '${JSON.stringify(schema.const)}'
1318
1187
  else
1319
- throw new Error(\`Item $\{JSON.stringify(obj${accessor})} does not match schema definition.\`)
1188
+ throw new Error(\`Item $\{JSON.stringify(${input})} does not match schema definition.\`)
1320
1189
  `
1321
1190
  } else if (schema.type === undefined) {
1322
1191
  code += `
1323
- json += JSON.stringify(obj${accessor})
1192
+ json += JSON.stringify(${input})
1324
1193
  `
1325
1194
  } else {
1326
1195
  throw new Error(`${schema.type} unsupported`)
@@ -1328,51 +1197,46 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1328
1197
  break
1329
1198
  default:
1330
1199
  if (Array.isArray(type)) {
1331
- const nullIndex = type.indexOf('null')
1332
- const sortedTypes = nullIndex !== -1 ? [type[nullIndex]].concat(type.slice(0, nullIndex)).concat(type.slice(nullIndex + 1)) : type
1200
+ let sortedTypes = type
1201
+ const nullable = schema.nullable === true || type.includes('null')
1202
+
1203
+ if (nullable) {
1204
+ sortedTypes = sortedTypes.filter(type => type !== 'null')
1205
+ code += `
1206
+ if (${input} === null) {
1207
+ json += null
1208
+ } else {`
1209
+ }
1210
+
1333
1211
  sortedTypes.forEach((type, index) => {
1334
1212
  const statement = index === 0 ? 'if' : 'else if'
1335
1213
  const tempSchema = Object.assign({}, schema, { type })
1336
- const nestedResult = nested(laterCode, name, key, mergeLocation(location, { schema: tempSchema }), subKey, isArray)
1214
+ const nestedResult = buildValue(laterCode, locationPath, input, mergeLocation(location, { schema: tempSchema }))
1337
1215
  switch (type) {
1338
1216
  case 'string': {
1339
1217
  code += `
1340
- ${statement}(obj${accessor} === null || typeof obj${accessor} === "${type}" || obj${accessor} instanceof Date || typeof obj${accessor}.toISOString === "function" || obj${accessor} instanceof RegExp || (typeof obj${accessor} === "object" && Object.hasOwnProperty.call(obj${accessor}, "toString")))
1341
- ${nestedResult.code}
1342
- `
1343
- break
1344
- }
1345
- case 'null': {
1346
- code += `
1347
- ${statement}(obj${accessor} == null)
1218
+ ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof Date || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
1348
1219
  ${nestedResult.code}
1349
1220
  `
1350
1221
  break
1351
1222
  }
1352
1223
  case 'array': {
1353
1224
  code += `
1354
- ${statement}(Array.isArray(obj${accessor}))
1225
+ ${statement}(Array.isArray(${input}))
1355
1226
  ${nestedResult.code}
1356
1227
  `
1357
1228
  break
1358
1229
  }
1359
1230
  case 'integer': {
1360
1231
  code += `
1361
- ${statement}(Number.isInteger(obj${accessor}) || obj${accessor} === null)
1362
- ${nestedResult.code}
1363
- `
1364
- break
1365
- }
1366
- case 'number': {
1367
- code += `
1368
- ${statement}(isNaN(obj${accessor}) === false)
1232
+ ${statement}(Number.isInteger(${input}) || ${input} === null)
1369
1233
  ${nestedResult.code}
1370
1234
  `
1371
1235
  break
1372
1236
  }
1373
1237
  default: {
1374
1238
  code += `
1375
- ${statement}(typeof obj${accessor} === "${type}")
1239
+ ${statement}(typeof ${input} === "${type}" || ${input} === null)
1376
1240
  ${nestedResult.code}
1377
1241
  `
1378
1242
  break
@@ -1381,17 +1245,32 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1381
1245
  laterCode = nestedResult.laterCode
1382
1246
  })
1383
1247
  code += `
1384
- else json+= null
1248
+ else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
1385
1249
  `
1250
+
1251
+ if (nullable) {
1252
+ code += `
1253
+ }
1254
+ `
1255
+ }
1386
1256
  } else {
1387
1257
  throw new Error(`${type} unsupported`)
1388
1258
  }
1389
1259
  }
1390
1260
 
1391
- return {
1392
- code,
1393
- laterCode,
1394
- mapFnName: funcName
1261
+ return { code, laterCode }
1262
+ }
1263
+
1264
+ function extendDateTimeType (schema) {
1265
+ if (schema.type === 'string' && ['date-time', 'date', 'time'].includes(schema.format)) {
1266
+ schema.fjs_date_type = schema.format
1267
+ delete schema.type
1268
+ delete schema.format
1269
+ }
1270
+ for (const property in schema) {
1271
+ if (typeof schema[property] === 'object') {
1272
+ extendDateTimeType(schema[property])
1273
+ }
1395
1274
  }
1396
1275
  }
1397
1276
 
@@ -1410,7 +1289,8 @@ module.exports = build
1410
1289
  module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms
1411
1290
 
1412
1291
  module.exports.restore = function ({ code, ajv }) {
1292
+ const serializer = new Serializer()
1413
1293
  // eslint-disable-next-line
1414
- return (Function.apply(null, ['ajv', code])
1415
- .apply(null, [ajv]))
1294
+ return (Function.apply(null, ['ajv', 'serializer', code])
1295
+ .apply(null, [ajv, serializer]))
1416
1296
  }