fast-json-stringify 3.0.3 → 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
@@ -3,6 +3,7 @@
3
3
  /* eslint no-prototype-builtins: 0 */
4
4
 
5
5
  const Ajv = require('ajv')
6
+ const fastUri = require('fast-uri')
6
7
  const ajvFormats = require('ajv-formats')
7
8
  const merge = require('deepmerge')
8
9
  const clone = require('rfdc')({ proto: true })
@@ -10,7 +11,14 @@ const fjsCloned = Symbol('fast-json-stringify.cloned')
10
11
  const { randomUUID } = require('crypto')
11
12
 
12
13
  const validate = require('./schema-validator')
14
+
15
+ let largeArraySize = 2e4
13
16
  let stringSimilarity = null
17
+ let largeArrayMechanism = 'default'
18
+ const validLargeArrayMechanisms = [
19
+ 'default',
20
+ 'json-stringify'
21
+ ]
14
22
 
15
23
  const addComma = `
16
24
  if (addComma) {
@@ -44,17 +52,210 @@ function mergeLocation (source, dest) {
44
52
 
45
53
  const arrayItemsReferenceSerializersMap = new Map()
46
54
  const objectReferenceSerializersMap = new Map()
55
+ const schemaReferenceMap = new Map()
56
+
47
57
  let ajvInstance = null
48
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
+
49
226
  function build (schema, options) {
50
227
  arrayItemsReferenceSerializersMap.clear()
51
228
  objectReferenceSerializersMap.clear()
229
+ schemaReferenceMap.clear()
52
230
 
53
231
  options = options || {}
54
232
 
55
- ajvInstance = new Ajv({ ...options.ajv, strictSchema: false })
233
+ ajvInstance = new Ajv({ ...options.ajv, strictSchema: false, uriResolver: fastUri })
56
234
  ajvFormats(ajvInstance)
57
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
+
58
259
  isValidSchema(schema)
59
260
  if (options.schema) {
60
261
  // eslint-disable-next-line
@@ -63,24 +264,29 @@ function build (schema, options) {
63
264
  }
64
265
  }
65
266
 
66
- let intParseFunctionName = 'trunc'
67
267
  if (options.rounding) {
68
- if (['floor', 'ceil', 'round'].includes(options.rounding)) {
69
- intParseFunctionName = options.rounding
70
- } else {
268
+ if (!['floor', 'ceil', 'round'].includes(options.rounding)) {
71
269
  throw new Error(`Unsupported integer rounding method ${options.rounding}`)
72
270
  }
73
271
  }
74
272
 
75
- /* eslint no-new-func: "off" */
76
- let code = `
77
- 'use strict'
78
- `
273
+ if (options.largeArrayMechanism) {
274
+ if (validLargeArrayMechanisms.includes(options.largeArrayMechanism)) {
275
+ largeArrayMechanism = options.largeArrayMechanism
276
+ } else {
277
+ throw new Error(`Unsupported large array mechanism ${options.rounding}`)
278
+ }
279
+ }
79
280
 
80
- code += `
81
- ${asFunctions}
82
- function parseInteger(int) { return Math.${intParseFunctionName}(int) }
83
- `
281
+ if (options.largeArraySize) {
282
+ if (!Number.isNaN(Number.parseInt(options.largeArraySize, 10))) {
283
+ largeArraySize = options.largeArraySize
284
+ } else {
285
+ throw new Error(`Unsupported large array size. Expected integer-like, got ${options.largeArraySize}`)
286
+ }
287
+ }
288
+
289
+ const serializer = new Serializer(options)
84
290
 
85
291
  let location = {
86
292
  schema,
@@ -97,61 +303,34 @@ function build (schema, options) {
97
303
  schema.type = inferTypeByKeyword(schema)
98
304
  }
99
305
 
100
- let main
101
-
102
- switch (schema.type) {
103
- case 'object':
104
- main = '$main'
105
- code = buildObject(location, code, main)
106
- break
107
- case 'string':
108
- main = schema.nullable ? '$asStringNullable' : getStringSerializer(schema.format)
109
- break
110
- case 'integer':
111
- main = schema.nullable ? '$asIntegerNullable' : '$asInteger'
112
- break
113
- case 'number':
114
- main = schema.nullable ? '$asNumberNullable' : '$asNumber'
115
- break
116
- case 'boolean':
117
- main = schema.nullable ? '$asBooleanNullable' : '$asBoolean'
118
- break
119
- case 'null':
120
- main = '$asNull'
121
- break
122
- case 'array':
123
- main = '$main'
124
- code = buildArray(location, code, main)
125
- schema = location.schema
126
- break
127
- case undefined:
128
- main = '$asAny'
129
- break
130
- default:
131
- throw new Error(`${schema.type} unsupported`)
132
- }
133
-
134
- code += `
135
- ;
136
- return ${main}
137
- `
138
- const dependencies = [ajvInstance]
139
- const dependenciesName = ['ajv']
140
- 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
+ `
141
317
 
142
- dependenciesName.push(code)
318
+ const dependenciesName = ['ajv', 'serializer', contextFunctionCode]
143
319
 
144
320
  if (options.debugMode) {
145
- return {
146
- code: dependenciesName.join('\n'),
147
- ajv: dependencies[0]
148
- }
321
+ return { code: dependenciesName.join('\n'), ajv: ajvInstance }
149
322
  }
150
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
151
329
  arrayItemsReferenceSerializersMap.clear()
152
330
  objectReferenceSerializersMap.clear()
331
+ schemaReferenceMap.clear()
153
332
 
154
- return (Function.apply(null, dependenciesName).apply(null, dependencies))
333
+ return stringifyFunc
155
334
  }
156
335
 
