fast-json-stringify 4.0.0 → 5.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
@@ -2,18 +2,16 @@
2
2
 
3
3
  /* eslint no-prototype-builtins: 0 */
4
4
 
5
- const Ajv = require('ajv')
6
- const fastUri = require('fast-uri')
7
- const ajvFormats = require('ajv-formats')
8
5
  const merge = require('deepmerge')
9
6
  const clone = require('rfdc')({ proto: true })
10
7
  const fjsCloned = Symbol('fast-json-stringify.cloned')
11
8
  const { randomUUID } = require('crypto')
12
9
 
13
10
  const validate = require('./schema-validator')
11
+ const Serializer = require('./serializer')
12
+ const buildAjv = require('./ajv')
14
13
 
15
14
  let largeArraySize = 2e4
16
- let stringSimilarity = null
17
15
  let largeArrayMechanism = 'default'
18
16
  const validLargeArrayMechanisms = [
19
17
  'default',
@@ -42,225 +40,83 @@ function isValidSchema (schema, name) {
42
40
  }
43
41
  }
44
42
 
45
- function mergeLocation (source, dest) {
43
+ function mergeLocation (location, key) {
46
44
  return {
47
- schema: dest.schema || source.schema,
48
- root: dest.root || source.root,
49
- externalSchema: dest.externalSchema || source.externalSchema
45
+ schema: location.schema[key],
46
+ schemaId: location.schemaId,
47
+ jsonPointer: location.jsonPointer + '/' + key
50
48
  }
51
49
  }
52
50
 
53
- const arrayItemsReferenceSerializersMap = new Map()
54
- const objectReferenceSerializersMap = new Map()
55
- const schemaReferenceMap = new Map()
56
-
57
- let ajvInstance = null
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)
51
+ function resolveRef (location, ref) {
52
+ let hashIndex = ref.indexOf('#')
53
+ if (hashIndex === -1) {
54
+ hashIndex = ref.length
124
55
  }
125
56
 
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
- }
57
+ const schemaId = ref.slice(0, hashIndex) || location.schemaId
58
+ const jsonPointer = ref.slice(hashIndex)
133
59
 
134
- asDatetimeNullable (date, skipQuotes) {
135
- return date === null ? 'null' : this.asDatetime(date, skipQuotes)
136
- }
60
+ const schemaRef = schemaId + jsonPointer
137
61
 
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)
62
+ let ajvSchema
63
+ try {
64
+ ajvSchema = ajvInstance.getSchema(schemaRef)
65
+ } catch (error) {
66
+ throw new Error(`Cannot find reference "${ref}"`)
148
67
  }
149
68
 
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)
69
+ if (ajvSchema === undefined) {
70
+ throw new Error(`Cannot find reference "${ref}"`)
156
71
  }
157
72
 
158
- asTimeNullable (date, skipQuotes) {
159
- return date === null ? 'null' : this.asTime(date, skipQuotes)
73
+ const schema = ajvSchema.schema
74
+ if (schema.$ref !== undefined) {
75
+ return resolveRef({ schema, schemaId, jsonPointer }, schema.$ref)
160
76
  }
161
77
 
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
- }
78
+ return { schema, schemaId, jsonPointer }
79
+ }
189
80
 
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
- }
81
+ const arrayItemsReferenceSerializersMap = new Map()
82
+ const objectReferenceSerializersMap = new Map()
216
83
 
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
- }
84
+ let rootSchemaId = null
85
+ let ajvInstance = null
86
+ let contextFunctions = null
225
87
 
226
88
  function build (schema, options) {
89
+ schema = clone(schema)
90
+
227
91
  arrayItemsReferenceSerializersMap.clear()
228
92
  objectReferenceSerializersMap.clear()
229
- schemaReferenceMap.clear()
230
93
 
94
+ contextFunctions = []
231
95
  options = options || {}
232
96
 
233
- ajvInstance = new Ajv({ ...options.ajv, strictSchema: false, uriResolver: fastUri })
234
- ajvFormats(ajvInstance)
97
+ ajvInstance = buildAjv(options.ajv)
98
+ rootSchemaId = schema.$id || randomUUID()
235
99
 
236
- const validateDateTimeFormat = ajvFormats.get('date-time').validate
237
- const validateDateFormat = ajvFormats.get('date').validate
238
- const validateTimeFormat = ajvFormats.get('time').validate
100
+ isValidSchema(schema)
101
+ extendDateTimeType(schema)
102
+ ajvInstance.addSchema(schema, rootSchemaId)
239
103
 
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)
104
+ if (options.schema) {
105
+ options.schema = clone(options.schema)
106
+
107
+ for (const key of Object.keys(options.schema)) {
108
+ const externalSchema = options.schema[key]
109
+ isValidSchema(externalSchema, key)
110
+ extendDateTimeType(externalSchema)
111
+
112
+ let schemaKey = externalSchema.$id || key
113
+ if (externalSchema.$id !== undefined && externalSchema.$id[0] === '#') {
114
+ schemaKey = key + externalSchema.$id // relative URI
254
115
  }
255
- return false
256
- }
257
- })
258
116
 
