fast-json-stringify 4.0.0 → 4.1.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/README.md CHANGED
@@ -62,6 +62,8 @@ compile-json-stringify date format x 1,086,187 ops/sec ±0.16% (99 runs sampled)
62
62
  - <a href="#nullable">`Nullable`</a>
63
63
  - <a href="#largearrays">`Large Arrays`</a>
64
64
  - <a href="#security">`Security Notice`</a>
65
+ - <a href="#debug">`Debug Mode`</a>
66
+ - <a href="#standalone">`Standalone Mode`</a>
65
67
  - <a href="#acknowledgements">`Acknowledgements`</a>
66
68
  - <a href="#license">`License`</a>
67
69
 
@@ -663,13 +665,37 @@ const debugCompiled = fastJson({
663
665
  type: 'string'
664
666
  }
665
667
  }
666
- }, { debugMode: true })
668
+ }, { mode: 'debug' })
667
669
 
668
- console.log(debugCompiled) // it is an array of functions that can create your `stringify` function
669
- console.log(debugCompiled.toString()) // print a "ready to read" string function, you can save it to a file
670
+ console.log(debugCompiled) // it is a object contain code, ajv instance
671
+ const rawString = debugCompiled.code // it is the generated code
672
+ console.log(rawString)
670
673
 
671
- const rawString = debugCompiled.toString()
672
- const stringify = fastJson.restore(rawString) // use the generated string to get back the `stringify` function
674
+ const stringify = fastJson.restore(debugCompiled) // use the generated string to get back the `stringify` function
675
+ console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
676
+ ```
677
+
678
+ <a name="standalone"></a>
679
+ ### Standalone Mode
680
+
681
+ The standalone mode is used to compile the code that can be directly run by `node`
682
+ itself. You need to install `fast-json-stringify`, `ajv`, `fast-uri` and `ajv-formats`
683
+ in order to let the standalone code works.
684
+
685
+ ```js
686
+ const fs = require('fs')
687
+ const code = fastJson({
688
+ title: 'default string',
689
+ type: 'object',
690
+ properties: {
691
+ firstName: {
692
+ type: 'string'
693
+ }
694
+ }
695
+ }, { mode: 'standalone' })
696
+
697
+ fs.writeFileSync('stringify.js', code)
698
+ const stringify = require('stringify.js')
673
699
  console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
674
700
  ```
675
701
 
package/ajv.js ADDED
@@ -0,0 +1,37 @@
1
+ 'use strict'
2
+
3
+ const Ajv = require('ajv')
4
+ const fastUri = require('fast-uri')
5
+ const ajvFormats = require('ajv-formats')
6
+
7
+ module.exports = buildAjv
8
+
9
+ function buildAjv (options) {
10
+ const ajvInstance = new Ajv({ ...options, strictSchema: false, uriResolver: fastUri })
11
+ ajvFormats(ajvInstance)
12
+
13
+ const validateDateTimeFormat = ajvFormats.get('date-time').validate
14
+ const validateDateFormat = ajvFormats.get('date').validate
15
+ const validateTimeFormat = ajvFormats.get('time').validate
16
+
17
+ ajvInstance.addKeyword({
18
+ keyword: 'fjs_date_type',
19
+ validate: (schema, date) => {
20
+ if (date instanceof Date) {
21
+ return true
22
+ }
23
+ if (schema === 'date-time') {
24
+ return validateDateTimeFormat(date)
25
+ }
26
+ if (schema === 'date') {
27
+ return validateDateFormat(date)
28
+ }
29
+ if (schema === 'time') {
30
+ return validateTimeFormat(date)
31
+ }
32
+ return false
33
+ }
34
+ })
35
+
36
+ return ajvInstance
37
+ }
package/index.js CHANGED
@@ -2,15 +2,14 @@
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
15
  let stringSimilarity = null
@@ -55,206 +54,17 @@ const objectReferenceSerializersMap = new Map()
55
54
  const schemaReferenceMap = new Map()
56
55
 
57
56
  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)
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
- }
57
+ let contextFunctions = null
225
58
 
226
59
  function build (schema, options) {
227
60
  arrayItemsReferenceSerializersMap.clear()
228
61
  objectReferenceSerializersMap.clear()
229
62
  schemaReferenceMap.clear()
230
63
 
64
+ contextFunctions = []
231
65
  options = options || {}
232
66
 
233
- ajvInstance = new Ajv({ ...options.ajv, strictSchema: false, uriResolver: fastUri })
234
- ajvFormats(ajvInstance)
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
- })
67
+ ajvInstance = buildAjv(options.ajv)
258
68
 
259
69
  isValidSchema(schema)
260
70
  if (options.schema) {
@@ -303,29 +113,50 @@ function build (schema, options) {
303
113
  schema.type = inferTypeByKeyword(schema)
304
114
  }
305
115
 
306
- const { code, laterCode } = buildValue('', 'main', 'input', location)
116
+ const code = buildValue('main', 'input', location)
117
+
307
118
  const contextFunctionCode = `
