fast-json-stringify 5.7.0 → 5.8.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.
@@ -1,4 +1,4 @@
1
- name: Benchmark
1
+ name: Benchmark PR
2
2
 
3
3
  on:
4
4
  pull_request_target:
@@ -11,15 +11,8 @@ jobs:
11
11
  permissions:
12
12
  contents: read
13
13
  outputs:
14
- PR-BENCH-14: ${{ steps.benchmark-pr.outputs.BENCH_RESULT14 }}
15
- PR-BENCH-16: ${{ steps.benchmark-pr.outputs.BENCH_RESULT16 }}
16
- PR-BENCH-18: ${{ steps.benchmark-pr.outputs.BENCH_RESULT18 }}
17
- MAIN-BENCH-14: ${{ steps.benchmark-main.outputs.BENCH_RESULT14 }}
18
- MAIN-BENCH-16: ${{ steps.benchmark-main.outputs.BENCH_RESULT16 }}
19
- MAIN-BENCH-18: ${{ steps.benchmark-main.outputs.BENCH_RESULT18 }}
20
- strategy:
21
- matrix:
22
- node-version: [14, 16, 18]
14
+ PR-BENCH: ${{ steps.benchmark-pr.outputs.BENCH_RESULT }}
15
+ MASTER-BENCH: ${{ steps.benchmark-master.outputs.BENCH_RESULT }}
23
16
  steps:
24
17
  - uses: actions/checkout@v3
25
18
  with:
@@ -29,7 +22,7 @@ jobs:
29
22
 
30
23
  - uses: actions/setup-node@v3
31
24
  with:
32
- node-version: ${{ matrix.node-version }}
25
+ node-version: 18
33
26
 
34
27
  - name: Install
35
28
  run: |
@@ -38,14 +31,14 @@ jobs:
38
31
  - name: Run benchmark
39
32
  id: benchmark-pr
40
33
  run: |
41
- npm run --silent benchmark > ./bench-result
34
+ npm run --silent bench > ./bench-result
42
35
  content=$(cat ./bench-result)
43
36
  content="${content//'%'/'%25'}"
44
37
  content="${content//$'\n'/'%0A'}"
45
38
  content="${content//$'\r'/'%0D'}"
46
- echo "::set-output name=BENCH_RESULT${{matrix.node-version}}::$content"
39
+ echo "::set-output name=BENCH_RESULT::$content"
47
40
 
48
- # main benchmark
41
+ # master benchmark
49
42
  - uses: actions/checkout@v3
50
43
  with:
51
44
  ref: 'master'
@@ -57,14 +50,15 @@ jobs:
57
50
  - name: Run benchmark
58
51
  id: benchmark-master
59
52
  run: |
60
- npm run --silent benchmark > ./bench-result
53
+ npm run --silent bench > ./bench-result
61
54
  content=$(cat ./bench-result)
62
55
  content="${content//'%'/'%25'}"
63
56
  content="${content//$'\n'/'%0A'}"
64
57
  content="${content//$'\r'/'%0D'}"
65
- echo "::set-output name=BENCH_RESULT${{matrix.node-version}}::$content"
58
+ echo "::set-output name=BENCH_RESULT::$content"
66
59
 
67
60
  output-benchmark:
61
+ if: "always()"
68
62
  needs: [benchmark]
69
63
  runs-on: ubuntu-latest
70
64
  permissions:
@@ -75,38 +69,13 @@ jobs:
75
69
  with:
76
70
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77
71
  message: |
78
- **Node**: 14
79
72
  **PR**:
80
73
  ```
81
- ${{ needs.benchmark.outputs.PR-BENCH-14 }}
74
+ ${{ needs.benchmark.outputs.PR-BENCH }}
82
75
  ```
83
- **MAIN**:
76
+ **MASTER**:
84
77
  ```
85
- ${{ needs.benchmark.outputs.MAIN-BENCH-14 }}
86
- ```
87
-
88
- ---
89
-
90
- **Node**: 16
91
- **PR**:
92
- ```
93
- ${{ needs.benchmark.outputs.PR-BENCH-16 }}
94
- ```
95
- **MAIN**:
96
- ```
97
- ${{ needs.benchmark.outputs.MAIN-BENCH-16 }}
98
- ```
99
-
100
- ---
101
-
102
- **Node**: 18
103
- **PR**:
104
- ```
105
- ${{ needs.benchmark.outputs.PR-BENCH-18 }}
106
- ```
107
- **MAIN**:
108
- ```
109
- ${{ needs.benchmark.outputs.MAIN-BENCH-18 }}
78
+ ${{ needs.benchmark.outputs.MASTER-BENCH }}
110
79
  ```