259
- isValidSchema(schema)
260
- if (options.schema) {
261
- // eslint-disable-next-line
262
- for (var key of Object.keys(options.schema)) {
263
- isValidSchema(options.schema[key], key)
117
+ if (ajvInstance.getSchema(schemaKey) === undefined) {
118
+ ajvInstance.addSchema(externalSchema, schemaKey)
119
+ }
264
120
  }
265
121
  }
266
122
 
@@ -288,47 +144,44 @@ function build (schema, options) {
288
144
 
289
145
  const serializer = new Serializer(options)
290
146
 
291
- let location = {
292
- schema,
293
- root: schema,
294
- externalSchema: options.schema
295
- }
296
-
297
- if (schema.$ref) {
298
- location = refFinder(schema.$ref, location)
299
- schema = location.schema
300
- }
301
-
302
- if (schema.type === undefined) {
303
- schema.type = inferTypeByKeyword(schema)
304
- }
147
+ const location = { schema, schemaId: rootSchemaId, jsonPointer: '#' }
148
+ const code = buildValue(location, 'input')
305
149
 
306
- const { code, laterCode } = buildValue('', 'main', 'input', location)
307
150
  const contextFunctionCode = `
308
- 'use strict'
309
151
  function main (input) {
310
152
  let json = ''
311
153
  ${code}
312
154
  return json
313
155
  }
314
- ${laterCode}
156
+ ${contextFunctions.join('\n')}
315
157
  return main
316
158
  `
317
159
 
318
160
  const dependenciesName = ['ajv', 'serializer', contextFunctionCode]
319
161
 
320
162
  if (options.debugMode) {
163
+ options.mode = 'debug'
164
+ }
165
+
166
+ if (options.mode === 'debug') {
321
167
  return { code: dependenciesName.join('\n'), ajv: ajvInstance }
322
168
  }
323
169
 
170
+ if (options.mode === 'standalone') {
171
+ // lazy load
172
+ const buildStandaloneCode = require('./standalone')
173
+ return buildStandaloneCode(options, ajvInstance, contextFunctionCode)
174
+ }
175
+
324
176
  /* eslint no-new-func: "off" */
325
177
  const contextFunc = new Function('ajv', 'serializer', contextFunctionCode)
326
178
  const stringifyFunc = contextFunc(ajvInstance, serializer)
327
179
 
328
180
  ajvInstance = null
181
+ rootSchemaId = null
182
+ contextFunctions = null
329
183
  arrayItemsReferenceSerializersMap.clear()
330
184
  objectReferenceSerializersMap.clear()
331
- schemaReferenceMap.clear()
332
185
 
333
186
  return stringifyFunc
334
187
  }
@@ -408,12 +261,12 @@ function addPatternProperties (location) {
408
261
  for (var i = 0; i < keys.length; i++) {
409
262
  if (properties[keys[i]]) continue
410
263
  `
411
- let laterCode = ''
412
264
 
413
- Object.keys(pp).forEach((regex, index) => {
414
- let ppLocation = mergeLocation(location, { schema: pp[regex] })
265
+ const patternPropertiesLocation = mergeLocation(location, 'patternProperties')
266
+ Object.keys(pp).forEach((regex) => {
267
+ let ppLocation = mergeLocation(patternPropertiesLocation, regex)
415
268
  if (pp[regex].$ref) {
416
- ppLocation = refFinder(pp[regex].$ref, location)
269
+ ppLocation = resolveRef(ppLocation, pp[regex].$ref)
417
270
  pp[regex] = ppLocation.schema
418
271
  }
419
272
 
@@ -423,27 +276,24 @@ function addPatternProperties (location) {
423
276
  throw new Error(`${err.message}. Found at ${regex} matching ${JSON.stringify(pp[regex])}`)
424
277
  }
425
278
 
426
- const valueCode = buildValue('', '', 'obj[keys[i]]', ppLocation)
427
- laterCode += valueCode.laterCode
279
+ const valueCode = buildValue(ppLocation, 'obj[keys[i]]')
428
280
  code += `
429
281
  if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {
430
282
  ${addComma}
431
283
  json += serializer.asString(keys[i]) + ':'
432
- ${valueCode.code}
284
+ ${valueCode}
433
285
  continue
434
286
  }
435
287
  `
436
288
  })
437
289
  if (schema.additionalProperties) {
438
- const additionalPropertyCode = additionalProperty(location)
439
- code += additionalPropertyCode.code
440
- laterCode += additionalPropertyCode.laterCode
290
+ code += additionalProperty(location)
441
291
  }
442
292
 
443
293
  code += `
444
294
  }
445
295
  `
446
- return { code, laterCode }
296
+ return code
447
297
  }
448
298
 
449
299
  function additionalProperty (location) {
@@ -457,182 +307,51 @@ function additionalProperty (location) {
457
307
  }
458
308
  `
459
309
 
460
- return { code, laterCode: '' }
310
+ return code
461
311
  }