308
- 'use strict'
309
119
  function main (input) {
310
120
  let json = ''
311
121
  ${code}
312
122
  return json
313
123
  }
314
- ${laterCode}
124
+ ${contextFunctions.join('\n')}
315
125
  return main
316
126
  `
317
127
 
318
128
  const dependenciesName = ['ajv', 'serializer', contextFunctionCode]
319
129
 
320
130
  if (options.debugMode) {
131
+ options.mode = 'debug'
132
+ }
133
+
134
+ if (options.mode === 'debug') {
321
135
  return { code: dependenciesName.join('\n'), ajv: ajvInstance }
322
136
  }
323
137
 
138
+ if (options.mode === 'standalone') {
139
+ return `
140
+ 'use strict'
141
+
142
+ const Serializer = require('fast-json-stringify/serializer')
143
+ const buildAjv = require('fast-json-stringify/ajv')
144
+
145
+ const serializer = new Serializer(${JSON.stringify(options || {})})
146
+ const ajv = buildAjv(${JSON.stringify(options.ajv || {})})
147
+
148
+ ${contextFunctionCode.replace('return main', '')}
149
+
150
+ module.exports = main
151
+ `
152
+ }
153
+
324
154
  /* eslint no-new-func: "off" */
325
155
  const contextFunc = new Function('ajv', 'serializer', contextFunctionCode)
326
156
  const stringifyFunc = contextFunc(ajvInstance, serializer)
327
157
 
328
158
  ajvInstance = null
159
+ contextFunctions = null
329
160
  arrayItemsReferenceSerializersMap.clear()
330
161
  objectReferenceSerializersMap.clear()
331
162
  schemaReferenceMap.clear()
@@ -408,7 +239,6 @@ function addPatternProperties (location) {
408
239
  for (var i = 0; i < keys.length; i++) {
409
240
  if (properties[keys[i]]) continue
410
241
  `
411
- let laterCode = ''
412
242
 
413
243
  Object.keys(pp).forEach((regex, index) => {
414
244
  let ppLocation = mergeLocation(location, { schema: pp[regex] })
@@ -423,27 +253,24 @@ function addPatternProperties (location) {
423
253
  throw new Error(`${err.message}. Found at ${regex} matching ${JSON.stringify(pp[regex])}`)
424
254
  }
425
255
 
426
- const valueCode = buildValue('', '', 'obj[keys[i]]', ppLocation)
427
- laterCode += valueCode.laterCode
256
+ const valueCode = buildValue('', 'obj[keys[i]]', ppLocation)
428
257
  code += `
429
258
  if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {
430
259
  ${addComma}
431
260
  json += serializer.asString(keys[i]) + ':'
432
- ${valueCode.code}
261
+ ${valueCode}
433
262
  continue
434
263
  }
435
264
  `
436
265
  })
437
266
  if (schema.additionalProperties) {
438
- const additionalPropertyCode = additionalProperty(location)
439
- code += additionalPropertyCode.code
440
- laterCode += additionalPropertyCode.laterCode
267
+ code += additionalProperty(location)
441
268
  }
442
269
 
443
270
  code += `
444
271
  }
445
272
  `
446
- return { code, laterCode }
273
+ return code
447
274
  }
448
275
 
