fast-json-stringify 5.6.2 → 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.
- package/.github/workflows/benchmark.yml +13 -44
- package/.github/workflows/ci.yml +5 -0
- package/README.md +1 -2
- package/benchmark/bench-thread.js +1 -1
- package/benchmark/bench.js +41 -0
- package/index.js +100 -61
- package/lib/serializer.js +27 -19
- package/lib/standalone.js +22 -33
- package/lib/validator.js +19 -0
- package/package.json +5 -4
- package/test/allof.test.js +69 -0
- package/test/basic.test.js +33 -1
- package/test/integer.test.js +12 -0
- package/test/ref.test.js +39 -0
|
@@ -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
|
|
15
|
-
|
|
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:
|
|
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
|
|
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
|
|
39
|
+
echo "::set-output name=BENCH_RESULT::$content"
|
|
47
40
|
|
|
48
|
-
#
|
|
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
|
|
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
|
|
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
|
|
74
|
+
${{ needs.benchmark.outputs.PR-BENCH }}
|
|
82
75
|
```
|
|
83
|
-
**
|
|
76
|
+
**MASTER**:
|
|
84
77
|
```
|
|
85
|
-
${{ needs.benchmark.outputs.
|
|
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
|
package/.github/workflows/ci.yml
CHANGED
package/README.md
CHANGED
|
@@ -689,8 +689,7 @@ console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"F
|
|
|
689
689
|
### Standalone Mode
|
|
690
690
|
|
|
691
691
|
The standalone mode is used to compile the code that can be directly run by `node`
|
|
692
|
-
itself. You need to
|
|
693
|
-
the standalone code to work.
|
|
692
|
+
itself. You need to have `fast-json-stringify` installed for the standalone code to work.
|
|
694
693
|
|
|
695
694
|
```js
|
|
696
695
|
const fs = require('fs')
|
package/benchmark/bench.js
CHANGED
|
@@ -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
|
|
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)') {
|
|
@@ -170,18 +171,15 @@ function build (schema, options) {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
/* eslint no-new-func: "off" */
|
|
175
|
+
const contextFunc = new Function('validator', 'serializer', contextFunctionCode)
|
|
176
|
+
|
|
173
177
|
if (options.mode === 'standalone') {
|
|
174
|
-
// lazy load
|
|
175
|
-
const isValidatorUsed = context.validatorSchemasIds.size > 0
|
|
176
178
|
const buildStandaloneCode = require('./lib/standalone')
|
|
177
|
-
return buildStandaloneCode(
|
|
179
|
+
return buildStandaloneCode(contextFunc, context, serializer, validator)
|
|
178
180
|
}
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
const contextFunc = new Function('validator', 'serializer', contextFunctionCode)
|
|
182
|
-
const stringifyFunc = contextFunc(validator, serializer)
|
|
183
|
-
|
|
184
|
-
return stringifyFunc
|
|
182
|
+
return contextFunc(validator, serializer)
|
|
185
183
|
}
|
|
186
184
|
|
|
187
185
|
const objectKeywords = [
|
|
@@ -300,59 +298,96 @@ function buildExtraObjectPropertiesSerializer (context, location) {
|
|
|
300
298
|
}
|
|
301
299
|
|
|
302
300
|
function buildInnerObject (context, location) {
|
|
301
|
+
let code = ''
|
|
303
302
|
const schema = location.schema
|
|
304
303
|
const required = schema.required || []
|
|
305
304
|
|
|
306
|
-
let code = ''
|
|
307
|
-
|
|
308
305
|
const propertiesLocation = location.getPropertyLocation('properties')
|
|
309
|
-
Object.keys(schema.properties || {}).forEach((key) => {
|
|
310
|
-
let propertyLocation = propertiesLocation.getPropertyLocation(key)
|
|
311
|
-
if (propertyLocation.schema.$ref) {
|
|
312
|
-
propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
|
|
313
|
-
}
|
|
314
306
|
|
|
315
|
-
|
|
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
|
+
}
|
|
316
318
|
|
|
317
|
-
|
|
318
|
-
// see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
|
|
319
|
+
const sanitizedKey = JSON.stringify(key)
|
|
319
320
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
}
|
|
325
331
|
|
|
326
|
-
|
|
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
|
+
}
|
|
327
337
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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 += `
|
|
332
356
|
${addComma}
|
|
333
|
-
json += ${JSON.stringify(
|
|
357
|
+
json += ${JSON.stringify(sanitizedKey + ':')}
|
|
358
|
+
${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
|
|
334
359
|
`
|
|
335
|
-
} else if (required.includes(key)) {
|
|
336
|
-
code += `
|
|
337
360
|
} else {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
}
|
|
343
379
|
}
|
|
344
|
-
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
for (const requiredProperty of required) {
|
|
348
|
-
if (schema.properties && schema.properties[requiredProperty] !== undefined) continue
|
|
349
|
-
code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
|
|
380
|
+
}
|
|
350
381
|
}
|
|
351
382
|
|
|
352
383
|
if (schema.patternProperties || schema.additionalProperties) {
|
|
353
384
|
code += buildExtraObjectPropertiesSerializer(context, location)
|
|
354
385
|
}
|
|
355
386
|
|
|
387
|
+
context.wrapObjects = wrapObjects
|
|
388
|
+
code += `
|
|
389
|
+
return json${context.wrapObjects ? ' + \'}\'' : ''}
|
|
390
|
+
`
|
|
356
391
|
return code
|
|
357
392
|
}
|
|
358
393
|
|
|
@@ -507,19 +542,13 @@ function buildObject (context, location) {
|
|
|
507
542
|
}
|
|
508
543
|
|
|
509
544
|
let functionCode = `
|
|
510
|
-
function ${functionName} (input) {
|
|
511
|
-
// ${schemaRef}
|
|
512
545
|
`
|
|
513
546
|
|
|
514
547
|
functionCode += `
|
|
548
|
+
// ${schemaRef}
|
|
549
|
+
function ${functionName} (input) {
|
|
515
550
|
const obj = ${toJSON('input')}
|
|
516
|
-
|
|
517
|
-
let addComma = false
|
|
518
|
-
`
|
|
519
|
-
|
|
520
|
-
functionCode += buildInnerObject(context, location)
|
|
521
|
-
functionCode += `
|
|
522
|
-
return json + '}'
|
|
551
|
+
${buildInnerObject(context, location)}
|
|
523
552
|
}
|
|
524
553
|
`
|
|
525
554
|
|
|
@@ -571,12 +600,8 @@ function buildArray (context, location) {
|
|
|
571
600
|
`
|
|
572
601
|
}
|
|
573
602
|
|
|
574
|
-
if (largeArrayMechanism
|
|
575
|
-
if (
|
|
576
|
-
functionCode += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
|
|
577
|
-
} else {
|
|
578
|
-
throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`)
|
|
579
|
-
}
|
|
603
|
+
if (largeArrayMechanism === 'json-stringify') {
|
|
604
|
+
functionCode += `if (arrayLength && arrayLength >= ${largeArraySize}) return JSON.stringify(obj)\n`
|
|
580
605
|
}
|
|
581
606
|
|
|
582
607
|
functionCode += `
|
|
@@ -663,8 +688,6 @@ function buildArrayTypeCondition (type, accessor) {
|
|
|
663
688
|
return buildArrayTypeCondition(subType, accessor)
|
|
664
689
|
})
|
|
665
690
|
condition = `(${conditions.join(' || ')})`
|
|
666
|
-
} else {
|
|
667
|
-
throw new Error(`${type} unsupported`)
|
|
668
691
|
}
|
|
669
692
|
}
|
|
670
693
|
return condition
|
|
@@ -841,9 +864,19 @@ function buildValue (context, location, input) {
|
|
|
841
864
|
|
|
842
865
|
let code = ''
|
|
843
866
|
|
|
844
|
-
if (type === undefined && (schema.anyOf || schema.oneOf)) {
|
|
867
|
+
if ((type === undefined || type === 'object') && (schema.anyOf || schema.oneOf)) {
|
|
845
868
|
context.validatorSchemasIds.add(location.getSchemaId())
|
|
846
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
|
+
|
|
847
880
|
const type = schema.anyOf ? 'anyOf' : 'oneOf'
|
|
848
881
|
const anyOfLocation = location.getPropertyLocation(type)
|
|
849
882
|
|
|
@@ -865,6 +898,12 @@ function buildValue (context, location, input) {
|
|
|
865
898
|
code += `
|
|
866
899
|
else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
|
|
867
900
|
`
|
|
901
|
+
if (schema.type === 'object') {
|
|
902
|
+
code += `
|
|
903
|
+
json += '}'
|
|
904
|
+
`
|
|
905
|
+
context.wrapObjects = true
|
|
906
|
+
}
|
|
868
907
|
return code
|
|
869
908
|
}
|
|
870
909
|
|
package/lib/serializer.js
CHANGED
|
@@ -20,6 +20,7 @@ module.exports = class Serializer {
|
|
|
20
20
|
this.parseInteger = Math.trunc
|
|
21
21
|
break
|
|
22
22
|
}
|
|
23
|
+
this._options = options
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
asInteger (i) {
|
|
@@ -54,7 +55,7 @@ module.exports = class Serializer {
|
|
|
54
55
|
if (Number.isNaN(num)) {
|
|
55
56
|
throw new Error(`The value "${i}" cannot be converted to a number.`)
|
|
56
57
|
} else if (!Number.isFinite(num)) {
|
|
57
|
-
return null
|
|
58
|
+
return 'null'
|
|
58
59
|
} else {
|
|
59
60
|
return '' + num
|
|
60
61
|
}
|
|
@@ -112,11 +113,10 @@ module.exports = class Serializer {
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
if (!STR_ESCAPE.test(str)) {
|
|
117
|
-
return '"' + str + '"'
|
|
118
|
-
} else if (str.length < 42) {
|
|
116
|
+
if (str.length < 42) {
|
|
119
117
|
return this.asStringSmall(str)
|
|
118
|
+
} else if (STR_ESCAPE.test(str) === false) {
|
|
119
|
+
return '"' + str + '"'
|
|
120
120
|
} else {
|
|
121
121
|
return JSON.stringify(str)
|
|
122
122
|
}
|
|
@@ -129,31 +129,39 @@ module.exports = class Serializer {
|
|
|
129
129
|
// 34 and 92 happens all the time, so we
|
|
130
130
|
// have a fast case for them
|
|
131
131
|
asStringSmall (str) {
|
|
132
|
-
const
|
|
132
|
+
const len = str.length
|
|
133
133
|
let result = ''
|
|
134
|
-
let last =
|
|
135
|
-
let found = false
|
|
136
|
-
let surrogateFound = false
|
|
134
|
+
let last = -1
|
|
137
135
|
let point = 255
|
|
136
|
+
|
|
138
137
|
// eslint-disable-next-line
|
|
139
|
-
for (var i = 0; i <
|
|
138
|
+
for (var i = 0; i < len; i++) {
|
|
140
139
|
point = str.charCodeAt(i)
|
|
140
|
+
if (point < 32) {
|
|
141
|
+
return JSON.stringify(str)
|
|
142
|
+
}
|
|
141
143
|
if (point >= 0xD800 && point <= 0xDFFF) {
|
|
142
144
|
// The current character is a surrogate.
|
|
143
|
-
|
|
145
|
+
return JSON.stringify(str)
|
|
144
146
|
}
|
|
145
|
-
if (
|
|
147
|
+
if (
|
|
148
|
+
point === 0x22 || // '"'
|
|
149
|
+
point === 0x5c // '\'
|
|
150
|
+
) {
|
|
151
|
+
last === -1 && (last = 0)
|
|
146
152
|
result += str.slice(last, i) + '\\'
|
|
147
153
|
last = i
|
|
148
|
-
found = true
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getState () {
|
|
161
|
+
return this._options
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static restoreFromState (state) {
|
|
165
|
+
return new Serializer(state)
|
|
158
166
|
}
|
|
159
167
|
}
|
package/lib/standalone.js
CHANGED
|
@@ -1,42 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let defaultAjvSchema = ''
|
|
11
|
-
// we need to export the custom json schema
|
|
12
|
-
const defaultMeta = validator.ajv.defaultMeta()
|
|
13
|
-
if (typeof defaultMeta === 'string') {
|
|
14
|
-
defaultAjvSchema = defaultMeta
|
|
15
|
-
} else {
|
|
16
|
-
defaultAjvSchema = defaultMeta.$id || defaultMeta.id
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
ajvSchemasCode += `const validator = new Validator(${JSON.stringify(options.ajv || {})})\n`
|
|
20
|
-
for (const [id, schema] of Object.entries(validator.ajv.schemas)) {
|
|
21
|
-
// should skip ajv default schema
|
|
22
|
-
if (id === defaultAjvSchema) continue
|
|
23
|
-
ajvSchemasCode += `validator.ajv.addSchema(${JSON.stringify(schema.schema)}, "${id}")\n`
|
|
24
|
-
}
|
|
25
|
-
buildAjvCode = fs.readFileSync(path.join(__dirname, 'validator.js')).toString()
|
|
26
|
-
buildAjvCode = buildAjvCode.replace("'use strict'", '').replace('module.exports = SchemaValidator', '')
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function buildStandaloneCode (contextFunc, context, serializer, validator) {
|
|
4
|
+
let ajvDependencyCode = ''
|
|
5
|
+
if (context.validatorSchemasIds.size > 0) {
|
|
6
|
+
ajvDependencyCode += `const validatorState = ${JSON.stringify(validator.getState())}\n`
|
|
7
|
+
ajvDependencyCode += 'const validator = Validator.restoreFromState(validatorState)\n'
|
|
8
|
+
} else {
|
|
9
|
+
ajvDependencyCode += 'const validator = null\n'
|
|
27
10
|
}
|
|
28
11
|
|
|
29
|
-
const serializerOptions = options && options.rounding ? JSON.stringify({ options: options.rounding }) : ''
|
|
30
12
|
return `
|
|
31
13
|
'use strict'
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
14
|
+
const { dependencies } = require('fast-json-stringify/lib/standalone')
|
|
15
|
+
|
|
16
|
+
const { Serializer, Validator } = dependencies
|
|
17
|
+
|
|
18
|
+
const serializerState = ${JSON.stringify(serializer.getState())}
|
|
19
|
+
const serializer = Serializer.restoreFromState(serializerState)
|
|
36
20
|
|
|
37
|
-
${
|
|
21
|
+
${ajvDependencyCode}
|
|
38
22
|
|
|
39
|
-
module.exports =
|
|
23
|
+
module.exports = ${contextFunc.toString()}(validator, serializer)`
|
|
40
24
|
}
|
|
41
25
|
|
|
42
26
|
module.exports = buildStandaloneCode
|
|
27
|
+
|
|
28
|
+
module.exports.dependencies = {
|
|
29
|
+
Serializer: require('./serializer'),
|
|
30
|
+
Validator: require('./validator')
|
|
31
|
+
}
|
package/lib/validator.js
CHANGED
|
@@ -25,6 +25,9 @@ class Validator {
|
|
|
25
25
|
return date instanceof Date
|
|
26
26
|
}
|
|
27
27
|
})
|
|
28
|
+
|
|
29
|
+
this._ajvSchemas = {}
|
|
30
|
+
this._ajvOptions = ajvOptions || {}
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
addSchema (schema, schemaName) {
|
|
@@ -40,6 +43,7 @@ class Validator {
|
|
|
40
43
|
const ajvSchema = clone(schema)
|
|
41
44
|
this.convertSchemaToAjvFormat(ajvSchema)
|
|
42
45
|
this.ajv.addSchema(ajvSchema, schemaKey)
|
|
46
|
+
this._ajvSchemas[schemaKey] = schema
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
49
|
|
|
@@ -70,6 +74,21 @@ class Validator {
|
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
}
|
|
77
|
+
|
|
78
|
+
getState () {
|
|
79
|
+
return {
|
|
80
|
+
ajvOptions: this._ajvOptions,
|
|
81
|
+
ajvSchemas: this._ajvSchemas
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static restoreFromState (state) {
|
|
86
|
+
const validator = new Validator(state.ajvOptions)
|
|
87
|
+
for (const [id, ajvSchema] of Object.entries(state.ajvSchemas)) {
|
|
88
|
+
validator.ajv.addSchema(ajvSchema, id)
|
|
89
|
+
}
|
|
90
|
+
return validator
|
|
91
|
+
}
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
module.exports = Validator
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-json-stringify",
|
|
3
|
-
"version": "5.
|
|
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.
|
|
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",
|
|
@@ -42,8 +42,9 @@
|
|
|
42
42
|
"simple-git": "^3.7.1",
|
|
43
43
|
"standard": "^17.0.0",
|
|
44
44
|
"tap": "^16.0.1",
|
|
45
|
-
"tsd": "^0.
|
|
46
|
-
"webpack": "^5.40.0"
|
|
45
|
+
"tsd": "^0.28.0",
|
|
46
|
+
"webpack": "^5.40.0",
|
|
47
|
+
"fast-json-stringify": "."
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@fastify/deepmerge": "^1.0.0",
|
package/test/allof.test.js
CHANGED
|
@@ -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
|
|
package/test/basic.test.js
CHANGED
|
@@ -344,7 +344,7 @@ test('patternProperties - throw on unknown type', (t) => {
|
|
|
344
344
|
}
|
|
345
345
|
})
|
|
346
346
|
|
|
347
|
-
test('render a
|
|
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
|
|
package/test/integer.test.js
CHANGED
|
@@ -35,6 +35,18 @@ test('render a float as an integer', (t) => {
|
|
|
35
35
|
}
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
+
test('throws on NaN', (t) => {
|
|
39
|
+
t.plan(1)
|
|
40
|
+
|
|
41
|
+
const schema = {
|
|
42
|
+
title: 'integer',
|
|
43
|
+
type: 'integer'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const stringify = build(schema)
|
|
47
|
+
t.throws(() => stringify(NaN), new Error('The value "NaN" cannot be converted to an integer.'))
|
|
48
|
+
})
|
|
49
|
+
|
|
38
50
|
test('render a float as an integer', (t) => {
|
|
39
51
|
const cases = [
|
|
40
52
|
{ input: Math.PI, output: '3' },
|
package/test/ref.test.js
CHANGED
|
@@ -2078,3 +2078,42 @@ test('should throw an Error if two non-identical schemas with same id are provid
|
|
|
2078
2078
|
|
|
2079
2079
|
t.throws(() => build(schema), new Error('There is already another schema with id inner_schema'))
|
|
2080
2080
|
})
|
|
2081
|
+
|
|
2082
|
+
test('ref internal - throw if schema has definition twice with different shape', (t) => {
|
|
2083
|
+
t.plan(1)
|
|
2084
|
+
|
|
2085
|
+
const schema = {
|
|
2086
|
+
$id: 'test',
|
|
2087
|
+
title: 'object with $ref',
|
|
2088
|
+
definitions: {
|
|
2089
|
+
def: {
|
|
2090
|
+
$id: '#uri',
|
|
2091
|
+
type: 'object',
|
|
2092
|
+
properties: {
|
|
2093
|
+
str: {
|
|
2094
|
+
type: 'string'
|
|
2095
|
+
}
|
|
2096
|
+
},
|
|
2097
|
+
required: ['str']
|
|
2098
|
+
},
|
|
2099
|
+
def2: {
|
|
2100
|
+
$id: '#uri',
|
|
2101
|
+
type: 'object',
|
|
2102
|
+
properties: {
|
|
2103
|
+
num: {
|
|
2104
|
+
type: 'number'
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
required: ['num']
|
|
2108
|
+
}
|
|
2109
|
+
},
|
|
2110
|
+
type: 'object',
|
|
2111
|
+
properties: {
|
|
2112
|
+
obj: {
|
|
2113
|
+
$ref: '#uri'
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
t.throws(() => build(schema), Error('There is already another schema with id test##uri'))
|
|
2119
|
+
})
|