fast-json-stringify 5.14.1 → 5.15.1

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
@@ -140,7 +140,19 @@ function build (schema, options) {
140
140
  const location = new Location(schema, context.rootSchemaId)
141
141
  const code = buildValue(context, location, 'input')
142
142
 
143
- let contextFunctionCode
143
+ let contextFunctionCode = `
144
+ const JSON_STR_BEGIN_OBJECT = '{'
145
+ const JSON_STR_END_OBJECT = '}'
146
+ const JSON_STR_BEGIN_ARRAY = '['
147
+ const JSON_STR_END_ARRAY = ']'
148
+ const JSON_STR_COMMA = ','
149
+ const JSON_STR_COLONS = ':'
150
+ const JSON_STR_QUOTE = '"'
151
+ const JSON_STR_EMPTY_OBJECT = JSON_STR_BEGIN_OBJECT + JSON_STR_END_OBJECT
152
+ const JSON_STR_EMPTY_ARRAY = JSON_STR_BEGIN_ARRAY + JSON_STR_END_ARRAY
153
+ const JSON_STR_EMPTY_STRING = JSON_STR_QUOTE + JSON_STR_QUOTE
154
+ const JSON_STR_NULL = 'null'
155
+ `
144
156
 
145
157
  // If we have only the invocation of the 'anonymous0' function, we would
146
158
  // basically just wrap the 'anonymous0' function in the 'main' function and
@@ -148,13 +160,13 @@ function build (schema, options) {
148
160
  // wrapping and the unnecessary memory allocation by aliasing 'anonymous0' to
149
161
  // 'main'
150
162
  if (code === 'json += anonymous0(input)') {
151
- contextFunctionCode = `
163
+ contextFunctionCode += `
152
164
  ${context.functions.join('\n')}
153
165
  const main = anonymous0
154
166
  return main
155
167
  `
156
168
  } else {
157
- contextFunctionCode = `
169
+ contextFunctionCode += `
158
170
  function main (input) {
159
171
  let json = ''
160
172
  ${code}
@@ -284,7 +296,7 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma) {
284
296
  code += `
285
297
  if (/${propertyKey.replace(/\\*\//g, '\\/')}/.test(key)) {
286
298
  ${addComma}
287
- json += serializer.asString(key) + ':'
299
+ json += serializer.asString(key) + JSON_STR_COLONS
288
300
  ${buildValue(context, propertyLocation, 'value')}
289
301
  continue
290
302
  }
@@ -299,13 +311,13 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma) {
299
311
  if (additionalPropertiesSchema === true) {
300
312
  code += `
301
313
  ${addComma}
302
- json += serializer.asString(key) + ':' + JSON.stringify(value)
314
+ json += serializer.asString(key) + JSON_STR_COLONS + JSON.stringify(value)
303
315
  `
304
316
  } else {
305
317
  const propertyLocation = location.getPropertyLocation('additionalProperties')
306
318
  code += `
307
319
  ${addComma}
308
- json += serializer.asString(key) + ':'
320
+ json += serializer.asString(key) + JSON_STR_COLONS
309
321
  ${buildValue(context, propertyLocation, 'value')}
310
322
  `
311
323
  }
@@ -333,20 +345,21 @@ function buildInnerObject (context, location) {
333
345
  )
334
346
  const hasRequiredProperties = requiredProperties.includes(propertiesKeys[0])
335
347
 
336
- let code = ''
348
+ let code = 'let value\n'
337
349
 
338
350
  for (const key of requiredProperties) {
339
351
  if (!propertiesKeys.includes(key)) {
340
- code += `if (obj['${key}'] === undefined) throw new Error('"${key}" is required!')\n`
352
+ const sanitizedKey = JSON.stringify(key)
353
+ code += `if (obj[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')\n`
341
354
  }
342
355
  }
343
356
 
344
- code += 'let json = \'{\'\n'
357
+ code += 'let json = JSON_STR_BEGIN_OBJECT\n'
345
358
 
346
359
  let addComma = ''
347
360
  if (!hasRequiredProperties) {
348
361
  code += 'let addComma = false\n'
349
- addComma = '!addComma && (addComma = true) || (json += \',\')'
362
+ addComma = '!addComma && (addComma = true) || (json += JSON_STR_COMMA)'
350
363
  }
351
364
 
352
365
  for (const key of propertiesKeys) {
@@ -360,10 +373,11 @@ function buildInnerObject (context, location) {
360
373
  const isRequired = requiredProperties.includes(key)
361
374
 
362
375
  code += `
363
- if (obj[${sanitizedKey}] !== undefined) {
376
+ value = obj[${sanitizedKey}]
377
+ if (value !== undefined) {
364
378
  ${addComma}
365
379
  json += ${JSON.stringify(sanitizedKey + ':')}
366
- ${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
380
+ ${buildValue(context, propertyLocation, 'value')}
367
381
  }`
368
382
 
369
383
  if (defaultValue !== undefined) {
@@ -374,7 +388,7 @@ function buildInnerObject (context, location) {
374
388
  `
375
389
  } else if (isRequired) {
376
390
  code += ` else {
377
- throw new Error('${sanitizedKey} is required!')
391
+ throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')
378
392
  }
379
393
  `
380
394
  } else {
@@ -391,7 +405,7 @@ function buildInnerObject (context, location) {
391
405
  }
392
406
 
393
407
  code += `
394
- return json + '}'
408
+ return json + JSON_STR_END_OBJECT
395
409
  `
396
410
  return code
397
411
  }
@@ -482,7 +496,7 @@ function buildObject (context, location) {
482
496
  // ${schemaRef}
483
497
  function ${functionName} (input) {
484
498
  const obj = ${toJSON('input')}
485
- ${!nullable ? 'if (obj === null) return \'{}\'' : ''}
499
+ ${!nullable ? 'if (obj === null) return JSON_STR_EMPTY_OBJECT' : ''}
486
500
 
487
501
  ${buildInnerObject(context, location)}
488
502
  }
@@ -523,7 +537,7 @@ function buildArray (context, location) {
523
537
 
524
538
  const nullable = schema.nullable === true
525
539
  functionCode += `
526
- ${!nullable ? 'if (obj === null) return \'[]\'' : ''}
540
+ ${!nullable ? 'if (obj === null) return JSON_STR_EMPTY_ARRAY' : ''}
527
541
  if (!Array.isArray(obj)) {
528
542
  throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
529
543
  }
@@ -539,25 +553,26 @@ function buildArray (context, location) {
539
553
  }
540
554
 
541
555
  if (largeArrayMechanism === 'json-stringify') {
542
- functionCode += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
556
+ functionCode += `if (arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
543
557
  }
544
558
 
545
559
  functionCode += `
546
- let jsonOutput = ''
560
+ const arrayEnd = arrayLength - 1
561
+ let value
562
+ let json = ''
547
563
  `
548
564
 
549
565
  if (Array.isArray(itemsSchema)) {
550
566
  for (let i = 0; i < itemsSchema.length; i++) {
551
567
  const item = itemsSchema[i]
552
- const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), `obj[${i}]`)
568
+ functionCode += `value = obj[${i}]`
569
+ const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), 'value')
553
570
  functionCode += `
554
571
  if (${i} < arrayLength) {
555
572
  if (${buildArrayTypeCondition(item.type, `[${i}]`)}) {
556
- let json = ''
557
573
  ${tmpRes}
558
- jsonOutput += json
559
- if (${i} < arrayLength - 1) {
560
- jsonOutput += ','
574
+ if (${i} < arrayEnd) {
575
+ json += JSON_STR_COMMA
561
576
  }
562
577
  } else {
563
578
  throw new Error(\`Item at ${i} does not match schema definition.\`)
@@ -569,9 +584,9 @@ function buildArray (context, location) {
569
584
  if (schema.additionalItems) {
570
585
  functionCode += `
571
586
  for (let i = ${itemsSchema.length}; i < arrayLength; i++) {
572
- jsonOutput += JSON.stringify(obj[i])
573
- if (i < arrayLength - 1) {
574
- jsonOutput += ','
587
+ json += JSON.stringify(obj[i])
588
+ if (i < arrayEnd) {
589
+ json += JSON_STR_COMMA
575
590
  }
576
591
  }`
577
592
  }
@@ -579,17 +594,15 @@ function buildArray (context, location) {
579
594
  const code = buildValue(context, itemsLocation, 'obj[i]')
580
595
  functionCode += `
581
596
  for (let i = 0; i < arrayLength; i++) {
582
- let json = ''
583
597
  ${code}
584
- jsonOutput += json
585
- if (i < arrayLength - 1) {
586
- jsonOutput += ','
598
+ if (i < arrayEnd) {
599
+ json += JSON_STR_COMMA
587
600
  }
588
601
  }`
589
602
  }
590
603
 
591
604
  functionCode += `
592
- return \`[\${jsonOutput}]\`
605
+ return JSON_STR_BEGIN_ARRAY + json + JSON_STR_END_ARRAY
593
606
  }`
594
607
 
595
608
  context.functions.push(functionCode)
@@ -600,33 +613,33 @@ function buildArrayTypeCondition (type, accessor) {
600
613
  let condition
601
614
  switch (type) {
602
615
  case 'null':
603
- condition = `obj${accessor} === null`
616
+ condition = 'value === null'
604
617
  break
605
618
  case 'string':
606
- condition = `typeof obj${accessor} === 'string' ||
607
- obj${accessor} === null ||
608
- obj${accessor} instanceof Date ||
609
- obj${accessor} instanceof RegExp ||
619
+ condition = `typeof value === 'string' ||
620
+ value === null ||
621
+ value instanceof Date ||
622
+ value instanceof RegExp ||
610
623
  (
611
- typeof obj${accessor} === "object" &&
612
- typeof obj${accessor}.toString === "function" &&
613
- obj${accessor}.toString !== Object.prototype.toString
624
+ typeof value === "object" &&
625
+ typeof value.toString === "function" &&
626
+ value.toString !== Object.prototype.toString
614
627
  )`
615
628
  break
616
629
  case 'integer':
617
- condition = `Number.isInteger(obj${accessor})`
630
+ condition = 'Number.isInteger(value)'
618
631
  break
619
632
  case 'number':
620
- condition = `Number.isFinite(obj${accessor})`
633
+ condition = 'Number.isFinite(value)'
621
634
  break
622
635
  case 'boolean':
623
- condition = `typeof obj${accessor} === 'boolean'`
636
+ condition = 'typeof value === \'boolean\''
624
637
  break
625
638
  case 'object':
626
- condition = `obj${accessor} && typeof obj${accessor} === 'object' && obj${accessor}.constructor === Object`
639
+ condition = 'value && typeof value === \'object\' && value.constructor === Object'
627
640
  break
628
641
  case 'array':
629
- condition = `Array.isArray(obj${accessor})`
642
+ condition = 'Array.isArray(value)'
630
643
  break
631
644
  default:
632
645
  if (Array.isArray(type)) {
@@ -717,7 +730,7 @@ function buildSingleTypeSerializer (context, location, input) {
717
730
 
718
731
  switch (schema.type) {
719
732
  case 'null':
720
- return 'json += \'null\''
733
+ return 'json += JSON_STR_NULL'
721
734
  case 'string': {
722
735
  if (schema.format === 'date-time') {
723
736
  return `json += serializer.asDateTime(${input})`
@@ -731,9 +744,9 @@ function buildSingleTypeSerializer (context, location, input) {
731
744
  return `
732
745
  if (typeof ${input} !== 'string') {
733
746
  if (${input} === null) {
734
- json += '""'
747
+ json += JSON_STR_EMPTY_STRING
735
748
  } else if (${input} instanceof Date) {
736
- json += '"' + ${input}.toISOString() + '"'
749
+ json += JSON_STR_QUOTE + ${input}.toISOString() + JSON_STR_QUOTE
737
750
  } else if (${input} instanceof RegExp) {
738
751
  json += serializer.asString(${input}.source)
739
752
  } else {
@@ -777,7 +790,7 @@ function buildConstSerializer (location, input) {
777
790
  if (hasNullType) {
778
791
  code += `
779
792
  if (${input} === null) {
780
- json += 'null'
793
+ json += JSON_STR_NULL
781
794
  } else {
782
795
  `
783
796
  }
@@ -984,7 +997,7 @@ function buildValue (context, location, input) {
984
997
  if (nullable) {
985
998
  code += `
986
999
  if (${input} === null) {
987
- json += 'null'
1000
+ json += JSON_STR_NULL
988
1001
  } else {
989
1002
  `
990
1003
  }
package/lib/serializer.js CHANGED
@@ -24,38 +24,29 @@ module.exports = class Serializer {
24
24
  }
25
25
 
26
26
  asInteger (i) {
27
- if (typeof i === 'number') {
28
- if (Number.isInteger(i)) {
29
- return '' + i
30
- }
31
- // check if number is Infinity or NaN
32
- // eslint-disable-next-line no-self-compare
33
- if (i === Infinity || i === -Infinity || i !== i) {
34
- throw new Error(`The value "${i}" cannot be converted to an integer.`)
35
- }
36
- return this.parseInteger(i)
37
- } else if (i === null) {
38
- return '0'
27
+ if (Number.isInteger(i)) {
28
+ return '' + i
39
29
  } else if (typeof i === 'bigint') {
40
30
  return i.toString()
41
- } else {
42
- /* eslint no-undef: "off" */
43
- const integer = this.parseInteger(i)
44
- if (Number.isFinite(integer)) {
45
- return '' + integer
46
- } else {
47
- throw new Error(`The value "${i}" cannot be converted to an integer.`)
48
- }
49
31
  }
32
+ /* eslint no-undef: "off" */
33
+ const integer = this.parseInteger(i)
34
+ // check if number is Infinity or NaN
35
+ // eslint-disable-next-line no-self-compare
36
+ if (integer === Infinity || integer === -Infinity || integer !== integer) {
37
+ throw new Error(`The value "${i}" cannot be converted to an integer.`)
38
+ }
39
+ return '' + integer
50
40
  }
51
41
 
52
42
  asNumber (i) {
53
- const num = Number(i)
43
+ // fast cast to number
44
+ const num = +i
54
45
  // check if number is NaN
55
46
  // eslint-disable-next-line no-self-compare
56
47
  if (num !== num) {
57
48
  throw new Error(`The value "${i}" cannot be converted to a number.`)
58
- } else if (!Number.isFinite(num)) {
49
+ } else if (num === Infinity || num === -Infinity) {
59
50
  return 'null'
60
51
  } else {
61
52
  return '' + num
@@ -100,9 +91,34 @@ module.exports = class Serializer {
100
91
  }
101
92
 
102
93
  asString (str) {
103
- if (str.length < 42) {
104
- return this.asStringSmall(str)
105
- } else if (str.length < 5000 && STR_ESCAPE.test(str) === false) {
94
+ const len = str.length
95
+ if (len < 42) {
96
+ // magically escape strings for json
97
+ // relying on their charCodeAt
98
+ // everything below 32 needs JSON.stringify()
99
+ // every string that contain surrogate needs JSON.stringify()
100
+ // 34 and 92 happens all the time, so we
101
+ // have a fast case for them
102
+ let result = ''
103
+ let last = -1
104
+ let point = 255
105
+ // eslint-disable-next-line
106
+ for (var i = 0; i < len; i++) {
107
+ point = str.charCodeAt(i)
108
+ if (
109
+ point === 0x22 || // '"'
110
+ point === 0x5c // '\'
111
+ ) {
112
+ last === -1 && (last = 0)
113
+ result += str.slice(last, i) + '\\'
114
+ last = i
115
+ } else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
116
+ // The current character is non-printable characters or a surrogate.
117
+ return JSON.stringify(str)
118
+ }
119
+ }
120
+ return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
121
+ } else if (len < 5000 && STR_ESCAPE.test(str) === false) {
106
122
  // Only use the regular expression for shorter input. The overhead is otherwise too much.
107
123
  return '"' + str + '"'
108
124
  } else {
@@ -114,37 +130,6 @@ module.exports = class Serializer {
114
130
  return '"' + str + '"'
115
131
  }
116
132
 
117
- // magically escape strings for json
118
- // relying on their charCodeAt
119
- // everything below 32 needs JSON.stringify()
120
- // every string that contain surrogate needs JSON.stringify()
121
- // 34 and 92 happens all the time, so we
122
- // have a fast case for them
123
- asStringSmall (str) {
124
- const len = str.length
125
- let result = ''
126
- let last = -1
127
- let point = 255
128
-
129
- // eslint-disable-next-line
130
- for (var i = 0; i < len; i++) {
131
- point = str.charCodeAt(i)
132
- if (
133
- point === 0x22 || // '"'
134
- point === 0x5c // '\'
135
- ) {
136
- last === -1 && (last = 0)
137
- result += str.slice(last, i) + '\\'
138
- last = i
139
- } else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
140
- // The current character is non-printable characters or a surrogate.
141
- return JSON.stringify(str)
142
- }
143
- }
144
-
145
- return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
146
- }
147
-
148
133
  getState () {
149
134
  return this._options
150
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.14.1",
3
+ "version": "5.15.1",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -0,0 +1,68 @@
1
+ 'use strict'
2
+
3
+ const test = require('tap').test
4
+ const build = require('..')
5
+
6
+ test('required property containing single quote, contains property', (t) => {
7
+ t.plan(1)
8
+
9
+ const stringify = build({
10
+ type: 'object',
11
+ properties: {
12
+ '\'': { type: 'string' }
13
+ },
14
+ required: [
15
+ '\''
16
+ ]
17
+ })
18
+
19
+ t.throws(() => stringify({}), new Error('"\'" is required!'))
20
+ })
21
+
22
+ test('required property containing double quote, contains property', (t) => {
23
+ t.plan(1)
24
+
25
+ const stringify = build({
26
+ type: 'object',
27
+ properties: {
28
+ '"': { type: 'string' }
29
+ },
30
+ required: [
31
+ '"'
32
+ ]
33
+ })
34
+
35
+ t.throws(() => stringify({}), new Error('""" is required!'))
36
+ })
37
+
38
+ test('required property containing single quote, does not contain property', (t) => {
39
+ t.plan(1)
40
+
41
+ const stringify = build({
42
+ type: 'object',
43
+ properties: {
44
+ a: { type: 'string' }
45
+ },
46
+ required: [
47
+ '\''
48
+ ]
49
+ })
50
+
51
+ t.throws(() => stringify({}), new Error('"\'" is required!'))
52
+ })
53
+
54
+ test('required property containing double quote, does not contain property', (t) => {
55
+ t.plan(1)
56
+
57
+ const stringify = build({
58
+ type: 'object',
59
+ properties: {
60
+ a: { type: 'string' }
61
+ },
62
+ required: [
63
+ '"'
64
+ ]
65
+ })
66
+
67
+ t.throws(() => stringify({}), new Error('""" is required!'))
68
+ })