449
276
  function additionalProperty (location) {
@@ -457,7 +284,7 @@ function additionalProperty (location) {
457
284
  }
458
285
  `
459
286
 
460
- return { code, laterCode: '' }
287
+ return code
461
288
  }
462
289
  let apLocation = mergeLocation(location, { schema: ap })
463
290
  if (ap.$ref) {
@@ -465,29 +292,26 @@ function additionalProperty (location) {
465
292
  ap = apLocation.schema
466
293
  }
467
294
 
468
- const valueCode = buildValue('', '', 'obj[keys[i]]', apLocation)
295
+ const valueCode = buildValue('', 'obj[keys[i]]', apLocation)
469
296
 
470
297
  code += `
471
298
  ${addComma}
472
299
  json += serializer.asString(keys[i]) + ':'
473
- ${valueCode.code}
300
+ ${valueCode}
474
301
  `
475
302
 
476
- return { code, laterCode: valueCode.laterCode }
303
+ return code
477
304
  }
478
305
 
479
306
  function addAdditionalProperties (location) {
480
- const additionalPropertyCode = additionalProperty(location)
481
- const code = `
307
+ return `
482
308
  var properties = ${JSON.stringify(location.schema.properties)} || {}
483
309
  var keys = Object.keys(obj)
484
310
  for (var i = 0; i < keys.length; i++) {
485
311
  if (properties[keys[i]]) continue
486
- ${additionalPropertyCode.code}
312
+ ${additionalProperty(location)}
487
313
  }
488
314
  `
489
-
490
- return { code, laterCode: additionalPropertyCode.laterCode }
491
315
  }
492
316
 
493
317
  function idFinder (schema, searchedId) {
@@ -621,7 +445,7 @@ function refFinder (ref, location) {
621
445
  }
622
446
  }
623
447
 