111
80
 
112
81
  - uses: actions-ecosystem/action-remove-labels@v1
@@ -2,6 +2,11 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
+ branches:
6
+ - main
7
+ - master
8
+ - next
9
+ - 'v*'
5
10
  paths-ignore:
6
11
  - 'docs/**'
7
12
  - '*.md'
@@ -3,7 +3,7 @@
3
3
  const { workerData: benchmark, parentPort } = require('worker_threads')
4
4
 
5
5
  const Benchmark = require('benchmark')
6
- Benchmark.options.minSamples = 500
6
+ Benchmark.options.minSamples = 100
7
7
 
8
8
  const suite = Benchmark.Suite()
9
9
 
@@ -18,6 +18,11 @@ function getRandomInt (max) {
18
18
  return Math.floor(Math.random() * max)
19
19
  }
20
20
 
21
+ let longSimpleString = ''
22
+ for (let i = 0; i < LONG_STRING_LENGTH; i++) {
23
+ longSimpleString += i
24
+ }
25
+
21
26
  let longString = ''
22
27
  for (let i = 0; i < LONG_STRING_LENGTH; i++) {
23
28
  longString += i
@@ -42,6 +47,20 @@ const benchmarks = [
42
47
  },
43
48
  input: 'hello world'
44
49
  },