157
336
  const objectKeywords = [
@@ -189,7 +368,7 @@ const numberKeywords = [
189
368
 
190
369
  /**
191
370
  * Infer type based on keyword in order to generate optimized code
192
- * https://json-schema.org/latest/json-schema-validation.html#rfc.section.6
371
+ * https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6
193
372
  */
194
373
  function inferTypeByKeyword (schema) {
195
374
  // eslint-disable-next-line
@@ -211,171 +390,15 @@ function inferTypeByKeyword (schema) {
211
390
  return schema.type
212
391
  }
213
392
 
214
- const stringSerializerMap = {
215
- 'date-time': '$asDatetime',
216
- date: '$asDate',
217
- time: '$asTime'
218
- }
219
-
220
- function getStringSerializer (format) {
221
- return stringSerializerMap[format] ||
222
- '$asString'
223
- }
224
-
225
- function getTestSerializer (format) {
226
- return stringSerializerMap[format]
227
- }
228
-
229
- const asFunctions = `
230
- function $pad2Zeros (num) {
231
- const s = '00' + num
232
- return s[s.length - 2] + s[s.length - 1]
233
- }
234
-
235
- function $asAny (i) {
236
- return JSON.stringify(i)
237
- }
238
-
239
- function $asNull () {
240
- return 'null'
241
- }
242
-
243
- function $asInteger (i) {
244
- if (typeof i === 'bigint') {
245
- return i.toString()
246
- } else if (Number.isInteger(i)) {
247
- return $asNumber(i)
248
- } else {
249
- /* eslint no-undef: "off" */
250
- return $asNumber(parseInteger(i))
251
- }
252
- }
253
-
254
- function $asIntegerNullable (i) {
255
- return i === null ? null : $asInteger(i)
256
- }
257
-
258
- function $asNumber (i) {
259
- const num = Number(i)
260
- if (isNaN(num)) {
261
- return 'null'
262
- } else {
263
- return '' + num
264
- }
265
- }
266
-
267
- function $asNumberNullable (i) {
268
- return i === null ? null : $asNumber(i)
269
- }
270
-
271
- function $asBoolean (bool) {
272
- return bool && 'true' || 'false' // eslint-disable-line
273
- }
274
-
275
- function $asBooleanNullable (bool) {
276
- return bool === null ? null : $asBoolean(bool)
277
- }
278
-
279
- function $asDatetime (date, skipQuotes) {
280
- const quotes = skipQuotes === true ? '' : '"'
281
- if (date instanceof Date) {
282
- return quotes + date.toISOString() + quotes
283
- } else if (date && typeof date.toISOString === 'function') {
284
- return quotes + date.toISOString() + quotes
285
- } else {
286
- return $asString(date, skipQuotes)
287
- }
288
- }
289
-
290
- function $asDate (date, skipQuotes) {
291
- const quotes = skipQuotes === true ? '' : '"'
292
- if (date instanceof Date) {
293
- return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000 )).toISOString().slice(0, 10) + quotes
294
- } else if (date && typeof date.format === 'function') {
295
- return quotes + date.format('YYYY-MM-DD') + quotes
296
- } else {
297
- return $asString(date, skipQuotes)
298
- }
299
- }
300
-
301
- function $asTime (date, skipQuotes) {
302
- const quotes = skipQuotes === true ? '' : '"'
303
- if (date instanceof Date) {
304
- const hour = new Intl.DateTimeFormat('en', { hour: 'numeric', hour12: false }).format(date)
305
- const minute = new Intl.DateTimeFormat('en', { minute: 'numeric' }).format(date)
306
- const second = new Intl.DateTimeFormat('en', { second: 'numeric' }).format(date)
307
- return quotes + $pad2Zeros(hour) + ':' + $pad2Zeros(minute) + ':' + $pad2Zeros(second) + quotes
308
- } else if (date && typeof date.format === 'function') {
309
- return quotes + date.format('HH:mm:ss') + quotes
310
- } else {
311
- return $asString(date, skipQuotes)
312
- }
313
- }
314
-
315
- function $asString (str, skipQuotes) {
316
- const quotes = skipQuotes === true ? '' : '"'
317
- if (str instanceof Date) {
318
- return quotes + str.toISOString() + quotes
319
- } else if (str === null) {
320
- return quotes + quotes
321
- } else if (str instanceof RegExp) {
322
- str = str.source
323
- } else if (typeof str !== 'string') {
324
- str = str.toString()
325
- }
326
- // If we skipQuotes it means that we are using it as test
327
- // no need to test the string length for the render
328
- if (skipQuotes) {
329
- return str
330
- }
331
-
332
- if (str.length < 42) {
333
- return $asStringSmall(str)
334
- } else {
335
- return JSON.stringify(str)
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)'
336
399
  }
337
400
  }
338
401
 
339
- function $asStringNullable (str) {
340
- return str === null ? null : $asString(str)
341
- }
342
-
343
- // magically escape strings for json
344
- // relying on their charCodeAt
345
- // everything below 32 needs JSON.stringify()
346
- // every string that contain surrogate needs JSON.stringify()
347
- // 34 and 92 happens all the time, so we
348
- // have a fast case for them
349
- function $asStringSmall (str) {
350
- const l = str.length
351
- let result = ''
352
- let last = 0
353
- let found = false
354
- let surrogateFound = false
355
- let point = 255
356
- // eslint-disable-next-line
357
- for (var i = 0; i < l && point >= 32; i++) {
358
- point = str.charCodeAt(i)
359
- if (point >= 0xD800 && point <= 0xDFFF) {
360
- // The current character is a surrogate.
361
- surrogateFound = true
362
- }
363
- if (point === 34 || point === 92) {
364
- result += str.slice(last, i) + '\\\\'
365
- last = i
366
- found = true
367
- }
368
- }
369
-
370
- if (!found) {
371
- result = str
372
- } else {
373
- result += str.slice(last)
374
- }
375
- return ((point < 32) || (surrogateFound === true)) ? JSON.stringify(str) : '"' + result + '"'
376
- }
377
- `
378
-
379
402
  function addPatternProperties (location) {
380
403
  const schema = location.schema
381
404
  const pp = schema.patternProperties
@@ -385,103 +408,56 @@ function addPatternProperties (location) {
385
408
  for (var i = 0; i < keys.length; i++) {
386
409
  if (properties[keys[i]]) continue
387
410
  `
411
+ let laterCode = ''
412
+
388
413
  Object.keys(pp).forEach((regex, index) => {
389
414
  let ppLocation = mergeLocation(location, { schema: pp[regex] })
390
415
  if (pp[regex].$ref) {
391
416
  ppLocation = refFinder(pp[regex].$ref, location)
392
417
  pp[regex] = ppLocation.schema
393
418
  }
394
- const type = pp[regex].type
395
- const format = pp[regex].format
396
- const stringSerializer = getStringSerializer(format)
419
+
397
420
  try {
398
421
  RegExp(regex)
399
422
  } catch (err) {
400
423
  throw new Error(`${err.message}. Found at ${regex} matching ${JSON.stringify(pp[regex])}`)
401
424
  }
402
425
 
403
- const ifPpKeyExists = `if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {`
404
-
405
- if (type === 'object') {
406
- code += `${buildObject(ppLocation, '', 'buildObjectPP' + index)}
407
- ${ifPpKeyExists}
408
- ${addComma}
409
- json += $asString(keys[i]) + ':' + buildObjectPP${index}(obj[keys[i]])
410
- `
411
- } else if (type === 'array') {
412
- code += `${buildArray(ppLocation, '', 'buildArrayPP' + index)}
413
- ${ifPpKeyExists}
414
- ${addComma}
415
- json += $asString(keys[i]) + ':' + buildArrayPP${index}(obj[keys[i]])
416
- `
417
- } else if (type === 'null') {
418
- code += `
419
- ${ifPpKeyExists}
420
- ${addComma}
421
- json += $asString(keys[i]) +':null'
422
- `
423
- } else if (type === 'string') {
424
- code += `
425
- ${ifPpKeyExists}
426
- ${addComma}
427
- json += $asString(keys[i]) + ':' + ${stringSerializer}(obj[keys[i]])
428
- `
429
- } else if (type === 'integer') {
430
- code += `
431
- ${ifPpKeyExists}
432
- ${addComma}
433
- json += $asString(keys[i]) + ':' + $asInteger(obj[keys[i]])
434
- `
435
- } else if (type === 'number') {
436
- code += `
437
- ${ifPpKeyExists}
438
- ${addComma}
439
- json += $asString(keys[i]) + ':' + $asNumber(obj[keys[i]])
440
- `
441
- } else if (type === 'boolean') {
442
- code += `
443
- ${ifPpKeyExists}
444
- ${addComma}
445
- json += $asString(keys[i]) + ':' + $asBoolean(obj[keys[i]])
446
- `
447
- } else if (type === undefined) {
448
- code += `
449
- ${ifPpKeyExists}
450
- ${addComma}
451
- json += $asString(keys[i]) + ':' + $asAny(obj[keys[i]])
452
- `
453
- } else {
454
- code += `
455
- ${ifPpKeyExists}
456
- throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ' + ${JSON.stringify(type)})
457
- `
458
- }
459
-
426
+ const valueCode = buildValue('', '', 'obj[keys[i]]', ppLocation)
427
+ laterCode += valueCode.laterCode
460
428
  code += `
461
- continue
462
- }
429
+ if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {
430
+ ${addComma}
431
+ json += serializer.asString(keys[i]) + ':'
432
+ ${valueCode.code}
433
+ continue
434
+ }
463
435
  `
464
436
  })
465
437
  if (schema.additionalProperties) {
466
- code += additionalProperty(location)
438
+ const additionalPropertyCode = additionalProperty(location)
439
+ code += additionalPropertyCode.code
440
+ laterCode += additionalPropertyCode.laterCode
467
441
  }
468
442
 
469
443
  code += `
470
444
  }
471
445
  `
472
- return code
446
+ return { code, laterCode }
473
447
  }
474
448
 
475
449
  function additionalProperty (location) {
476
450
  let ap = location.schema.additionalProperties
477
451
  let code = ''
478
452
  if (ap === true) {
479
- return `
453
+ code += `
480
454
  if (obj[keys[i]] !== undefined && typeof obj[keys[i]] !== 'function' && typeof obj[keys[i]] !== 'symbol') {
481
455
  ${addComma}
482
- json += $asString(keys[i]) + ':' + JSON.stringify(obj[keys[i]])
456
+ json += serializer.asString(keys[i]) + ':' + JSON.stringify(obj[keys[i]])
483
457
  }
484
458
  `
459
+
460
+ return { code, laterCode: '' }
485
461
  }
486
462
  let apLocation = mergeLocation(location, { schema: ap })
487
463
  if (ap.$ref) {
@@ -489,72 +465,29 @@ function additionalProperty (location) {
489
465
  ap = apLocation.schema
490
466
  }
491
467
 
492
- const type = ap.type
493
- const format = ap.format
494
- const stringSerializer = getStringSerializer(format)
495
- if (type === 'object') {
496
- code += `${buildObject(apLocation, '', 'buildObjectAP')}
497
- ${addComma}
498
- json += $asString(keys[i]) + ':' + buildObjectAP(obj[keys[i]])
499
- `
500
- } else if (type === 'array') {
501
- code += `${buildArray(apLocation, '', 'buildArrayAP')}
502
- ${addComma}
503
- json += $asString(keys[i]) + ':' + buildArrayAP(obj[keys[i]])
504
- `
505
- } else if (type === 'null') {
506
- code += `
507
- ${addComma}
508
- json += $asString(keys[i]) +':null'
509
- `
510
- } else if (type === 'string') {
511
- code += `
512
- ${addComma}
513
- json += $asString(keys[i]) + ':' + ${stringSerializer}(obj[keys[i]])
514
- `
515
- } else if (type === 'integer') {
516
- code += `
517
- var t = Number(obj[keys[i]])
518
- if (!isNaN(t)) {
519
- ${addComma}
520
- json += $asString(keys[i]) + ':' + t
521
- }
522
- `
523
- } else if (type === 'number') {
524
- code += `
525
- var t = Number(obj[keys[i]])
526
- if (!isNaN(t)) {
527
- ${addComma}
528
- json += $asString(keys[i]) + ':' + t
529
- }
530
- `
531
- } else if (type === 'boolean') {
532
- code += `
533
- ${addComma}
534
- json += $asString(keys[i]) + ':' + $asBoolean(obj[keys[i]])
535
- `
536
- } else if (type === undefined) {
537
- code += `
538
- ${addComma}
539
- json += $asString(keys[i]) + ':' + $asAny(obj[keys[i]])
540
- `
541
- } else {
542
- code += `
543
- throw new Error('Cannot coerce ' + obj[keys[i]] + ' to ' + ${JSON.stringify(type)})
544
- `
545
- }
546
- 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 }
547
477
  }
548
478
 
549
479
  function addAdditionalProperties (location) {
550
- return `
480
+ const additionalPropertyCode = additionalProperty(location)
481
+ const code = `
551
482
  var properties = ${JSON.stringify(location.schema.properties)} || {}
552
483
  var keys = Object.keys(obj)
553
484
  for (var i = 0; i < keys.length; i++) {
554
485
  if (properties[keys[i]]) continue
555
- ${additionalProperty(location)}
486
+ ${additionalPropertyCode.code}
556
487
  }
557
488
  `
489
+
490
+ return { code, laterCode: additionalPropertyCode.laterCode }
558
491
  }
559
492
 
560
493
  function idFinder (schema, searchedId) {
@@ -581,15 +514,25 @@ function refFinder (ref, location) {
581
514
  return {
582
515
  schema: externalSchema[ref],
583
516
  root: externalSchema[ref],
584
- externalSchema: externalSchema
517
+ externalSchema
585
518
  }
586
519
  }
587
520
 
588
521
  // Split file from walk
589
522
  ref = ref.split('#')
590
523
 
591
- // If external file
592
- if (ref[0]) {
524
+ // Check schemaReferenceMap for $id entry
525
+ if (ref[0] && schemaReferenceMap.has(ref[0])) {
526
+ schema = schemaReferenceMap.get(ref[0])
527
+ root = schemaReferenceMap.get(ref[0])
528
+ if (schema.$ref) {
529
+ return refFinder(schema.$ref, {
530
+ schema,
531
+ root,
532
+ externalSchema
533
+ })
534
+ }
535
+ } else if (ref[0]) { // If external file
593
536
  schema = externalSchema[ref[0]]
594
537
  root = externalSchema[ref[0]]
595
538
 
@@ -599,9 +542,9 @@ function refFinder (ref, location) {
599
542
 
600
543
  if (schema.$ref) {
601
544
  return refFinder(schema.$ref, {
602
- schema: schema,
603
- root: root,
604
- externalSchema: externalSchema
545
+ schema,
546
+ root,
547
+ externalSchema
605
548
  })
606
549
  }
607
550
  }
@@ -628,8 +571,8 @@ function refFinder (ref, location) {
628
571
 
629
572
  return {
630
573
  schema: dereferenced,
631
- root: root,
632
- externalSchema: externalSchema
574
+ root,
575
+ externalSchema
633
576
  }
634
577
  } else {
635
578
  // eslint-disable-next-line
@@ -650,16 +593,16 @@ function refFinder (ref, location) {
650
593
 
651
594
  if (result.$ref) {
652
595
  return refFinder(result.$ref, {
653
- schema: schema,
654
- root: root,
655
- externalSchema: externalSchema
596
+ schema,
597
+ root,
598
+ externalSchema
656
599
  })
657
600
  }
658
601
 
659
602
  return {
660
603
  schema: result,
661
- root: root,
662
- externalSchema: externalSchema
604
+ root,
605
+ externalSchema
663
606
  }
664
607
 
665
608
  function findBadKey (obj, keys) {
@@ -678,15 +621,15 @@ function refFinder (ref, location) {
678
621
  }
679
622
  }
680
623
 
681
- function buildCode (location, code, laterCode, name) {
624
+ function buildCode (location, code, laterCode, locationPath) {
682
625
  if (location.schema.$ref) {
683
626
  location = refFinder(location.schema.$ref, location)
684
627
  }
685
628
 
686
629
  const schema = location.schema
687
- let required = schema.required
630
+ const required = schema.required || []
688
631
 
689
- Object.keys(schema.properties || {}).forEach((key, i, a) => {
632
+ Object.keys(schema.properties || {}).forEach((key) => {
690
633
  let propertyLocation = mergeLocation(location, { schema: schema.properties[key] })
691
634
  if (schema.properties[key].$ref) {
692
635
  propertyLocation = refFinder(schema.properties[key].$ref, location)
@@ -696,61 +639,27 @@ function buildCode (location, code, laterCode, name) {
696
639
  // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
697
640
  // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
698
641
 
699
- const type = schema.properties[key].type
700
- const nullable = schema.properties[key].nullable
701
642
  const sanitized = JSON.stringify(key)
702
643
  const asString = JSON.stringify(sanitized)
703
644
 
704
- if (nullable) {
705
- code += `
706
- if (obj[${sanitized}] === null) {
707
- ${addComma}
708
- json += ${asString} + ':null'
709
- var rendered = true
710
- } else {
645
+ code += `
646
+ if (obj[${sanitized}] !== undefined) {
647
+ ${addComma}
648
+ json += ${asString} + ':'
711
649
  `
712
- }
713
650
 
714
- if (type === 'number') {
715
- code += `
716
- var t = Number(obj[${sanitized}])
717
- if (!isNaN(t)) {
718
- ${addComma}
719
- json += ${asString} + ':' + t
720
- `
721
- } else if (type === 'integer') {
722
- code += `
723
- var rendered = false
724
- var t = $asInteger(obj[${sanitized}])
725
- if (!isNaN(t)) {
726
- ${addComma}
727
- json += ${asString} + ':' + t
728
- rendered = true
729
- }
730
- if (rendered) {
731
- `
732
- } else {
733
- code += `
734
- if (obj[${sanitized}] !== undefined) {
735
- ${addComma}
736
- json += ${asString} + ':'
737
- `
738
-
739
- const result = nested(laterCode, name, key, mergeLocation(propertyLocation, { schema: schema.properties[key] }), undefined, false)
740
- code += result.code
741
- laterCode = result.laterCode
742
- }
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
743
654
 
744
655
  const defaultValue = schema.properties[key].default
745
656
  if (defaultValue !== undefined) {
746
- required = filterRequired(required, key)
747
657
  code += `
748
658
  } else {
749
659
  ${addComma}
750
660
  json += ${asString} + ':' + ${JSON.stringify(JSON.stringify(defaultValue))}
751
661
  `
752
- } else if (required && required.indexOf(key) !== -1) {
753
- required = filterRequired(required, key)
662
+ } else if (required.includes(key)) {
754
663
  code += `
755
664
  } else {
756
665
  throw new Error('${sanitized} is required!')
@@ -760,75 +669,122 @@ function buildCode (location, code, laterCode, name) {
760
669
  code += `
761
670
  }
762
671
  `
672
+ })
763
673
 
764
- if (nullable) {
765
- code += `
766
- }
767
- `
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
768
686
  }
769
- })
770
687
 
771
- if (required && required.length > 0) {
772
- code += 'var required = ['
773
- // eslint-disable-next-line
774
- for (var i = 0; i < required.length; i++) {
775
- if (i > 0) {
776
- 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')
777
699
  }
778
- code += `${JSON.stringify(required[i])}`
700
+ mergedSchema.type = allOfSchemaType
779
701
  }
780
- code += `]
781
- for (var i = 0; i < required.length; i++) {
782
- 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')
783
709
  }
784
- `
785
- }
710
+ mergedSchema.format = allOfSchema.format
711
+ }
786
712
 
787
- if (schema.allOf) {
788
- const builtCode = buildCodeWithAllOfs(location, code, laterCode, name)
789
- code = builtCode.code
790
- laterCode = builtCode.laterCode
791
- }
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
+ }
792
722
 
793
- return { code: code, laterCode: laterCode }
794
- }
723
+ if (allOfSchema.properties !== undefined) {
724
+ if (mergedSchema.properties === undefined) {
725
+ mergedSchema.properties = {}
726
+ }
727
+ Object.assign(mergedSchema.properties, allOfSchema.properties)
728
+ }
795
729
 
796
- function filterRequired (required, key) {
797
- if (!required) {
798
- return required
799
- }
800
- return required.filter(k => k !== key)
801
- }
730
+ if (allOfSchema.additionalProperties !== undefined) {
731
+ if (mergedSchema.additionalProperties === undefined) {
732
+ mergedSchema.additionalProperties = {}
733
+ }
734
+ Object.assign(mergedSchema.additionalProperties, allOfSchema.additionalProperties)
735
+ }
802
736
 
803
- function buildCodeWithAllOfs (location, code, laterCode, name) {
804
- if (location.schema.allOf) {
805
- location.schema.allOf.forEach((ss) => {
806
- const builtCode = buildCodeWithAllOfs(mergeLocation(location, { schema: ss }), code, laterCode, name)
807
- code = builtCode.code
808
- laterCode = builtCode.laterCode
809
- })
810
- } else {
811
- 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
+ }
812
743
 
813
- code = builtCode.code
814
- laterCode = builtCode.laterCode
815
- }
744
+ if (allOfSchema.required !== undefined) {
745
+ if (mergedSchema.required === undefined) {
746
+ mergedSchema.required = []
747
+ }
748
+ mergedSchema.required.push(...allOfSchema.required)
749
+ }
750
+
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
+ }
816
764
 
817
- return { code: code, laterCode: laterCode }
765
+ if (allOfSchema.allOf !== undefined) {
766
+ mergeAllOfSchema(location, allOfSchema, mergedSchema)
767
+ }
768
+ }
769
+ delete mergedSchema.allOf
818
770
  }
819
771
 
820
- function buildInnerObject (location, name) {
772
+ function buildInnerObject (location, locationPath) {
821
773
  const schema = location.schema
822
- const result = buildCodeWithAllOfs(location, '', '', name)
774
+ const result = buildCode(location, '', '', locationPath)
823
775
  if (schema.patternProperties) {
824
- result.code += addPatternProperties(location)
776
+ const { code, laterCode } = addPatternProperties(location)
777
+ result.code += code
778
+ result.laterCode += laterCode
825
779
  } else if (schema.additionalProperties && !schema.patternProperties) {
826
- result.code += addAdditionalProperties(location)
780
+ const { code, laterCode } = addAdditionalProperties(location)
781
+ result.code += code
782
+ result.laterCode += laterCode
827
783
  }
828
784
  return result
829
785
  }
830
786
 
831
- function addIfThenElse (location, name) {
787
+ function addIfThenElse (location, locationPath) {
832
788
  let code = ''
833
789
  let r
834
790
  let laterCode = ''
@@ -853,12 +809,12 @@ function addIfThenElse (location, name) {
853
809
  if (valid) {
854
810
  `
855
811
  if (merged.if && merged.then) {
856
- innerR = addIfThenElse(mergedLocation, name + 'Then')
812
+ innerR = addIfThenElse(mergedLocation, locationPath + 'Then')
857
813
  code += innerR.code
858
814
  laterCode = innerR.laterCode
859
815
  }
860
816
 
861
- r = buildInnerObject(mergedLocation, name + 'Then')
817
+ r = buildInnerObject(mergedLocation, locationPath + 'Then')
862
818
  code += r.code
863
819
  laterCode += r.laterCode
864
820
 
@@ -873,19 +829,19 @@ function addIfThenElse (location, name) {
873
829
  `
874
830
 
875
831
  if (merged.if && merged.then) {
876
- innerR = addIfThenElse(mergedLocation, name + 'Else')
832
+ innerR = addIfThenElse(mergedLocation, locationPath + 'Else')
877
833
  code += innerR.code
878
834
  laterCode += innerR.laterCode
879
835
  }
880
836
 
881
- r = buildInnerObject(mergedLocation, name + 'Else')
837
+ r = buildInnerObject(mergedLocation, locationPath + 'Else')
882
838
  code += r.code
883
839
  laterCode += r.laterCode
884
840
 
885
841
  code += `
886
842
  }
887
843
  `
888
- return { code: code, laterCode: laterCode }
844
+ return { code, laterCode }
889
845
  }
890
846
 
891
847
  function toJSON (variableName) {
@@ -895,11 +851,14 @@ function toJSON (variableName) {
895
851
  `
896
852
  }
897
853
 
898
- function buildObject (location, code, name) {
854
+ function buildObject (location, code, functionName, locationPath) {
899
855
  const schema = location.schema
900
-
856
+ if (schema.$id !== undefined) {
857
+ schemaReferenceMap.set(schema.$id, schema)
858
+ }
901
859
  code += `
902
- function ${name} (input) {
860
+ function ${functionName} (input) {
861
+ // ${locationPath}
903
862
  `
904
863
  if (schema.nullable) {
905
864
  code += `
@@ -909,14 +868,14 @@ function buildObject (location, code, name) {
909
868
  `
910
869
  }
911
870
 
912
- if (objectReferenceSerializersMap.has(schema) && objectReferenceSerializersMap.get(schema) !== name) {
871
+ if (objectReferenceSerializersMap.has(schema) && objectReferenceSerializersMap.get(schema) !== functionName) {
913
872
  code += `
914
873
  return ${objectReferenceSerializersMap.get(schema)}(input)
915
874
  }
916
875
  `
917
876
  return code
918
877
  }
919
- objectReferenceSerializersMap.set(schema, name)
878
+ objectReferenceSerializersMap.set(schema, functionName)
920
879
 
921
880
  code += `
922
881
  var obj = ${toJSON('input')}
@@ -929,9 +888,9 @@ function buildObject (location, code, name) {
929
888
  code += `
930
889
  var valid
931
890
  `
932
- r = addIfThenElse(location, name)
891
+ r = addIfThenElse(location, locationPath)
933
892
  } else {
934
- r = buildInnerObject(location, name)
893
+ r = buildInnerObject(location, locationPath)
935
894
  }
936
895
 
937
896
  // Removes the comma if is the last element of the string (in case there are not properties)
@@ -945,10 +904,14 @@ function buildObject (location, code, name) {
945
904
  return code
946
905
  }
947
906
 
948
- function buildArray (location, code, name, key = null) {
907
+ function buildArray (location, code, functionName, locationPath) {
949
908
  let schema = location.schema
909
+ if (schema.$id !== undefined) {
910
+ schemaReferenceMap.set(schema.$id, schema)
911
+ }
950
912
  code += `
951
- function ${name} (obj) {
913
+ function ${functionName} (obj) {
914
+ // ${locationPath}
952
915
  `
953
916
  if (schema.nullable) {
954
917
  code += `
@@ -981,14 +944,14 @@ function buildArray (location, code, name, key = null) {
981
944
  `
982
945
  return code
983
946
  }
984
- arrayItemsReferenceSerializersMap.set(schema.items, name)
947
+ arrayItemsReferenceSerializersMap.set(schema.items, functionName)
985
948
  }
986
949
 
987
950
  let result = { code: '', laterCode: '' }
988
951
  const accessor = '[i]'
989
952
  if (Array.isArray(schema.items)) {
990
953
  result = schema.items.reduce((res, item, i) => {
991
- 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 }))
992
955
  const condition = `i === ${i} && ${buildArrayTypeCondition(item.type, accessor)}`
993
956
  return {
994
957
  code: `${res.code}
@@ -1001,7 +964,7 @@ function buildArray (location, code, name, key = null) {
1001
964
  }, result)
1002
965
 
1003
966
  if (schema.additionalItems) {
1004
- 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 }))
1005
968
  result.code += `
1006
969
  else if (i >= ${schema.items.length}) {
1007
970
  ${tmpRes.code}
@@ -1015,31 +978,39 @@ function buildArray (location, code, name, key = null) {
1015
978
  }
1016
979
  `
1017
980
  } else {
1018
- result = nested(laterCode, name, accessor, mergeLocation(location, { schema: schema.items }), undefined, true)
981
+ result = buildValue(laterCode, locationPath + accessor, 'obj[i]', mergeLocation(location, { schema: schema.items }))
1019
982
  }
1020
983
 
1021
- if (key) {
1022
- code += `
1023
- if(!Array.isArray(obj)) {
1024
- throw new TypeError(\`Property '${key}' should be of type array, received '$\{obj}' instead.\`)
984
+ code += `
985
+ if (!Array.isArray(obj)) {
986
+ throw new TypeError(\`The value '$\{obj}' does not match schema definition.\`)
987
+ }
988
+ `
989
+
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}`)
1025
996
  }
1026
- `
1027
997
  }
1028
998
 
1029
999
  code += `
1030
- var l = obj.length
1031
- var jsonOutput= ''
1032
- for (var i = 0; i < l; i++) {
1033
- var json = ''
1000
+ let jsonOutput= ''
1001
+ for (let i = 0; i < arrayLength; i++) {
1002
+ let json = ''
1034
1003
  ${result.code}
1035
1004
  jsonOutput += json
1036
1005
 
1037
- if (json.length > 0 && i < l - 1) {
1006
+ if (json.length > 0 && i < arrayLength - 1) {
1038
1007
  jsonOutput += ','
1039
1008
  }
1040
1009
  }
1041
1010
  return \`[\${jsonOutput}]\`
1042
- }
1011
+ }`
1012
+
1013
+ code += `
1043
1014
  ${result.laterCode}
1044
1015
  `
1045
1016
 
@@ -1107,26 +1078,12 @@ function dereferenceOfRefs (location, type) {
1107
1078
  return locations
1108
1079
  }
1109
1080
 
1110
- let strNameCounter = 0
1111
- function asFuncName (str) {
1112
- // only allow chars that can work
1113
- let rep = str.replace(/[^a-zA-Z0-9$_]/g, '')
1114
-
1115
- if (rep.length === 0) {
1116
- return 'anan' + strNameCounter++
1117
- } else if (rep !== str) {
1118
- rep += strNameCounter++
1119
- }
1120
-
1121
- return rep
1081
+ let genFuncNameCounter = 0
1082
+ function generateFuncName () {
1083
+ return 'anonymous' + genFuncNameCounter++
1122
1084
  }
1123
1085
 
1124
- function nested (laterCode, name, key, location, subKey, isArray) {
1125
- let code = ''
1126
- let funcName
1127
-
1128
- subKey = subKey || ''
1129
-
1086
+ function buildValue (laterCode, locationPath, input, location) {
1130
1087
  let schema = location.schema
1131
1088
 
1132
1089
  if (schema.$ref) {
@@ -1140,115 +1097,99 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1140
1097
  }
1141
1098
  }
1142
1099
 
1100
+ if (schema.allOf) {
1101
+ const mergedSchema = clone(schema)
1102
+ mergeAllOfSchema(location, schema, mergedSchema)
1103
+ schema = mergedSchema
1104
+ location.schema = mergedSchema
1105
+ }
1106
+
1143
1107
  const type = schema.type
1144
1108
  const nullable = schema.nullable === true
1145
1109
 
1146
- const accessor = isArray ? key : `[${JSON.stringify(key)}]`
1110
+ let code = ''
1111
+ let funcName
1147
1112
 
1148
1113
  switch (type) {
1149
1114
  case 'null':
1150
1115
  code += `
1151
- json += $asNull()
1116
+ json += serializer.asNull()
1152
1117
  `
1153
1118
  break
1154
1119
  case 'string': {
1155
- const stringSerializer = getStringSerializer(schema.format)
1156
- 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})`
1157
1122
  break
1158
1123
  }
1159
1124
  case 'integer':
1160
- 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})`
1161
1127
  break
1162
1128
  case 'number':
1163
- 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})`
1164
1131
  break
1165
1132
  case 'boolean':
1166
- 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})`
1167
1135
  break
1168
1136
  case 'object':
1169
- funcName = asFuncName(name + key + subKey)
1170
- laterCode = buildObject(location, laterCode, funcName)
1171
- code += `
1172
- json += ${funcName}(obj${accessor})
1173
- `
1137
+ funcName = generateFuncName()
1138
+ laterCode = buildObject(location, laterCode, funcName, locationPath)
1139
+ code += `json += ${funcName}(${input})`
1174
1140
  break
1175
1141
  case 'array':
1176
- funcName = asFuncName('$arr' + name + key + subKey) // eslint-disable-line
1177
- laterCode = buildArray(location, laterCode, funcName, key)
1178
- code += `
1179
- json += ${funcName}(obj${accessor})
1180
- `
1142
+ funcName = generateFuncName()
1143
+ laterCode = buildArray(location, laterCode, funcName, locationPath)
1144
+ code += `json += ${funcName}(${input})`
1181
1145
  break
1182
1146
  case undefined:
1183
- if ('anyOf' in schema) {
1147
+ if (schema.anyOf || schema.oneOf) {
1184
1148
  // beware: dereferenceOfRefs has side effects and changes schema.anyOf
1185
- const anyOfLocations = dereferenceOfRefs(location, 'anyOf')
1186
- anyOfLocations.forEach((location, index) => {
1187
- const nestedResult = nested(laterCode, name, key, location, subKey !== '' ? subKey : 'i' + index, isArray)
1188
- // We need a test serializer as the String serializer will not work with
1189
- // date/time ajv validations
1190
- // see: https://github.com/fastify/fast-json-stringify/issues/325
1191
- const testSerializer = getTestSerializer(location.schema.format)
1192
- const testValue = testSerializer !== undefined ? `${testSerializer}(obj${accessor}, true)` : `obj${accessor}`
1193
-
1149
+ const locations = dereferenceOfRefs(location, schema.anyOf ? 'anyOf' : 'oneOf')
1150
+ locations.forEach((location, index) => {
1151
+ const nestedResult = buildValue(laterCode, locationPath + 'i' + index, input, location)
1194
1152
  // Since we are only passing the relevant schema to ajv.validate, it needs to be full dereferenced
1195
1153
  // otherwise any $ref pointing to an external schema would result in an error.
1196
1154
  // Full dereference of the schema happens as side effect of two functions:
1197
1155
  // 1. `dereferenceOfRefs` loops through the `schema.anyOf`` array and replaces any top level reference
1198
1156
  // with the actual schema
1199
- // 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
1200
1158
  // (see https://github.com/fastify/fast-json-stringify/blob/6da3b3e8ac24b1ca5578223adedb4083b7adf8db/index.js#L631)
1201
1159
 
1202
- const schemaKey = location.schema.$id || randomUUID()
1203
- ajvInstance.addSchema(location.schema, schemaKey)
1204
-
1205
- code += `
1206
- ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${testValue}))
1207
- ${nestedResult.code}
1208
- `
1209
- laterCode = nestedResult.laterCode
1210
- })
1211
- code += `
1212
- else json+= null
1213
- `
1214
- } else if ('oneOf' in schema) {
1215
- // beware: dereferenceOfRefs has side effects and changes schema.oneOf
1216
- const oneOfLocations = dereferenceOfRefs(location, 'oneOf')
1217
- oneOfLocations.forEach((location, index) => {
1218
- const nestedResult = nested(laterCode, name, key, location, subKey !== '' ? subKey : 'i' + index, isArray)
1219
- const testSerializer = getTestSerializer(location.schema.format)
1220
- const testValue = testSerializer !== undefined ? `${testSerializer}(obj${accessor}, true)` : `obj${accessor}`
1221
- // 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)
1222
1165
 
1223
1166
  const schemaKey = location.schema.$id || randomUUID()
1224
- ajvInstance.addSchema(location.schema, schemaKey)
1167
+ ajvInstance.addSchema(extendedSchema, schemaKey)
1225
1168
 
1226
1169
  code += `
1227
- ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${testValue}))
1170
+ ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${input}))
1228
1171
  ${nestedResult.code}
1229
1172
  `
1230
1173
  laterCode = nestedResult.laterCode
1231
1174
  })
1232
1175
 
1233
- if (!isArray) {
1234
- code += `
1235
- else json+= null
1236
- `
1237
- }
1176
+ code += `
1177
+ else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
1178
+ `
1238
1179
  } else if (isEmpty(schema)) {
1239
1180
  code += `
1240
- json += JSON.stringify(obj${accessor})
1181
+ json += JSON.stringify(${input})
1241
1182
  `
1242
1183
  } else if ('const' in schema) {
1243
1184
  code += `
1244
- if(ajv.validate(${JSON.stringify(schema)}, obj${accessor}))
1185
+ if(ajv.validate(${JSON.stringify(schema)}, ${input}))
1245
1186
  json += '${JSON.stringify(schema.const)}'
1246
1187
  else
1247
- 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.\`)
1248
1189
  `
1249
1190
  } else if (schema.type === undefined) {
1250
1191
  code += `
1251
- json += JSON.stringify(obj${accessor})
1192
+ json += JSON.stringify(${input})
1252
1193
  `
1253
1194
  } else {
1254
1195
  throw new Error(`${schema.type} unsupported`)
@@ -1256,51 +1197,46 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1256
1197
  break
1257
1198
  default:
1258
1199
  if (Array.isArray(type)) {
1259
- const nullIndex = type.indexOf('null')
1260
- 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
+
1261
1211
  sortedTypes.forEach((type, index) => {
1262
1212
  const statement = index === 0 ? 'if' : 'else if'
1263
1213
  const tempSchema = Object.assign({}, schema, { type })
1264
- const nestedResult = nested(laterCode, name, key, mergeLocation(location, { schema: tempSchema }), subKey, isArray)
1214
+ const nestedResult = buildValue(laterCode, locationPath, input, mergeLocation(location, { schema: tempSchema }))
1265
1215
  switch (type) {
1266
1216
  case 'string': {
1267
1217
  code += `
1268
- ${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")))
1269
- ${nestedResult.code}
1270
- `
1271
- break
1272
- }
1273
- case 'null': {
1274
- code += `
1275
- ${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")))
1276
1219
  ${nestedResult.code}
1277
1220
  `
1278
1221
  break
1279
1222
  }
1280
1223
  case 'array': {
1281
1224
  code += `
1282
- ${statement}(Array.isArray(obj${accessor}))
1225
+ ${statement}(Array.isArray(${input}))
1283
1226
  ${nestedResult.code}
1284
1227
  `
1285
1228
  break
1286
1229
  }
1287
1230
  case 'integer': {
1288
1231
  code += `
1289
- ${statement}(Number.isInteger(obj${accessor}) || obj${accessor} === null)
1290
- ${nestedResult.code}
1291
- `
1292
- break
1293
- }
1294
- case 'number': {
1295
- code += `
1296
- ${statement}(isNaN(obj${accessor}) === false)
1232
+ ${statement}(Number.isInteger(${input}) || ${input} === null)
1297
1233
  ${nestedResult.code}
1298
1234
  `
1299
1235
  break
1300
1236
  }
1301
1237
  default: {
1302
1238
  code += `
1303
- ${statement}(typeof obj${accessor} === "${type}")
1239
+ ${statement}(typeof ${input} === "${type}" || ${input} === null)
1304
1240
  ${nestedResult.code}
1305
1241
  `
1306
1242
  break
@@ -1309,16 +1245,32 @@ function nested (laterCode, name, key, location, subKey, isArray) {
1309
1245
  laterCode = nestedResult.laterCode
1310
1246
  })
1311
1247
  code += `
1312
- else json+= null
1248
+ else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
1313
1249
  `
1250
+
1251
+ if (nullable) {
1252
+ code += `
1253
+ }
1254
+ `
1255
+ }
1314
1256
  } else {
1315
1257
  throw new Error(`${type} unsupported`)
1316
1258
  }
1317
1259
  }
1318
1260
 
1319
- return {
1320
- code,
1321
- laterCode
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
+ }
1322
1274
  }
1323
1275
  }
1324
1276
 
@@ -1334,8 +1286,11 @@ function isEmpty (schema) {
1334
1286
 
1335
1287
  module.exports = build
1336
1288
 
1289
+ module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms
1290
+
1337
1291
  module.exports.restore = function ({ code, ajv }) {
1292
+ const serializer = new Serializer()
1338
1293
  // eslint-disable-next-line
1339
- return (Function.apply(null, ['ajv', code])
1340
- .apply(null, [ajv]))
1294
+ return (Function.apply(null, ['ajv', 'serializer', code])
1295
+ .apply(null, [ajv, serializer]))
1341
1296
  }