624
- function buildCode (location, code, laterCode, locationPath) {
448
+ function buildCode (location, locationPath) {
625
449
  if (location.schema.$ref) {
626
450
  location = refFinder(location.schema.$ref, location)
627
451
  }
@@ -629,6 +453,8 @@ function buildCode (location, code, laterCode, locationPath) {
629
453
  const schema = location.schema
630
454
  const required = schema.required || []
631
455
 
456
+ let code = ''
457
+
632
458
  Object.keys(schema.properties || {}).forEach((key) => {
633
459
  let propertyLocation = mergeLocation(location, { schema: schema.properties[key] })
634
460
  if (schema.properties[key].$ref) {
@@ -648,9 +474,7 @@ function buildCode (location, code, laterCode, locationPath) {
648
474
  json += ${asString} + ':'
649
475
  `
650
476
 
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
477
+ code += buildValue(locationPath + key, `obj[${JSON.stringify(key)}]`, mergeLocation(propertyLocation, { schema: schema.properties[key] }))
654
478
 
655
479
  const defaultValue = schema.properties[key].default
656
480
  if (defaultValue !== undefined) {
@@ -676,7 +500,7 @@ function buildCode (location, code, laterCode, locationPath) {
676
500
  code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
677
501
  }
678
502
 
679
- return { code, laterCode }
503
+ return code
680
504
  }
681
505
 
682
506
  function mergeAllOfSchema (location, schema, mergedSchema) {
@@ -771,24 +595,17 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
771
595
 
772
596
  function buildInnerObject (location, locationPath) {
773
597
  const schema = location.schema
774
- const result = buildCode(location, '', '', locationPath)
598
+ let code = buildCode(location, locationPath)
775
599
  if (schema.patternProperties) {
776
- const { code, laterCode } = addPatternProperties(location)
777
- result.code += code
778
- result.laterCode += laterCode
600
+ code += addPatternProperties(location)
779
601
  } else if (schema.additionalProperties && !schema.patternProperties) {
780
- const { code, laterCode } = addAdditionalProperties(location)
781
- result.code += code
782
- result.laterCode += laterCode
602
+ code += addAdditionalProperties(location)
783
603
  }
784
- return result
604
+ return code
785
605
  }
786
606
 
787
607
  function addIfThenElse (location, locationPath) {
788
608
  let code = ''
789
- let r
790
- let laterCode = ''
791
- let innerR
792
609
 
793
610
  const schema = location.schema
794
611
  const copy = merge({}, schema)
@@ -809,14 +626,10 @@ function addIfThenElse (location, locationPath) {
809
626
  if (valid) {
810
627
  `
811
628
  if (merged.if && merged.then) {
812
- innerR = addIfThenElse(mergedLocation, locationPath + 'Then')
813
- code += innerR.code
814
- laterCode = innerR.laterCode
629
+ code += addIfThenElse(mergedLocation, locationPath + 'Then')
815
630
  }
816
631
 
817
- r = buildInnerObject(mergedLocation, locationPath + 'Then')
818
- code += r.code
819
- laterCode += r.laterCode
632
+ code += buildInnerObject(mergedLocation, locationPath + 'Then')
820
633
 
821
634
  code += `
822
635
  }
@@ -829,19 +642,15 @@ function addIfThenElse (location, locationPath) {
829
642
  `
830
643
 
831
644
  if (merged.if && merged.then) {
832
- innerR = addIfThenElse(mergedLocation, locationPath + 'Else')
833
- code += innerR.code
834
- laterCode += innerR.laterCode
645
+ code += addIfThenElse(mergedLocation, locationPath + 'Else')
835
646
  }
836
647
 
837
- r = buildInnerObject(mergedLocation, locationPath + 'Else')
838
- code += r.code
839
- laterCode += r.laterCode
648
+ code += buildInnerObject(mergedLocation, locationPath + 'Else')
840
649
 
841
650
  code += `
842
651
  }
843
652
  `
844
- return { code, laterCode }
653
+ return code
845
654
  }
846
655
 
847
656
  function toJSON (variableName) {
@@ -851,76 +660,63 @@ function toJSON (variableName) {
851
660
  `
852
661
  }
853
662
 
854
- function buildObject (location, code, functionName, locationPath) {
663
+ function buildObject (location, locationPath) {
855
664
  const schema = location.schema
856
665
  if (schema.$id !== undefined) {
857
666
  schemaReferenceMap.set(schema.$id, schema)
858
667
  }
859
- code += `
668
+
669
+ if (objectReferenceSerializersMap.has(schema)) {
670
+ return objectReferenceSerializersMap.get(schema)
671
+ }
672
+
673
+ const functionName = generateFuncName()
674
+ objectReferenceSerializersMap.set(schema, functionName)
675
+
676
+ let functionCode = `
860
677
  function ${functionName} (input) {
861
678
  // ${locationPath}
862
679
  `
863
680
  if (schema.nullable) {
864
- code += `
865
- if(input === null) {
681
+ functionCode += `
682
+ if (input === null) {
866
683
  return 'null';
867
684
  }
868
685
  `
869
686
  }
870
687
 
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 += `
688
+ functionCode += `
881
689
  var obj = ${toJSON('input')}
882
690
  var json = '{'
883
691
  var addComma = false
884
692
  `
885
693
 
886
- let r
694
+ let rCode
887
695
  if (schema.if && schema.then) {
888
- code += `
696
+ functionCode += `
889
697
  var valid
890
698
  `
891
- r = addIfThenElse(location, locationPath)
699
+ rCode = addIfThenElse(location, locationPath)
892
700
  } else {
893
- r = buildInnerObject(location, locationPath)
701
+ rCode = buildInnerObject(location, locationPath)
894
702
  }
895
703
 
896
704
  // Removes the comma if is the last element of the string (in case there are not properties)
897
- code += `${r.code}
705
+ functionCode += `${rCode}
898
706
  json += '}'
899
707
  return json
900
708
  }
901
- ${r.laterCode}
902
709
  `
903
710
 
904
- return code
711
+ contextFunctions.push(functionCode)
712
+ return functionName
905
713
  }
906
714
 
907
- function buildArray (location, code, functionName, locationPath) {
715
+ function buildArray (location, locationPath) {
908
716
  let schema = location.schema
909
717
  if (schema.$id !== undefined) {
910
718
  schemaReferenceMap.set(schema.$id, schema)
911
719
  }
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
720
 
925
721
  // default to any items type
926
722
  if (!schema.items) {
@@ -936,85 +732,105 @@ function buildArray (location, code, functionName, locationPath) {
936
732
 
937
733
  location = refFinder(schema.items.$ref, location)
938
734
  schema.items = location.schema
735
+ }
939
736
 
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)
737
+ if (arrayItemsReferenceSerializersMap.has(schema.items)) {
738
+ return arrayItemsReferenceSerializersMap.get(schema.items)
948
739
  }
949
740
 
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)
741
+ const functionName = generateFuncName()
742
+ arrayItemsReferenceSerializersMap.set(schema.items, functionName)
965
743
 
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
- }
744
+ let functionCode = `
745
+ function ${functionName} (obj) {
746
+ // ${locationPath}
747
+ `
974
748
 
975
- result.code += `
976
- else {
977
- throw new Error(\`Item at $\{i} does not match schema definition.\`)
978
- }
749
+ if (schema.nullable) {
750
+ functionCode += `
751
+ if (obj === null) {
752
+ return 'null';
753
+ }
979
754
  `
980
- } else {
981
- result = buildValue(laterCode, locationPath + accessor, 'obj[i]', mergeLocation(location, { schema: schema.items }))
982
755
  }
983
756
 
984
- code += `
757
+ functionCode += `
985
758
  if (!Array.isArray(obj)) {
986
759
  throw new TypeError(\`The value '$\{obj}' does not match schema definition.\`)
987
760
  }
761
+ const arrayLength = obj.length
988
762
  `
989
763
 
990
- code += 'const arrayLength = obj.length\n'
764
+ if (!schema.additionalItems) {
765
+ functionCode += `
766
+ if (arrayLength > ${schema.items.length}) {
767
+ throw new Error(\`Item at ${schema.items.length} does not match schema definition.\`)
768
+ }
769
+ `
770
+ }
771
+
991
772
  if (largeArrayMechanism !== 'default') {
992
773
  if (largeArrayMechanism === 'json-stringify') {
993
- code += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
774
+ functionCode += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
994
775
  } else {
995
776
  throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`)
996
777
  }
997
778
  }
998
779
 
999
- code += `
1000
- let jsonOutput= ''
1001
- for (let i = 0; i < arrayLength; i++) {
1002
- let json = ''
1003
- ${result.code}
1004
- jsonOutput += json
780
+ functionCode += `
781
+ let jsonOutput = ''
782
+ `
1005
783
 
1006
- if (json.length > 0 && i < arrayLength - 1) {
1007
- jsonOutput += ','
1008
- }
784
+ const accessor = '[i]'
785
+ if (Array.isArray(schema.items)) {
786
+ for (let i = 0; i < schema.items.length; i++) {
787
+ const item = schema.items[i]
788
+ const tmpRes = buildValue(locationPath + accessor + i, `obj[${i}]`, mergeLocation(location, { schema: item }))
789
+ functionCode += `
790
+ if (${i} < arrayLength) {
791
+ if (${buildArrayTypeCondition(item.type, `[${i}]`)}) {
792
+ let json = ''
793
+ ${tmpRes}
794
+ jsonOutput += json
795
+ if (${i} < arrayLength - 1) {
796
+ jsonOutput += ','
797
+ }
798
+ } else {
799
+ throw new Error(\`Item at ${i} does not match schema definition.\`)
800
+ }
801
+ }
802
+ `
803
+ }
804
+
805
+ if (schema.additionalItems) {
806
+ functionCode += `
807
+ for (let i = ${schema.items.length}; i < arrayLength; i++) {
808
+ let json = JSON.stringify(obj[i])
809
+ jsonOutput += json
810
+ if (i < arrayLength - 1) {
811
+ jsonOutput += ','
812
+ }
813
+ }`
1009
814
  }
815
+ } else {
816
+ const code = buildValue(locationPath + accessor, 'obj[i]', mergeLocation(location, { schema: schema.items }))
817
+ functionCode += `
818
+ for (let i = 0; i < arrayLength; i++) {
819
+ let json = ''
820
+ ${code}
821
+ jsonOutput += json
822
+ if (i < arrayLength - 1) {
823
+ jsonOutput += ','
824
+ }
825
+ }`
826
+ }
827
+
828
+ functionCode += `
1010
829
  return \`[\${jsonOutput}]\`
1011
830
  }`
1012
831
 
1013
- code += `
1014
- ${result.laterCode}
1015
- `
1016
-
1017
- return code
832
+ contextFunctions.push(functionCode)
833
+ return functionName
1018
834
  }
1019
835
 
1020
836
  function buildArrayTypeCondition (type, accessor) {
@@ -1083,7 +899,7 @@ function generateFuncName () {
1083
899
  return 'anonymous' + genFuncNameCounter++
1084
900
  }
1085
901
 
1086
- function buildValue (laterCode, locationPath, input, location) {
902
+ function buildValue (locationPath, input, location) {
1087
903
  let schema = location.schema
1088
904
 
1089
905
  if (schema.$ref) {
@@ -1134,13 +950,11 @@ function buildValue (laterCode, locationPath, input, location) {
1134
950
  code += `json += ${funcName}(${input})`
1135
951
  break
1136
952
  case 'object':
1137
- funcName = generateFuncName()
1138
- laterCode = buildObject(location, laterCode, funcName, locationPath)
953
+ funcName = buildObject(location, locationPath)
1139
954
  code += `json += ${funcName}(${input})`
1140
955
  break
1141
956
  case 'array':
1142
- funcName = generateFuncName()
1143
- laterCode = buildArray(location, laterCode, funcName, locationPath)
957
+ funcName = buildArray(location, locationPath)
1144
958
  code += `json += ${funcName}(${input})`
1145
959
  break
1146
960
  case undefined:
@@ -1148,7 +962,7 @@ function buildValue (laterCode, locationPath, input, location) {
1148
962
  // beware: dereferenceOfRefs has side effects and changes schema.anyOf
1149
963
  const locations = dereferenceOfRefs(location, schema.anyOf ? 'anyOf' : 'oneOf')
1150
964
  locations.forEach((location, index) => {
1151
- const nestedResult = buildValue(laterCode, locationPath + 'i' + index, input, location)
965
+ const nestedResult = buildValue(locationPath + 'i' + index, input, location)
1152
966
  // Since we are only passing the relevant schema to ajv.validate, it needs to be full dereferenced
1153
967
  // otherwise any $ref pointing to an external schema would result in an error.
1154
968
  // Full dereference of the schema happens as side effect of two functions:
@@ -1168,9 +982,8 @@ function buildValue (laterCode, locationPath, input, location) {
1168
982
 
1169
983
  code += `
1170
984
  ${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${input}))
1171
- ${nestedResult.code}
985
+ ${nestedResult}
1172
986
  `
1173
- laterCode = nestedResult.laterCode
1174
987
  })
1175
988
 
1176
989
  code += `
@@ -1211,38 +1024,37 @@ function buildValue (laterCode, locationPath, input, location) {
1211
1024
  sortedTypes.forEach((type, index) => {
1212
1025
  const statement = index === 0 ? 'if' : 'else if'
1213
1026
  const tempSchema = Object.assign({}, schema, { type })
1214
- const nestedResult = buildValue(laterCode, locationPath, input, mergeLocation(location, { schema: tempSchema }))
1027
+ const nestedResult = buildValue(locationPath, input, mergeLocation(location, { schema: tempSchema }))
1215
1028
  switch (type) {
1216
1029
  case 'string': {
1217
1030
  code += `
1218
1031
  ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof Date || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
1219
- ${nestedResult.code}
1032
+ ${nestedResult}
1220
1033
  `
1221
1034
  break
1222
1035
  }
1223
1036
  case 'array': {
1224
1037
  code += `
1225
1038
  ${statement}(Array.isArray(${input}))
1226
- ${nestedResult.code}
1039
+ ${nestedResult}
1227
1040
  `
1228
1041
  break
1229
1042
  }
1230
1043
  case 'integer': {
1231
1044
  code += `
1232
1045
  ${statement}(Number.isInteger(${input}) || ${input} === null)
1233
- ${nestedResult.code}
1046
+ ${nestedResult}
1234
1047
  `
1235
1048
  break
1236
1049
  }
1237
1050
  default: {
1238
1051
  code += `
1239
1052
  ${statement}(typeof ${input} === "${type}" || ${input} === null)
1240
- ${nestedResult.code}
1053
+ ${nestedResult}
1241
1054
  `
1242
1055
  break
1243
1056
  }
1244
1057
  }
1245
- laterCode = nestedResult.laterCode
1246
1058
  })
1247
1059
  code += `
1248
1060
  else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
@@ -1258,7 +1070,7 @@ function buildValue (laterCode, locationPath, input, location) {
1258
1070
  }
1259
1071
  }
1260
1072
 
1261
- return { code, laterCode }
1073
+ return code
1262
1074
  }
1263
1075
 
1264
1076
  function extendDateTimeType (schema) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/serializer.js ADDED
@@ -0,0 +1,168 @@
1
+ 'use strict'
2
+
3
+ module.exports = class Serializer {
4
+ constructor (options = {}) {
5
+ switch (options.rounding) {
6
+ case 'floor':
7
+ this.parseInteger = Math.floor
8
+ break
9
+ case 'ceil':
10
+ this.parseInteger = Math.ceil
11
+ break
12
+ case 'round':
13
+ this.parseInteger = Math.round
14
+ break
15
+ default:
16
+ this.parseInteger = Math.trunc
17
+ break
18
+ }
19
+ }
20
+
21
+ asAny (i) {
22
+ return JSON.stringify(i)
23
+ }
24
+
25
+ asNull () {
26
+ return 'null'
27
+ }
28
+
29
+ asInteger (i) {
30
+ if (typeof i === 'bigint') {
31
+ return i.toString()
32
+ } else if (Number.isInteger(i)) {
33
+ return '' + i
34
+ } else {
35
+ /* eslint no-undef: "off" */
36
+ const integer = this.parseInteger(i)
37
+ if (Number.isNaN(integer)) {
38
+ throw new Error(`The value "${i}" cannot be converted to an integer.`)
39
+ } else {
40
+ return '' + integer
41
+ }
42
+ }
43
+ }
44
+
45
+ asIntegerNullable (i) {
46
+ return i === null ? 'null' : this.asInteger(i)
47
+ }
48
+
49
+ asNumber (i) {
50
+ const num = Number(i)
51
+ if (Number.isNaN(num)) {
52
+ throw new Error(`The value "${i}" cannot be converted to a number.`)
53
+ } else {
54
+ return '' + num
55
+ }
56
+ }
57
+
58
+ asNumberNullable (i) {
59
+ return i === null ? 'null' : this.asNumber(i)
60
+ }
61
+
62
+ asBoolean (bool) {
63
+ return bool && 'true' || 'false' // eslint-disable-line
64
+ }
65
+
66
+ asBooleanNullable (bool) {
67
+ return bool === null ? 'null' : this.asBoolean(bool)
68
+ }
69
+
70
+ asDatetime (date, skipQuotes) {
71
+ const quotes = skipQuotes === true ? '' : '"'
72
+ if (date instanceof Date) {
73
+ return quotes + date.toISOString() + quotes
74
+ }
75
+ return this.asString(date, skipQuotes)
76
+ }
77
+
78
+ asDatetimeNullable (date, skipQuotes) {
79
+ return date === null ? 'null' : this.asDatetime(date, skipQuotes)
80
+ }
81
+
82
+ asDate (date, skipQuotes) {
83
+ const quotes = skipQuotes === true ? '' : '"'
84
+ if (date instanceof Date) {
85
+ return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + quotes
86
+ }
87
+ return this.asString(date, skipQuotes)
88
+ }
89
+
90
+ asDateNullable (date, skipQuotes) {
91
+ return date === null ? 'null' : this.asDate(date, skipQuotes)
92
+ }
93
+
94
+ asTime (date, skipQuotes) {
95
+ const quotes = skipQuotes === true ? '' : '"'
96
+ if (date instanceof Date) {
97
+ return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + quotes
98
+ }
99
+ return this.asString(date, skipQuotes)
100
+ }
101
+
102
+ asTimeNullable (date, skipQuotes) {
103
+ return date === null ? 'null' : this.asTime(date, skipQuotes)
104
+ }
105
+
106
+ asString (str, skipQuotes) {
107
+ const quotes = skipQuotes === true ? '' : '"'
108
+ if (str instanceof Date) {
109
+ return quotes + str.toISOString() + quotes
110
+ } else if (str === null) {
111
+ return quotes + quotes
112
+ } else if (str instanceof RegExp) {
113
+ str = str.source
114
+ } else if (typeof str !== 'string') {
115
+ str = str.toString()
116
+ }
117
+ // If we skipQuotes it means that we are using it as test
118
+ // no need to test the string length for the render
119
+ if (skipQuotes) {
120
+ return str
121
+ }
122
+
123
+ if (str.length < 42) {
124
+ return this.asStringSmall(str)
125
+ } else {
126
+ return JSON.stringify(str)
127
+ }
128
+ }
129
+
130
+ asStringNullable (str) {
131
+ return str === null ? 'null' : this.asString(str)
132
+ }
133
+
134
+ // magically escape strings for json
135
+ // relying on their charCodeAt
136
+ // everything below 32 needs JSON.stringify()
137
+ // every string that contain surrogate needs JSON.stringify()
138
+ // 34 and 92 happens all the time, so we
139
+ // have a fast case for them
140
+ asStringSmall (str) {
141
+ const l = str.length
142
+ let result = ''
143
+ let last = 0
144
+ let found = false
145
+ let surrogateFound = false
146
+ let point = 255
147
+ // eslint-disable-next-line
148
+ for (var i = 0; i < l && point >= 32; i++) {
149
+ point = str.charCodeAt(i)
150
+ if (point >= 0xD800 && point <= 0xDFFF) {
151
+ // The current character is a surrogate.
152
+ surrogateFound = true
153
+ }
154
+ if (point === 34 || point === 92) {
155
+ result += str.slice(last, i) + '\\'
156
+ last = i
157
+ found = true
158
+ }
159
+ }
160
+
161
+ if (!found) {
162
+ result = str
163
+ } else {
164
+ result += str.slice(last)
165
+ }
166
+ return ((point < 32) || (surrogateFound === true)) ? JSON.stringify(str) : '"' + result + '"'
167
+ }
168
+ }
@@ -218,6 +218,35 @@ test('array items is a list of schema and additionalItems is true, just the desc
218
218
  t.equal(result, '{"foo":["foo","bar",1]}')
219
219
  })
220
220
 
221
+ test('array items is a list of schema and additionalItems is true, just the described item is validated', (t) => {
222
+ t.plan(1)
223
+
224
+ const schema = {
225
+ type: 'object',
226
+ properties: {
227
+ foo: {
228
+ type: 'array',
229
+ items: [
230
+ {
231
+ type: 'string'
232
+ },
233
+ {
234
+ type: 'number'
235
+ }
236
+ ],
237
+ additionalItems: true
238
+ }
239
+ }
240
+ }
241
+
242
+ const stringify = build(schema)
243
+ const result = stringify({
244
+ foo: ['foo']
245
+ })
246
+
247
+ t.equal(result, '{"foo":["foo"]}')
248
+ })
249
+
221
250
  test('array items is a list of schema and additionalItems is false', (t) => {
222
251
  t.plan(1)
223
252
 
File without changes
@@ -0,0 +1,38 @@
1
+ 'use strict'
2
+
3
+ const test = require('tap').test
4
+ const fjs = require('..')
5
+ const fs = require('fs')
6
+ const path = require('path')
7
+
8
+ function build (opts) {
9
+ return fjs({
10
+ title: 'default string',
11
+ type: 'object',
12
+ properties: {
13
+ firstName: {
14
+ type: 'string'
15
+ }
16
+ },
17
+ required: ['firstName']
18
+ }, opts)
19
+ }
20
+
21
+ const tmpDir = 'test/fixtures'
22
+
23
+ test('activate standalone mode', async (t) => {
24
+ t.plan(2)
25
+ let code = build({ mode: 'standalone' })
26
+ t.type(code, 'string')
27
+ code = code.replace(/fast-json-stringify/g, '../..')
28
+
29
+ const destionation = path.resolve(tmpDir, 'standalone.js')
30
+
31
+ t.teardown(async () => {
32
+ await fs.promises.rm(destionation, { force: true })
33
+ })
34
+
35
+ await fs.promises.writeFile(destionation, code)
36
+ const standalone = require(destionation)
37
+ t.same(standalone({ firstName: 'Foo', surname: 'bar' }), JSON.stringify({ firstName: 'Foo' }), 'surname evicted')
38
+ })