462
- let apLocation = mergeLocation(location, { schema: ap })
312
+ let apLocation = mergeLocation(location, 'additionalProperties')
463
313
  if (ap.$ref) {
464
- apLocation = refFinder(ap.$ref, location)
314
+ apLocation = resolveRef(location, ap.$ref)
465
315
  ap = apLocation.schema
466
316
  }
467
317
 
468
- const valueCode = buildValue('', '', 'obj[keys[i]]', apLocation)
318
+ const valueCode = buildValue(apLocation, 'obj[keys[i]]')
469
319
 
470
320
  code += `
471
321
  ${addComma}
472
322
  json += serializer.asString(keys[i]) + ':'
473
- ${valueCode.code}
323
+ ${valueCode}
474
324
  `
475
325
 
476
- return { code, laterCode: valueCode.laterCode }
326
+ return code
477
327
  }
478
328
 
479
329
  function addAdditionalProperties (location) {
480
- const additionalPropertyCode = additionalProperty(location)
481
- const code = `
330
+ return `
482
331
  var properties = ${JSON.stringify(location.schema.properties)} || {}
483
332
  var keys = Object.keys(obj)
484
333
  for (var i = 0; i < keys.length; i++) {
485
334
  if (properties[keys[i]]) continue
486
- ${additionalPropertyCode.code}
335
+ ${additionalProperty(location)}
487
336
  }
488
337
  `
489
-
490
- return { code, laterCode: additionalPropertyCode.laterCode }
491
- }
492
-
493
- function idFinder (schema, searchedId) {
494
- let objSchema
495
- const explore = (schema, searchedId) => {
496
- Object.keys(schema || {}).forEach((key, i, a) => {
497
- if (key === '$id' && schema[key] === searchedId) {
498
- objSchema = schema
499
- } else if (objSchema === undefined && typeof schema[key] === 'object') {
500
- explore(schema[key], searchedId)
501
- }
502
- })
503
- }
504
- explore(schema, searchedId)
505
- return objSchema
506
- }
507
-
508
- function refFinder (ref, location) {
509
- const externalSchema = location.externalSchema
510
- let root = location.root
511
- let schema = location.schema
512
-
513
- if (externalSchema && externalSchema[ref]) {
514
- return {
515
- schema: externalSchema[ref],
516
- root: externalSchema[ref],
517
- externalSchema
518
- }
519
- }
520
-
521
- // Split file from walk
522
- ref = ref.split('#')
523
-
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
536
- schema = externalSchema[ref[0]]
537
- root = externalSchema[ref[0]]
538
-
539
- if (schema === undefined) {
540
- findBadKey(externalSchema, [ref[0]])
541
- }
542
-
543
- if (schema.$ref) {
544
- return refFinder(schema.$ref, {
545
- schema,
546
- root,
547
- externalSchema
548
- })
549
- }
550
- }
551
-
552
- let code = 'return schema'
553
- // If it has a path
554
- if (ref[1]) {
555
- // ref[1] could contain a JSON pointer - ex: /definitions/num
556
- // or plain name fragment id without suffix # - ex: customId
557
- const walk = ref[1].split('/')
558
- if (walk.length === 1) {
559
- const targetId = `#${ref[1]}`
560
- let dereferenced = idFinder(schema, targetId)
561
- if (dereferenced === undefined && !ref[0]) {
562
- // eslint-disable-next-line
563
- for (var key of Object.keys(externalSchema)) {
564
- dereferenced = idFinder(externalSchema[key], targetId)
565
- if (dereferenced !== undefined) {
566
- root = externalSchema[key]
567
- break
568
- }
569
- }
570
- }
571
-
572
- return {
573
- schema: dereferenced,
574
- root,
575
- externalSchema
576
- }
577
- } else {
578
- // eslint-disable-next-line
579
- for (var i = 1; i < walk.length; i++) {
580
- code += `[${JSON.stringify(walk[i])}]`
581
- }
582
- }
583
- }
584
- let result
585
- try {
586
- result = (new Function('schema', code))(root)
587
- } catch (err) {}
588
-
589
- if (result === undefined && ref[1]) {
590
- const walk = ref[1].split('/')
591
- findBadKey(schema, walk.slice(1))
592
- }
593
-
594
- if (result.$ref) {
595
- return refFinder(result.$ref, {
596
- schema,
597
- root,
598
- externalSchema
599
- })
600
- }
601
-
602
- return {
603
- schema: result,
604
- root,
605
- externalSchema
606
- }
607
-
608
- function findBadKey (obj, keys) {
609
- if (keys.length === 0) return null
610
- const key = keys.shift()
611
- if (obj[key] === undefined) {
612
- stringSimilarity = stringSimilarity || require('string-similarity')
613
- const { bestMatch } = stringSimilarity.findBestMatch(key, Object.keys(obj))
614
- if (bestMatch.rating >= 0.5) {
615
- throw new Error(`Cannot find reference ${JSON.stringify(key)}, did you mean ${JSON.stringify(bestMatch.target)}?`)
616
- } else {
617
- throw new Error(`Cannot find reference ${JSON.stringify(key)}`)
618
- }
619
- }
620
- return findBadKey(obj[key], keys)
621
- }
622
338
  }