50
+ {
51
+ name: 'short string with double quote',
52
+ schema: {
53
+ type: 'string'
54
+ },
55
+ input: 'hello " world'
56
+ },
57
+ {
58
+ name: 'long string without double quotes',
59
+ schema: {
60
+ type: 'string'
61
+ },
62
+ input: longSimpleString
63
+ },
45
64
  {
46
65
  name: 'long string',
47
66
  schema: {
@@ -235,6 +254,28 @@ const benchmarks = [
235
254
  },
236
255
  input: { firstName: 'Max', lastName: 'Power', age: 22 }
237
256
  },
257
+ {
258
+ name: 'simple object with required fields',
259
+ schema: {
260
+ title: 'Example Schema',
261
+ type: 'object',
262
+ properties: {
263
+ firstName: {
264
+ type: 'string'
265
+ },
266
+ lastName: {
267
+ type: ['string', 'null']
268
+ },
269
+ age: {
270
+ description: 'Age in years',
271
+ type: 'integer',
272
+ minimum: 0
273
+ }
274
+ },
275
+ required: ['firstName', 'lastName', 'age']
276
+ },
277
+ input: { firstName: 'Max', lastName: 'Power', age: 22 }
278
+ },
238
279
  {
239
280
  name: 'object with const string property',
240
281
  schema: {
package/index.js CHANGED
@@ -76,6 +76,7 @@ function build (schema, options) {
76
76
  functionsCounter: 0,
77
77
  functionsNamesBySchema: new Map(),
78
78
  options,
79
+ wrapObjects: true,
79
80
  refResolver: new RefResolver(),
80
81
  rootSchemaId: schema.$id || randomUUID(),
81
82
  validatorSchemasIds: new Set()
@@ -123,7 +124,7 @@ function build (schema, options) {
123
124
 
124
125
  // If we have only the invocation of the 'anonymous0' function, we would
125
126
  // basically just wrap the 'anonymous0' function in the 'main' function and
126
- // and the overhead of the intermediate variabe 'json'. We can avoid the
127
+ // and the overhead of the intermediate variable 'json'. We can avoid the
127
128
  // wrapping and the unnecessary memory allocation by aliasing 'anonymous0' to
128
129
  // 'main'
129
130
  if (code === 'json += anonymous0(input)') {
@@ -297,59 +298,96 @@ function buildExtraObjectPropertiesSerializer (context, location) {
297
298
  }
298
299
 
299
300
  function buildInnerObject (context, location) {
301
+ let code = ''
300
302
  const schema = location.schema
301
303
  const required = schema.required || []
302
304
 
303
- let code = ''
304
-
305
305
  const propertiesLocation = location.getPropertyLocation('properties')
306
- Object.keys(schema.properties || {}).forEach((key) => {
307
- let propertyLocation = propertiesLocation.getPropertyLocation(key)
308
- if (propertyLocation.schema.$ref) {
309
- propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
310
- }
311
306
 
312
- const sanitized = JSON.stringify(key)
307
+ const requiredWithDefault = []
308
+ const requiredWithoutDefault = []
309
+ if (schema.properties) {
310
+ for (const key of Object.keys(schema.properties)) {
311
+ if (required.indexOf(key) === -1) {
312
+ continue
313
+ }
314
+ let propertyLocation = propertiesLocation.getPropertyLocation(key)
315
+ if (propertyLocation.schema.$ref) {
316
+ propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
317
+ }
313
318
 
314
- // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
315
- // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
319
+ const sanitizedKey = JSON.stringify(key)
316
320
 
317
- code += `
318
- if (obj[${sanitized}] !== undefined) {
319
- ${addComma}
320
- json += ${JSON.stringify(sanitized + ':')}
321
- `
321
+ // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
322
+ // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
323
+ const defaultValue = propertyLocation.schema.default
324
+ if (defaultValue === undefined) {
325
+ code += `if (obj[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey} is required!')\n`
326
+ requiredWithoutDefault.push(key)
327
+ }
328
+ requiredWithDefault.push(key)
329
+ }
330
+ }
322
331
 
323
- code += buildValue(context, propertyLocation, `obj[${sanitized}]`)
332
+ // handle extraneous required fields
333
+ for (const requiredProperty of required) {
334
+ if (requiredWithDefault.indexOf(requiredProperty) !== -1) continue
335
+ code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
336
+ }
324
337
 
325
- const defaultValue = propertyLocation.schema.default
326
- if (defaultValue !== undefined) {
327
- code += `
328
- } else {
338
+ code += `
339
+ let addComma = false
340
+ let json = '${context.wrapObjects ? '{' : ''}'
341
+ `
342
+ const wrapObjects = context.wrapObjects
343
+ context.wrapObjects = true
344
+
345
+ if (schema.properties) {
346
+ for (const key of Object.keys(schema.properties)) {
347
+ let propertyLocation = propertiesLocation.getPropertyLocation(key)
348
+ if (propertyLocation.schema.$ref) {
349
+ propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
350
+ }
351
+
352
+ const sanitizedKey = JSON.stringify(key)
353
+
354
+ if (requiredWithoutDefault.indexOf(key) !== -1) {
355
+ code += `
329
356
  ${addComma}
330
- json += ${JSON.stringify(sanitized + ':' + JSON.stringify(defaultValue))}
357
+ json += ${JSON.stringify(sanitizedKey + ':')}
358
+ ${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
331
359
  `
332
- } else if (required.includes(key)) {
333
- code += `
334
360
  } else {
335
- throw new Error('${sanitized} is required!')
336
- `
337
- }
338
-
339
- code += `
361
+ // Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
362
+ // see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
363
+ code += `
364
+ if (obj[${sanitizedKey}] !== undefined) {
365
+ ${addComma}
366
+ json += ${JSON.stringify(sanitizedKey + ':')}
367
+ ${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
368
+ }
369
+ `
370
+ const defaultValue = propertyLocation.schema.default
371
+ if (defaultValue !== undefined) {
372
+ code += `
373
+ else {
374
+ ${addComma}
375
+ json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
376
+ }
377
+ `
378
+ }
340
379
  }
341
- `
342
- })
343
-
344
- for (const requiredProperty of required) {
345
- if (schema.properties && schema.properties[requiredProperty] !== undefined) continue
346
- code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
380
+ }
347
381
  }
348
382
 
349
383
  if (schema.patternProperties || schema.additionalProperties) {
350
384
  code += buildExtraObjectPropertiesSerializer(context, location)
351
385
  }
352
386
 
387
+ context.wrapObjects = wrapObjects
388
+ code += `
389
+ return json${context.wrapObjects ? ' + \'}\'' : ''}
390
+ `
353
391
  return code
354
392
  }
355
393
 
@@ -504,19 +542,13 @@ function buildObject (context, location) {
504
542
  }
505
543
 
506
544
  let functionCode = `
507
- function ${functionName} (input) {
508
- // ${schemaRef}
509
545
  `
510
546
 
511
547
  functionCode += `
548
+ // ${schemaRef}
549
+ function ${functionName} (input) {
512
550
  const obj = ${toJSON('input')}
513
- let json = '{'
514
- let addComma = false
515
- `
516
-
517
- functionCode += buildInnerObject(context, location)
518
- functionCode += `
519
- return json + '}'
551
+ ${buildInnerObject(context, location)}
520
552
  }
521
553
  `
522
554
 
@@ -832,9 +864,19 @@ function buildValue (context, location, input) {
832
864
 
833
865
  let code = ''
834
866
 
835
- if (type === undefined && (schema.anyOf || schema.oneOf)) {
867
+ if ((type === undefined || type === 'object') && (schema.anyOf || schema.oneOf)) {
836
868
  context.validatorSchemasIds.add(location.getSchemaId())
837
869
 
870
+ if (schema.type === 'object') {
871
+ context.wrapObjects = false
872
+ const funcName = buildObject(context, location)
873
+ code += `
874
+ json += '{'
875
+ json += ${funcName}(${input})
876
+ json += ','
877
+ `
878
+ }
879
+
838
880
  const type = schema.anyOf ? 'anyOf' : 'oneOf'
839
881
  const anyOfLocation = location.getPropertyLocation(type)
840
882
 
@@ -856,6 +898,12 @@ function buildValue (context, location, input) {
856
898
  code += `
857
899
  else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
858
900
  `
901
+ if (schema.type === 'object') {
902
+ code += `
903
+ json += '}'
904
+ `
905
+ context.wrapObjects = true
906
+ }
859
907
  return code
860
908
  }
861
909
 
package/lib/serializer.js CHANGED
@@ -113,11 +113,10 @@ module.exports = class Serializer {
113
113
  }
114
114
  }
115
115
 
116
- // Fast escape chars check
117
- if (!STR_ESCAPE.test(str)) {
118
- return '"' + str + '"'
119
- } else if (str.length < 42) {
116
+ if (str.length < 42) {
120
117
  return this.asStringSmall(str)
118
+ } else if (STR_ESCAPE.test(str) === false) {
119
+ return '"' + str + '"'
121
120
  } else {
122
121
  return JSON.stringify(str)
123
122
  }
@@ -130,32 +129,32 @@ module.exports = class Serializer {
130
129
  // 34 and 92 happens all the time, so we
131
130
  // have a fast case for them
132
131
  asStringSmall (str) {
133
- const l = str.length
132
+ const len = str.length
134
133
  let result = ''
135
- let last = 0
136
- let found = false
137
- let surrogateFound = false
134
+ let last = -1
138
135
  let point = 255
136
+
139
137
  // eslint-disable-next-line
140
- for (var i = 0; i < l && point >= 32; i++) {
138
+ for (var i = 0; i < len; i++) {
141
139
  point = str.charCodeAt(i)
140
+ if (point < 32) {
141
+ return JSON.stringify(str)
142
+ }
142
143
  if (point >= 0xD800 && point <= 0xDFFF) {
143
144
  // The current character is a surrogate.
144
- surrogateFound = true
145
+ return JSON.stringify(str)
145
146
  }
146
- if (point === 34 || point === 92) {
147
+ if (
148
+ point === 0x22 || // '"'
149
+ point === 0x5c // '\'
150
+ ) {
151
+ last === -1 && (last = 0)
147
152
  result += str.slice(last, i) + '\\'
148
153
  last = i
149
- found = true
150
154
  }
151
155
  }
152
156
 
153
- if (!found) {
154
- result = str
155
- } else {
156
- result += str.slice(last)
157
- }
158
- return ((point < 32) || (surrogateFound === true)) ? JSON.stringify(str) : '"' + result + '"'
157
+ return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
159
158
  }
160
159
 
161
160
  getState () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.7.0",
3
+ "version": "5.8.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
@@ -34,7 +34,7 @@
34
34
  "homepage": "https://github.com/fastify/fast-json-stringify#readme",
35
35
  "devDependencies": {
36
36
  "@fastify/pre-commit": "^2.0.2",
37
- "@sinclair/typebox": "^0.26.5",
37
+ "@sinclair/typebox": "^0.29.1",
38
38
  "benchmark": "^2.1.4",
39
39
  "cli-select": "^1.1.2",
40
40
  "compile-json-stringify": "^0.1.2",
@@ -220,6 +220,75 @@ test('object with nested allOfs', (t) => {
220
220
  t.equal(value, '{"id1":1,"id2":2,"id3":3}')
221
221
  })
222
222
 
223
+ test('object with anyOf nested inside allOf', (t) => {
224
+ t.plan(1)
225
+
226
+ const schema = {
227
+ title: 'object with anyOf nested inside allOf',
228
+ type: 'object',
229
+ allOf: [
230
+ {
231
+ required: ['id1', 'obj'],
232
+ type: 'object',
233
+ properties: {
234
+ id1: {
235
+ type: 'integer'
236
+ },
237
+ obj: {
238
+ type: 'object',
239
+ properties: {
240
+ nested: { type: 'string' }
241
+ }
242
+ }
243
+ }
244
+ },
245
+ {
246
+ anyOf: [
247
+ {
248
+ type: 'object',
249
+ properties: {
250
+ id2: { type: 'string' }
251
+ },
252
+ required: ['id2']
253
+ },
254
+ {
255
+ type: 'object',
256
+ properties: {
257
+ id3: {
258
+ type: 'integer'
259
+ },
260
+ nestedObj: {
261
+ type: 'object',
262
+ properties: {
263
+ nested: { type: 'string' }
264
+ }
265
+ }
266
+ },
267
+ required: ['id3']
268
+ },
269
+ {
270
+ type: 'object',
271
+ properties: {
272
+ id4: { type: 'integer' }
273
+ },
274
+ required: ['id4']
275
+ }
276
+ ]
277
+ }
278
+ ]
279
+ }
280
+
281
+ const stringify = build(schema)
282
+ const value = stringify({
283
+ id1: 1,
284
+ id3: 3,
285
+ id4: 4, // extra prop shouldn't be in result
286
+ obj: { nested: 'yes' },
287
+ nestedObj: { nested: 'yes' }
288
+ })
289
+ t.equal(value, '{"id1":1,"obj":{"nested":"yes"},"id3":3,"nestedObj":{"nested":"yes"}}')
290
+ })
291
+
223
292
  test('object with $ref in allOf', (t) => {
224
293
  t.plan(1)
225
294
 
@@ -344,7 +344,7 @@ test('patternProperties - throw on unknown type', (t) => {
344
344
  }
345
345
  })
346
346
 
347
- test('render a single quote as JSON', (t) => {
347
+ test('render a double quote as JSON /1', (t) => {
348
348
  t.plan(2)
349
349
 
350
350
  const schema = {
@@ -360,6 +360,38 @@ test('render a single quote as JSON', (t) => {
360
360
  t.ok(validate(JSON.parse(output)), 'valid schema')
361
361
  })
362
362
 
363
+ test('render a double quote as JSON /2', (t) => {
364
+ t.plan(2)
365
+
366
+ const schema = {
367
+ type: 'string'
368
+ }
369
+ const toStringify = 'double quote " 2'
370
+
371
+ const validate = validator(schema)
372
+ const stringify = build(schema)
373
+ const output = stringify(toStringify)
374
+
375
+ t.equal(output, JSON.stringify(toStringify))
376
+ t.ok(validate(JSON.parse(output)), 'valid schema')
377
+ })
378
+
379
+ test('render a long string', (t) => {
380
+ t.plan(2)
381
+
382
+ const schema = {
383
+ type: 'string'
384
+ }
385
+ const toStringify = 'the Ultimate Question of Life, the Universe, and Everything.'
386
+
387
+ const validate = validator(schema)
388
+ const stringify = build(schema)
389
+ const output = stringify(toStringify)
390
+
391
+ t.equal(output, JSON.stringify(toStringify))
392
+ t.ok(validate(JSON.parse(output)), 'valid schema')
393
+ })
394
+
363
395
  test('returns JSON.stringify if schema type is boolean', t => {
364
396
  t.plan(1)
365
397