623
339
 
624
- function buildCode (location, code, laterCode, locationPath) {
340
+ function buildCode (location) {
625
341
  if (location.schema.$ref) {
626
- location = refFinder(location.schema.$ref, location)
342
+ location = resolveRef(location, location.schema.$ref)
627
343
  }
628
344
 
629
345
  const schema = location.schema
630
346
  const required = schema.required || []
631
347
 
348
+ let code = ''
349
+
350
+ const propertiesLocation = mergeLocation(location, 'properties')
632
351
  Object.keys(schema.properties || {}).forEach((key) => {
633
- let propertyLocation = mergeLocation(location, { schema: schema.properties[key] })
352
+ let propertyLocation = mergeLocation(propertiesLocation, key)
634
353
  if (schema.properties[key].$ref) {
635
- propertyLocation = refFinder(schema.properties[key].$ref, location)
354
+ propertyLocation = resolveRef(location, schema.properties[key].$ref)
636
355
  schema.properties[key] = propertyLocation.schema
637
356
  }
638
357
 
@@ -648,9 +367,7 @@ function buildCode (location, code, laterCode, locationPath) {
648
367
  json += ${asString} + ':'
649
368
  `
650
369
 
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
370
+ code += buildValue(propertyLocation, `obj[${JSON.stringify(key)}]`)
654
371
 
655
372
  const defaultValue = schema.properties[key].default
656
373
  if (defaultValue !== undefined) {
@@ -676,13 +393,18 @@ function buildCode (location, code, laterCode, locationPath) {
676
393
  code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
677
394
  }
678
395
 
679
- return { code, laterCode }
396
+ return code
680
397
  }
681
398
 
682
399
  function mergeAllOfSchema (location, schema, mergedSchema) {
683
- for (let allOfSchema of schema.allOf) {
400
+ const allOfLocation = mergeLocation(location, 'allOf')
401
+
402
+ for (let i = 0; i < schema.allOf.length; i++) {
403
+ let allOfSchema = schema.allOf[i]
404
+
684
405
  if (allOfSchema.$ref) {
685
- allOfSchema = refFinder(allOfSchema.$ref, mergeLocation(location, { schema: allOfSchema })).schema
406
+ const allOfSchemaLocation = mergeLocation(allOfLocation, i)
407
+ allOfSchema = resolveRef(allOfSchemaLocation, allOfSchema.$ref).schema
686
408
  }
687
409
 
688
410
  let allOfSchemaType = allOfSchema.type
@@ -769,79 +491,59 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
769
491
  delete mergedSchema.allOf
770
492
  }
771
493
 
772
- function buildInnerObject (location, locationPath) {
494
+ function buildInnerObject (location) {
773
495
  const schema = location.schema
774
- const result = buildCode(location, '', '', locationPath)
496
+ let code = buildCode(location)
775
497
  if (schema.patternProperties) {
776
- const { code, laterCode } = addPatternProperties(location)
777
- result.code += code
778
- result.laterCode += laterCode
498
+ code += addPatternProperties(location)
779
499
  } else if (schema.additionalProperties && !schema.patternProperties) {
780
- const { code, laterCode } = addAdditionalProperties(location)
781
- result.code += code
782
- result.laterCode += laterCode
500
+ code += addAdditionalProperties(location)
783
501
  }
784
- return result
502
+ return code
785
503
  }
786
504
 
787
- function addIfThenElse (location, locationPath) {
788
- let code = ''
789
- let r
790
- let laterCode = ''
791
- let innerR
505
+ function addIfThenElse (location) {
506
+ const schema = merge({}, location.schema)
507
+ const thenSchema = schema.then
508
+ const elseSchema = schema.else || { additionalProperties: true }
792
509
 
793
- const schema = location.schema
794
- const copy = merge({}, schema)
795
- const i = copy.if
796
- const then = copy.then
797
- const e = copy.else ? copy.else : { additionalProperties: true }
798
- delete copy.if
799
- delete copy.then
800
- delete copy.else
801
- let merged = merge(copy, then)
802
- let mergedLocation = mergeLocation(location, { schema: merged })
803
-
804
- const schemaKey = i.$id || randomUUID()
805
- ajvInstance.addSchema(i, schemaKey)
510
+ delete schema.if
511
+ delete schema.then
512
+ delete schema.else
806
513
 
807
- code += `
808
- valid = ajv.validate("${schemaKey}", obj)
809
- if (valid) {
514
+ const ifLocation = mergeLocation(location, 'if')
515
+ const ifSchemaRef = ifLocation.schemaId + ifLocation.jsonPointer
516
+
517
+ let code = `
518
+ if (ajv.validate("${ifSchemaRef}", obj)) {
810
519
  `
811
- if (merged.if && merged.then) {
812
- innerR = addIfThenElse(mergedLocation, locationPath + 'Then')
813
- code += innerR.code
814
- laterCode = innerR.laterCode
815
- }
816
520
 
817
- r = buildInnerObject(mergedLocation, locationPath + 'Then')
818
- code += r.code
819
- laterCode += r.laterCode
521
+ const thenLocation = mergeLocation(location, 'then')
522
+ thenLocation.schema = merge(schema, thenSchema)
820
523
 
524
+ if (thenSchema.if && thenSchema.then) {
525
+ code += addIfThenElse(thenLocation)
526
+ }
527
+ code += buildInnerObject(thenLocation)
821
528
  code += `
822
529
  }
823
530
  `
824
- merged = merge(copy, e)
825
- mergedLocation = mergeLocation(mergedLocation, { schema: merged })
531
+
532
+ const elseLocation = mergeLocation(location, 'else')
533
+ elseLocation.schema = merge(schema, elseSchema)
826
534
 
827
535
  code += `
828
536
  else {
829
537
  `
830
538
 
831
- if (merged.if && merged.then) {
832
- innerR = addIfThenElse(mergedLocation, locationPath + 'Else')
833
- code += innerR.code
834
- laterCode += innerR.laterCode
539
+ if (elseSchema.if && elseSchema.then) {
540
+ code += addIfThenElse(elseLocation)
835
541
  }
836
-
837
- r = buildInnerObject(mergedLocation, locationPath + 'Else')
838
- code += r.code
839
- laterCode += r.laterCode
840
-
542
+ code += buildInnerObject(elseLocation)
841
543
  code += `
842
544
  }
843
545
  `
844
- return { code, laterCode }
546
+ return code
845
547
  }
846
548
 
847
549
  function toJSON (variableName) {
@@ -851,82 +553,63 @@ function toJSON (variableName) {
851
553
  `
852
554
  }
853
555
 
854
- function buildObject (location, code, functionName, locationPath) {
556
+ function buildObject (location) {
855
557
  const schema = location.schema
856
- if (schema.$id !== undefined) {
857
- schemaReferenceMap.set(schema.$id, schema)
558
+
559
+ if (objectReferenceSerializersMap.has(schema)) {
560
+ return objectReferenceSerializersMap.get(schema)
858
561
  }
859
- code += `
562
+
563
+ const functionName = generateFuncName()
564
+ objectReferenceSerializersMap.set(schema, functionName)
565
+
566
+ const schemaId = location.schemaId === rootSchemaId ? '' : location.schemaId
567
+ let functionCode = `
860
568
  function ${functionName} (input) {
861
- // ${locationPath}
569
+ // ${schemaId + location.jsonPointer}
862
570
  `
863
571
  if (schema.nullable) {
864
- code += `
865
- if(input === null) {
572
+ functionCode += `
573
+ if (input === null) {
866
574
  return 'null';
867
575
  }
868
576
  `
869
577
  }
870
578
 
871
- if (objectReferenceSerializersMap.has(schema) && objectReferenceSerializersMap.get(schema) !== functionName) {
872
- code += `
873
- return ${objectReferenceSerializersMap.get(schema)}(input)
874
- }
875
- `
876
- return code
877
- }
878
- objectReferenceSerializersMap.set(schema, functionName)
879
-
880
- code += `
579
+ functionCode += `
881
580
  var obj = ${toJSON('input')}
882
581
  var json = '{'
883
582
  var addComma = false
884
583
  `
885
584
 
886
- let r
585
+ let rCode
887
586
  if (schema.if && schema.then) {
888
- code += `
889
- var valid
890
- `
891
- r = addIfThenElse(location, locationPath)
587
+ rCode = addIfThenElse(location)
892
588
  } else {
893
- r = buildInnerObject(location, locationPath)
589
+ rCode = buildInnerObject(location)
894
590
  }
895
591
 
896
592
  // Removes the comma if is the last element of the string (in case there are not properties)
897
- code += `${r.code}
593
+ functionCode += `${rCode}
898
594
  json += '}'
899
595
  return json
900
596
  }
901
- ${r.laterCode}
902
597
  `
903
598
 
904
- return code
599
+ contextFunctions.push(functionCode)
600
+ return functionName
905
601
  }
906
602
 
907
- function buildArray (location, code, functionName, locationPath) {
603
+ function buildArray (location) {
908
604
  let schema = location.schema
909
- if (schema.$id !== undefined) {
910
- schemaReferenceMap.set(schema.$id, schema)
911
- }
912
- code += `
913
- function ${functionName} (obj) {
914
- // ${locationPath}
915
- `
916
- if (schema.nullable) {
917
- code += `
918
- if(obj === null) {
919
- return 'null';
920
- }
921
- `
922
- }
923
- const laterCode = ''
924
605
 
925
606
  // default to any items type
926
607
  if (!schema.items) {
927
608
  schema.items = {}
928
609
  }
929
610
 
611
+ let itemsLocation = mergeLocation(location, 'items')
612
+
930
613
  if (schema.items.$ref) {
931
614
  if (!schema[fjsCloned]) {
932
615
  location.schema = clone(location.schema)
@@ -934,87 +617,108 @@ function buildArray (location, code, functionName, locationPath) {
934
617
  schema[fjsCloned] = true
935
618
  }
936
619
 
937
- location = refFinder(schema.items.$ref, location)
620
+ location = resolveRef(location, schema.items.$ref)
621
+ itemsLocation = location
938
622
  schema.items = location.schema
623
+ }
939
624
 
940
- if (arrayItemsReferenceSerializersMap.has(schema.items)) {
941
- code += `
942
- return ${arrayItemsReferenceSerializersMap.get(schema.items)}(obj)
943
- }
944
- `
945
- return code
946
- }
947
- arrayItemsReferenceSerializersMap.set(schema.items, functionName)
625
+ if (arrayItemsReferenceSerializersMap.has(schema.items)) {
626
+ return arrayItemsReferenceSerializersMap.get(schema.items)
948
627
  }
949
628
 
950
- let result = { code: '', laterCode: '' }
951
- const accessor = '[i]'
952
- if (Array.isArray(schema.items)) {
953
- result = schema.items.reduce((res, item, i) => {
954
- const tmpRes = buildValue(laterCode, locationPath + accessor + i, 'obj[i]', mergeLocation(location, { schema: item }))
955
- const condition = `i === ${i} && ${buildArrayTypeCondition(item.type, accessor)}`
956
- return {
957
- code: `${res.code}
958
- ${i > 0 ? 'else' : ''} if (${condition}) {
959
- ${tmpRes.code}
960
- }`,
961
- laterCode: `${res.laterCode}
962
- ${tmpRes.laterCode}`
963
- }
964
- }, result)
629
+ const functionName = generateFuncName()
630
+ arrayItemsReferenceSerializersMap.set(schema.items, functionName)
965
631
 
966
- if (schema.additionalItems) {
967
- const tmpRes = buildValue(laterCode, locationPath + accessor, 'obj[i]', mergeLocation(location, { schema: schema.items }))
968
- result.code += `
969
- else if (i >= ${schema.items.length}) {
970
- ${tmpRes.code}
971
- }
972
- `
973
- }
632
+ const schemaId = location.schemaId === rootSchemaId ? '' : location.schemaId
633
+ let functionCode = `
634
+ function ${functionName} (obj) {
635
+ // ${schemaId + location.jsonPointer}
636
+ `
974
637
 
975
- result.code += `
976
- else {
977
- throw new Error(\`Item at $\{i} does not match schema definition.\`)
978
- }
638
+ if (schema.nullable) {
639
+ functionCode += `
640
+ if (obj === null) {
641
+ return 'null';
642
+ }
979
643
  `
980
- } else {
981
- result = buildValue(laterCode, locationPath + accessor, 'obj[i]', mergeLocation(location, { schema: schema.items }))
982
644
  }
983
645
 
984
- code += `
646
+ functionCode += `
985
647
  if (!Array.isArray(obj)) {
986
648
  throw new TypeError(\`The value '$\{obj}' does not match schema definition.\`)
987
649
  }
650
+ const arrayLength = obj.length
988
651
  `
989
652
 
990
- code += 'const arrayLength = obj.length\n'
653
+ if (!schema.additionalItems) {
654
+ functionCode += `
655
+ if (arrayLength > ${schema.items.length}) {
656
+ throw new Error(\`Item at ${schema.items.length} does not match schema definition.\`)
657
+ }
658
+ `
659
+ }
660
+
991
661
  if (largeArrayMechanism !== 'default') {
992
662
  if (largeArrayMechanism === 'json-stringify') {
993
- code += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
663
+ functionCode += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
994
664
  } else {
995
665
  throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`)
996
666
  }
997
667
  }
998
668
 
999
- code += `
1000
- let jsonOutput= ''
1001
- for (let i = 0; i < arrayLength; i++) {
1002
- let json = ''
1003
- ${result.code}
1004
- jsonOutput += json
669
+ functionCode += `
670
+ let jsonOutput = ''
671
+ `
1005
672
 
1006
- if (json.length > 0 && i < arrayLength - 1) {
1007
- jsonOutput += ','
1008
- }
673
+ if (Array.isArray(schema.items)) {
674
+ for (let i = 0; i < schema.items.length; i++) {
675
+ const item = schema.items[i]
676
+ const tmpRes = buildValue(mergeLocation(itemsLocation, i), `obj[${i}]`)
677
+ functionCode += `
678
+ if (${i} < arrayLength) {
679
+ if (${buildArrayTypeCondition(item.type, `[${i}]`)}) {
680
+ let json = ''
681
+ ${tmpRes}
682
+ jsonOutput += json
683
+ if (${i} < arrayLength - 1) {
684
+ jsonOutput += ','
685
+ }
686
+ } else {
687
+ throw new Error(\`Item at ${i} does not match schema definition.\`)
688
+ }
689
+ }
690
+ `
691
+ }
692
+
693
+ if (schema.additionalItems) {
694
+ functionCode += `
695
+ for (let i = ${schema.items.length}; i < arrayLength; i++) {
696
+ let json = JSON.stringify(obj[i])
697
+ jsonOutput += json
698
+ if (i < arrayLength - 1) {
699
+ jsonOutput += ','
700
+ }
701
+ }`
1009
702
  }
703
+ } else {
704
+ const code = buildValue(itemsLocation, 'obj[i]')
705
+ functionCode += `
706
+ for (let i = 0; i < arrayLength; i++) {
707
+ let json = ''
708
+ ${code}
709
+ jsonOutput += json
710
+ if (i < arrayLength - 1) {
711
+ jsonOutput += ','
712
+ }
713
+ }`
714
+ }
715
+
716
+ functionCode += `
1010
717
  return \`[\${jsonOutput}]\`
1011
718
  }`
1012
719
 
1013
- code += `
1014
- ${result.laterCode}
1015
- `
1016
-
1017
- return code
720
+ contextFunctions.push(functionCode)
721
+ return functionName
1018
722
  }
1019
723
 
1020
724
  function buildArrayTypeCondition (type, accessor) {
@@ -1054,40 +758,17 @@ function buildArrayTypeCondition (type, accessor) {
1054
758
  return condition
1055
759
  }
1056
760
 
1057
- function dereferenceOfRefs (location, type) {
1058
- if (!location.schema[fjsCloned]) {
1059
- const schemaClone = clone(location.schema)
1060
- schemaClone[fjsCloned] = true
1061
- location.schema = schemaClone
1062
- }
1063
-
1064
- const schema = location.schema
1065
- const locations = []
1066
-
1067
- schema[type].forEach((s, index) => {
1068
- // follow the refs
1069
- let sLocation = mergeLocation(location, { schema: s })
1070
- while (s.$ref) {
1071
- sLocation = refFinder(s.$ref, sLocation)
1072
- schema[type][index] = sLocation.schema
1073
- s = schema[type][index]
1074
- }
1075
- locations[index] = sLocation
1076
- })
1077
-
1078
- return locations
1079
- }
1080
-
1081
761
  let genFuncNameCounter = 0
1082
762
  function generateFuncName () {
1083
763
  return 'anonymous' + genFuncNameCounter++
1084
764
  }
1085
765
 
1086
- function buildValue (laterCode, locationPath, input, location) {
766
+ function buildValue (location, input) {
1087
767
  let schema = location.schema
1088
768
 
1089
769
  if (schema.$ref) {
1090
- schema = refFinder(schema.$ref, location)
770
+ location = resolveRef(location, schema.$ref)
771
+ schema = location.schema
1091
772
  }
1092
773
 
1093
774
  if (schema.type === undefined) {
@@ -1134,44 +815,32 @@ function buildValue (laterCode, locationPath, input, location) {
1134
815
  code += `json += ${funcName}(${input})`
1135
816
  break
1136
817
  case 'object':
1137
- funcName = generateFuncName()
1138
- laterCode = buildObject(location, laterCode, funcName, locationPath)
818
+ funcName = buildObject(location)
1139
819
  code += `json += ${funcName}(${input})`
1140
820
  break
1141
821
  case 'array':
1142
- funcName = generateFuncName()
1143
- laterCode = buildArray(location, laterCode, funcName, locationPath)
822
+ funcName = buildArray(location)
1144
823
  code += `json += ${funcName}(${input})`
1145
824
  break
1146
825
  case undefined:
1147
- if (schema.anyOf || schema.oneOf) {
826
+ if (schema.fjs_date_type) {
827
+ funcName = getStringSerializer(schema.fjs_date_type, nullable)
828
+ code += `json += ${funcName}(${input})`
829
+ break
830
+ } else if (schema.anyOf || schema.oneOf) {
1148
831
  // beware: dereferenceOfRefs has side effects and changes schema.anyOf
1149
- const locations = dereferenceOfRefs(location, schema.anyOf ? 'anyOf' : 'oneOf')
1150
- locations.forEach((location, index) => {
1151
- const nestedResult = buildValue(laterCode, locationPath + 'i' + index, input, location)
1152
- // Since we are only passing the relevant schema to ajv.validate, it needs to be full dereferenced
1153
- // otherwise any $ref pointing to an external schema would result in an error.
1154
- // Full dereference of the schema happens as side effect of two functions:
1155
- // 1. `dereferenceOfRefs` loops through the `schema.anyOf`` array and replaces any top level reference
1156
- // with the actual schema
1157
- // 2. `buildValue`, through `buildCode`, replaces any reference in object properties with the actual schema
1158
- // (see https://github.com/fastify/fast-json-stringify/blob/6da3b3e8ac24b1ca5578223adedb4083b7adf8db/index.js#L631)
1159
-
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)
1165
-
1166
- const schemaKey = location.schema.$id || randomUUID()
1167
- ajvInstance.addSchema(extendedSchema, schemaKey)
832
+ const type = schema.anyOf ? 'anyOf' : 'oneOf'
833
+ const anyOfLocation = mergeLocation(location, type)
1168
834
 
835
+ for (let index = 0; index < location.schema[type].length; index++) {
836
+ const optionLocation = mergeLocation(anyOfLocation, index)
837
+ const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer
838
+ const nestedResult = buildValue(optionLocation, input)
1169
839
  code += `
1170
- ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${input}))
1171
- ${nestedResult.code}
840
+ ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaRef}", ${input}))
841
+ ${nestedResult}
1172
842
  `
1173
- laterCode = nestedResult.laterCode
1174
- })
843
+ }
1175
844
 
1176
845
  code += `
1177
846
  else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
@@ -1208,41 +877,41 @@ function buildValue (laterCode, locationPath, input, location) {
1208
877
  } else {`
1209
878
  }
1210
879
 
880
+ const locationClone = clone(location)
1211
881
  sortedTypes.forEach((type, index) => {
1212
882
  const statement = index === 0 ? 'if' : 'else if'
1213
- const tempSchema = Object.assign({}, schema, { type })
1214
- const nestedResult = buildValue(laterCode, locationPath, input, mergeLocation(location, { schema: tempSchema }))
883
+ locationClone.schema.type = type
884
+ const nestedResult = buildValue(locationClone, input)
1215
885
  switch (type) {
1216
886
  case 'string': {
1217
887
  code += `
1218
888
  ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof Date || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
1219
- ${nestedResult.code}
889
+ ${nestedResult}
1220
890
  `
1221
891
  break
1222
892
  }
1223
893
  case 'array': {
1224
894
  code += `
1225
895
  ${statement}(Array.isArray(${input}))
1226
- ${nestedResult.code}
896
+ ${nestedResult}
1227
897
  `
1228
898
  break
1229
899
  }
1230
900
  case 'integer': {
1231
901
  code += `
1232
902
  ${statement}(Number.isInteger(${input}) || ${input} === null)
1233
- ${nestedResult.code}
903
+ ${nestedResult}
1234
904
  `
1235
905
  break
1236
906
  }
1237
907
  default: {
1238
908
  code += `
1239
909
  ${statement}(typeof ${input} === "${type}" || ${input} === null)
1240
- ${nestedResult.code}
910
+ ${nestedResult}
1241
911
  `
1242
912
  break
1243
913
  }
1244
914
  }
1245
- laterCode = nestedResult.laterCode
1246
915
  })
1247
916
  code += `
1248
917
  else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
@@ -1258,10 +927,15 @@ function buildValue (laterCode, locationPath, input, location) {
1258
927
  }
1259
928
  }
1260
929
 
1261
- return { code, laterCode }
930
+ return code
1262
931
  }
1263
932
 
933
+ // Ajv does not support js date format. In order to properly validate objects containing a date,
934
+ // it needs to replace all occurrences of the string date format with a custom keyword fjs_date_type.
935
+ // (see https://github.com/fastify/fast-json-stringify/pull/441)
1264
936
  function extendDateTimeType (schema) {
937
+ if (schema === null) return
938
+
1265
939
  if (schema.type === 'string' && ['date-time', 'date', 'time'].includes(schema.format)) {
1266
940
  schema.fjs_date_type = schema.format
1267
941
  delete schema.type