fast-json-stringify 5.7.0 → 5.9.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/.gitattributes +2 -0
- package/.github/workflows/benchmark.yml +16 -47
- package/.github/workflows/ci.yml +5 -0
- package/README.md +1 -1
- package/benchmark/bench-cmp-branch.js +1 -1
- package/benchmark/bench-thread.js +2 -2
- package/benchmark/bench.js +43 -2
- package/build/build-schema-validator.js +2 -2
- package/examples/example.js +2 -0
- package/examples/server.js +1 -1
- package/index.js +117 -54
- package/lib/serializer.js +17 -18
- package/package.json +6 -4
- package/test/allof.test.js +69 -0
- package/test/basic.test.js +33 -1
- package/test/const.test.js +20 -0
- package/test/ref.test.js +14 -3
- package/test/standalone-mode.test.js +2 -2
- package/test/webpack.test.js +1 -1
- package/lib/ref-resolver.js +0 -104
package/.gitattributes
ADDED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
name: Benchmark
|
|
1
|
+
name: Benchmark PR
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
pull_request_target:
|
|
@@ -11,25 +11,18 @@ 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
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@v4
|
|
25
18
|
with:
|
|
26
19
|
persist-credentials: false
|
|
27
20
|
ref: ${{github.event.pull_request.head.sha}}
|
|
28
21
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
|
29
22
|
|
|
30
|
-
- uses: actions/setup-node@
|
|
23
|
+
- uses: actions/setup-node@v4
|
|
31
24
|
with:
|
|
32
|
-
node-version:
|
|
25
|
+
node-version: 18
|
|
33
26
|
|
|
34
27
|
- name: Install
|
|
35
28
|
run: |
|
|
@@ -38,15 +31,15 @@ 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
|
-
#
|
|
49
|
-
- uses: actions/checkout@
|
|
41
|
+
# master benchmark
|
|
42
|
+
- uses: actions/checkout@v4
|
|
50
43
|
with:
|
|
51
44
|
ref: 'master'
|
|
52
45
|
|
|
@@ -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
|
@@ -692,7 +692,7 @@ The standalone mode is used to compile the code that can be directly run by `nod
|
|
|
692
692
|
itself. You need to have `fast-json-stringify` installed for the standalone code to work.
|
|
693
693
|
|
|
694
694
|
```js
|
|
695
|
-
const fs = require('fs')
|
|
695
|
+
const fs = require('node:fs')
|
|
696
696
|
const code = fastJson({
|
|
697
697
|
title: 'default string',
|
|
698
698
|
type: 'object',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { workerData: benchmark, parentPort } = require('worker_threads')
|
|
3
|
+
const { workerData: benchmark, parentPort } = require('node:worker_threads')
|
|
4
4
|
|
|
5
5
|
const Benchmark = require('benchmark')
|
|
6
|
-
Benchmark.options.minSamples =
|
|
6
|
+
Benchmark.options.minSamples = 100
|
|
7
7
|
|
|
8
8
|
const suite = Benchmark.Suite()
|
|
9
9
|
|
package/benchmark/bench.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const path = require('path')
|
|
4
|
-
const { Worker } = require('worker_threads')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const { Worker } = require('node:worker_threads')
|
|
5
5
|
|
|
6
6
|
const BENCH_THREAD_PATH = path.join(__dirname, 'bench-thread.js')
|
|
7
7
|
|
|
@@ -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: {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
const Ajv = require('ajv')
|
|
4
4
|
const standaloneCode = require('ajv/dist/standalone').default
|
|
5
5
|
const ajvFormats = require('ajv-formats')
|
|
6
|
-
const fs = require('fs')
|
|
7
|
-
const path = require('path')
|
|
6
|
+
const fs = require('node:fs')
|
|
7
|
+
const path = require('node:path')
|
|
8
8
|
|
|
9
9
|
const ajv = new Ajv({
|
|
10
10
|
addUsedSchema: false,
|
package/examples/example.js
CHANGED
package/examples/server.js
CHANGED
package/index.js
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
const merge = require('@fastify/deepmerge')()
|
|
6
6
|
const clone = require('rfdc')({ proto: true })
|
|
7
|
-
const { randomUUID } = require('crypto')
|
|
7
|
+
const { randomUUID } = require('node:crypto')
|
|
8
|
+
const { RefResolver } = require('json-schema-ref-resolver')
|
|
8
9
|
|
|
9
10
|
const validate = require('./lib/schema-validator')
|
|
10
11
|
const Serializer = require('./lib/serializer')
|
|
11
12
|
const Validator = require('./lib/validator')
|
|
12
|
-
const RefResolver = require('./lib/ref-resolver')
|
|
13
13
|
const Location = require('./lib/location')
|
|
14
14
|
|
|
15
|
+
const SINGLE_TICK = /'/g
|
|
16
|
+
|
|
15
17
|
let largeArraySize = 2e4
|
|
16
18
|
let largeArrayMechanism = 'default'
|
|
17
19
|
|
|
@@ -53,8 +55,7 @@ function resolveRef (context, location, ref) {
|
|
|
53
55
|
const jsonPointer = ref.slice(hashIndex) || '#'
|
|
54
56
|
|
|
55
57
|
const schema = context.refResolver.getSchema(schemaId, jsonPointer)
|
|
56
|
-
|
|
57
|
-
if (schema === undefined) {
|
|
58
|
+
if (schema === null) {
|
|
58
59
|
throw new Error(`Cannot find reference "${ref}"`)
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -66,6 +67,13 @@ function resolveRef (context, location, ref) {
|
|
|
66
67
|
return newLocation
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function getSchemaId (schema, rootSchemaId) {
|
|
71
|
+
if (schema.$id && schema.$id.charAt(0) !== '#') {
|
|
72
|
+
return schema.$id
|
|
73
|
+
}
|
|
74
|
+
return rootSchemaId
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
function build (schema, options) {
|
|
70
78
|
isValidSchema(schema)
|
|
71
79
|
|
|
@@ -76,17 +84,25 @@ function build (schema, options) {
|
|
|
76
84
|
functionsCounter: 0,
|
|
77
85
|
functionsNamesBySchema: new Map(),
|
|
78
86
|
options,
|
|
87
|
+
wrapObjects: true,
|
|
79
88
|
refResolver: new RefResolver(),
|
|
80
89
|
rootSchemaId: schema.$id || randomUUID(),
|
|
81
90
|
validatorSchemasIds: new Set()
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
|
|
93
|
+
const schemaId = getSchemaId(schema, context.rootSchemaId)
|
|
94
|
+
if (!context.refResolver.hasSchema(schemaId)) {
|
|
95
|
+
context.refResolver.addSchema(schema, context.rootSchemaId)
|
|
96
|
+
}
|
|
85
97
|
|
|
86
98
|
if (options.schema) {
|
|
87
|
-
for (const key
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
for (const key in options.schema) {
|
|
100
|
+
const schema = options.schema[key]
|
|
101
|
+
const schemaId = getSchemaId(schema, key)
|
|
102
|
+
if (!context.refResolver.hasSchema(schemaId)) {
|
|
103
|
+
isValidSchema(schema, key)
|
|
104
|
+
context.refResolver.addSchema(schema, key)
|
|
105
|
+
}
|
|
90
106
|
}
|
|
91
107
|
}
|
|
92
108
|
|
|
@@ -123,7 +139,7 @@ function build (schema, options) {
|
|
|
123
139
|
|
|
124
140
|
// If we have only the invocation of the 'anonymous0' function, we would
|
|
125
141
|
// basically just wrap the 'anonymous0' function in the 'main' function and
|
|
126
|
-
// and the overhead of the intermediate
|
|
142
|
+
// and the overhead of the intermediate variable 'json'. We can avoid the
|
|
127
143
|
// wrapping and the unnecessary memory allocation by aliasing 'anonymous0' to
|
|
128
144
|
// 'main'
|
|
129
145
|
if (code === 'json += anonymous0(input)') {
|
|
@@ -297,59 +313,96 @@ function buildExtraObjectPropertiesSerializer (context, location) {
|
|
|
297
313
|
}
|
|
298
314
|
|
|
299
315
|
function buildInnerObject (context, location) {
|
|
316
|
+
let code = ''
|
|
300
317
|
const schema = location.schema
|
|
301
318
|
const required = schema.required || []
|
|
302
319
|
|
|
303
|
-
let code = ''
|
|
304
|
-
|
|
305
320
|
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
321
|
|
|
312
|
-
|
|
322
|
+
const requiredWithDefault = []
|
|
323
|
+
const requiredWithoutDefault = []
|
|
324
|
+
if (schema.properties) {
|
|
325
|
+
for (const key of Object.keys(schema.properties)) {
|
|
326
|
+
if (required.indexOf(key) === -1) {
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
let propertyLocation = propertiesLocation.getPropertyLocation(key)
|
|
330
|
+
if (propertyLocation.schema.$ref) {
|
|
331
|
+
propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
|
|
332
|
+
}
|
|
313
333
|
|
|
314
|
-
|
|
315
|
-
// see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
|
|
334
|
+
const sanitizedKey = JSON.stringify(key)
|
|
316
335
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
336
|
+
// Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
|
|
337
|
+
// see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
|
|
338
|
+
const defaultValue = propertyLocation.schema.default
|
|
339
|
+
if (defaultValue === undefined) {
|
|
340
|
+
code += `if (obj[${sanitizedKey}] === undefined) throw new Error('${sanitizedKey} is required!')\n`
|
|
341
|
+
requiredWithoutDefault.push(key)
|
|
342
|
+
}
|
|
343
|
+
requiredWithDefault.push(key)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
322
346
|
|
|
323
|
-
|
|
347
|
+
// handle extraneous required fields
|
|
348
|
+
for (const requiredProperty of required) {
|
|
349
|
+
if (requiredWithDefault.indexOf(requiredProperty) !== -1) continue
|
|
350
|
+
code += `if (obj['${requiredProperty}'] === undefined) throw new Error('"${requiredProperty}" is required!')\n`
|
|
351
|
+
}
|
|
324
352
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
353
|
+
code += `
|
|
354
|
+
let addComma = false
|
|
355
|
+
let json = '${context.wrapObjects ? '{' : ''}'
|
|
356
|
+
`
|
|
357
|
+
const wrapObjects = context.wrapObjects
|
|
358
|
+
context.wrapObjects = true
|
|
359
|
+
|
|
360
|
+
if (schema.properties) {
|
|
361
|
+
for (const key of Object.keys(schema.properties)) {
|
|
362
|
+
let propertyLocation = propertiesLocation.getPropertyLocation(key)
|
|
363
|
+
if (propertyLocation.schema.$ref) {
|
|
364
|
+
propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const sanitizedKey = JSON.stringify(key)
|
|
368
|
+
|
|
369
|
+
if (requiredWithoutDefault.indexOf(key) !== -1) {
|
|
370
|
+
code += `
|
|
329
371
|
${addComma}
|
|
330
|
-
json += ${JSON.stringify(
|
|
372
|
+
json += ${JSON.stringify(sanitizedKey + ':')}
|
|
373
|
+
${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
|
|
331
374
|
`
|
|
332
|
-
} else if (required.includes(key)) {
|
|
333
|
-
code += `
|
|
334
375
|
} else {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
376
|
+
// Using obj['key'] !== undefined instead of obj.hasOwnProperty(prop) for perf reasons,
|
|
377
|
+
// see https://github.com/mcollina/fast-json-stringify/pull/3 for discussion.
|
|
378
|
+
code += `
|
|
379
|
+
if (obj[${sanitizedKey}] !== undefined) {
|
|
380
|
+
${addComma}
|
|
381
|
+
json += ${JSON.stringify(sanitizedKey + ':')}
|
|
382
|
+
${buildValue(context, propertyLocation, `obj[${sanitizedKey}]`)}
|
|
383
|
+
}
|
|
384
|
+
`
|
|
385
|
+
const defaultValue = propertyLocation.schema.default
|
|
386
|
+
if (defaultValue !== undefined) {
|
|
387
|
+
code += `
|
|
388
|
+
else {
|
|
389
|
+
${addComma}
|
|
390
|
+
json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
|
|
391
|
+
}
|
|
392
|
+
`
|
|
393
|
+
}
|
|
340
394
|
}
|
|
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`
|
|
395
|
+
}
|
|
347
396
|
}
|
|
348
397
|
|
|
349
398
|
if (schema.patternProperties || schema.additionalProperties) {
|
|
350
399
|
code += buildExtraObjectPropertiesSerializer(context, location)
|
|
351
400
|
}
|
|
352
401
|
|
|
402
|
+
context.wrapObjects = wrapObjects
|
|
403
|
+
code += `
|
|
404
|
+
return json${context.wrapObjects ? ' + \'}\'' : ''}
|
|
405
|
+
`
|
|
353
406
|
return code
|
|
354
407
|
}
|
|
355
408
|
|
|
@@ -504,19 +557,13 @@ function buildObject (context, location) {
|
|
|
504
557
|
}
|
|
505
558
|
|
|
506
559
|
let functionCode = `
|
|
507
|
-
function ${functionName} (input) {
|
|
508
|
-
// ${schemaRef}
|
|
509
560
|
`
|
|
510
561
|
|
|
511
562
|
functionCode += `
|
|
563
|
+
// ${schemaRef}
|
|
564
|
+
function ${functionName} (input) {
|
|
512
565
|
const obj = ${toJSON('input')}
|
|
513
|
-
|
|
514
|
-
let addComma = false
|
|
515
|
-
`
|
|
516
|
-
|
|
517
|
-
functionCode += buildInnerObject(context, location)
|
|
518
|
-
functionCode += `
|
|
519
|
-
return json + '}'
|
|
566
|
+
${buildInnerObject(context, location)}
|
|
520
567
|
}
|
|
521
568
|
`
|
|
522
569
|
|
|
@@ -789,7 +836,7 @@ function buildConstSerializer (location, input) {
|
|
|
789
836
|
`
|
|
790
837
|
}
|
|
791
838
|
|
|
792
|
-
code += `json += '${JSON.stringify(schema.const)}'`
|
|
839
|
+
code += `json += '${JSON.stringify(schema.const).replace(SINGLE_TICK, "\\'")}'`
|
|
793
840
|
|
|
794
841
|
if (hasNullType) {
|
|
795
842
|
code += `
|
|
@@ -832,9 +879,19 @@ function buildValue (context, location, input) {
|
|
|
832
879
|
|
|
833
880
|
let code = ''
|
|
834
881
|
|
|
835
|
-
if (type === undefined && (schema.anyOf || schema.oneOf)) {
|
|
882
|
+
if ((type === undefined || type === 'object') && (schema.anyOf || schema.oneOf)) {
|
|
836
883
|
context.validatorSchemasIds.add(location.getSchemaId())
|
|
837
884
|
|
|
885
|
+
if (schema.type === 'object') {
|
|
886
|
+
context.wrapObjects = false
|
|
887
|
+
const funcName = buildObject(context, location)
|
|
888
|
+
code += `
|
|
889
|
+
json += '{'
|
|
890
|
+
json += ${funcName}(${input})
|
|
891
|
+
json += ','
|
|
892
|
+
`
|
|
893
|
+
}
|
|
894
|
+
|
|
838
895
|
const type = schema.anyOf ? 'anyOf' : 'oneOf'
|
|
839
896
|
const anyOfLocation = location.getPropertyLocation(type)
|
|
840
897
|
|
|
@@ -856,6 +913,12 @@ function buildValue (context, location, input) {
|
|
|
856
913
|
code += `
|
|
857
914
|
else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
|
|
858
915
|
`
|
|
916
|
+
if (schema.type === 'object') {
|
|
917
|
+
code += `
|
|
918
|
+
json += '}'
|
|
919
|
+
`
|
|
920
|
+
context.wrapObjects = true
|
|
921
|
+
}
|
|
859
922
|
return code
|
|
860
923
|
}
|
|
861
924
|
|
package/lib/serializer.js
CHANGED
|
@@ -113,11 +113,10 @@ module.exports = class Serializer {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
|
|
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
|
|
132
|
+
const len = str.length
|
|
134
133
|
let result = ''
|
|
135
|
-
let last =
|
|
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 <
|
|
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
|
-
|
|
145
|
+
return JSON.stringify(str)
|
|
145
146
|
}
|
|
146
|
-
if (
|
|
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
|
-
|
|
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,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-json-stringify",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"description": "Stringify your JSON at max speed",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"type": "commonjs",
|
|
6
7
|
"types": "types/index.d.ts",
|
|
7
8
|
"scripts": {
|
|
8
9
|
"bench": "node ./benchmark/bench.js",
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"homepage": "https://github.com/fastify/fast-json-stringify#readme",
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@fastify/pre-commit": "^2.0.2",
|
|
37
|
-
"@sinclair/typebox": "^0.
|
|
38
|
+
"@sinclair/typebox": "^0.31.1",
|
|
38
39
|
"benchmark": "^2.1.4",
|
|
39
40
|
"cli-select": "^1.1.2",
|
|
40
41
|
"compile-json-stringify": "^0.1.2",
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"simple-git": "^3.7.1",
|
|
43
44
|
"standard": "^17.0.0",
|
|
44
45
|
"tap": "^16.0.1",
|
|
45
|
-
"tsd": "^0.
|
|
46
|
+
"tsd": "^0.29.0",
|
|
46
47
|
"webpack": "^5.40.0",
|
|
47
48
|
"fast-json-stringify": "."
|
|
48
49
|
},
|
|
@@ -52,7 +53,8 @@
|
|
|
52
53
|
"ajv-formats": "^2.1.1",
|
|
53
54
|
"fast-deep-equal": "^3.1.3",
|
|
54
55
|
"fast-uri": "^2.1.0",
|
|
55
|
-
"rfdc": "^1.2.0"
|
|
56
|
+
"rfdc": "^1.2.0",
|
|
57
|
+
"json-schema-ref-resolver": "^1.0.1"
|
|
56
58
|
},
|
|
57
59
|
"standard": {
|
|
58
60
|
"ignore": [
|
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/const.test.js
CHANGED
|
@@ -82,6 +82,26 @@ test('schema with const string and no input', (t) => {
|
|
|
82
82
|
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
+
test('schema with const string that contains \'', (t) => {
|
|
86
|
+
t.plan(2)
|
|
87
|
+
|
|
88
|
+
const schema = {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
foo: { const: "'bar'" }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const validate = validator(schema)
|
|
96
|
+
const stringify = build(schema)
|
|
97
|
+
const output = stringify({
|
|
98
|
+
foo: "'bar'"
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
t.equal(output, '{"foo":"\'bar\'"}')
|
|
102
|
+
t.ok(validate(JSON.parse(output)), 'valid schema')
|
|
103
|
+
})
|
|
104
|
+
|
|
85
105
|
test('schema with const number', (t) => {
|
|
86
106
|
t.plan(2)
|
|
87
107
|
|
package/test/ref.test.js
CHANGED
|
@@ -1377,7 +1377,10 @@ test('Bad key', t => {
|
|
|
1377
1377
|
})
|
|
1378
1378
|
t.fail('Should throw')
|
|
1379
1379
|
} catch (err) {
|
|
1380
|
-
t.equal(
|
|
1380
|
+
t.equal(
|
|
1381
|
+
err.message,
|
|
1382
|
+
'Cannot resolve ref "extrenal#/definitions/projectId". Schema with id "extrenal" is not found.'
|
|
1383
|
+
)
|
|
1381
1384
|
}
|
|
1382
1385
|
})
|
|
1383
1386
|
|
|
@@ -2076,7 +2079,11 @@ test('should throw an Error if two non-identical schemas with same id are provid
|
|
|
2076
2079
|
]
|
|
2077
2080
|
}
|
|
2078
2081
|
|
|
2079
|
-
|
|
2082
|
+
try {
|
|
2083
|
+
build(schema)
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
t.equal(err.message, 'There is already another schema with id "inner_schema".')
|
|
2086
|
+
}
|
|
2080
2087
|
})
|
|
2081
2088
|
|
|
2082
2089
|
test('ref internal - throw if schema has definition twice with different shape', (t) => {
|
|
@@ -2115,5 +2122,9 @@ test('ref internal - throw if schema has definition twice with different shape',
|
|
|
2115
2122
|
}
|
|
2116
2123
|
}
|
|
2117
2124
|
|
|
2118
|
-
|
|
2125
|
+
try {
|
|
2126
|
+
build(schema)
|
|
2127
|
+
} catch (err) {
|
|
2128
|
+
t.equal(err.message, 'There is already another anchor "#uri" in a schema "test".')
|
|
2129
|
+
}
|
|
2119
2130
|
})
|
package/test/webpack.test.js
CHANGED
package/lib/ref-resolver.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const deepEqual = require('fast-deep-equal')
|
|
4
|
-
|
|
5
|
-
class RefResolver {
|
|
6
|
-
constructor () {
|
|
7
|
-
this.schemas = {}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
addSchema (schema, schemaId) {
|
|
11
|
-
if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {
|
|
12
|
-
schemaId = schema.$id
|
|
13
|
-
}
|
|
14
|
-
if (this.getSchema(schemaId) === undefined) {
|
|
15
|
-
this.insertSchemaBySchemaId(schema, schemaId)
|
|
16
|
-
this.insertSchemaSubschemas(schema, schemaId)
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
getSchema (schemaId, jsonPointer = '#') {
|
|
21
|
-
const schema = this.schemas[schemaId]
|
|
22
|
-
if (schema === undefined) {
|
|
23
|
-
return undefined
|
|
24
|
-
}
|
|
25
|
-
if (schema.anchors[jsonPointer] !== undefined) {
|
|
26
|
-
return schema.anchors[jsonPointer]
|
|
27
|
-
}
|
|
28
|
-
return getDataByJSONPointer(schema.schema, jsonPointer)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
getSchemaDependencies (schemaId, dependencies = {}) {
|
|
32
|
-
const schema = this.schemas[schemaId]
|
|
33
|
-
|
|
34
|
-
for (const dependencySchemaId of schema.dependencies) {
|
|
35
|
-
if (dependencies[dependencySchemaId] !== undefined) continue
|
|
36
|
-
dependencies[dependencySchemaId] = this.getSchema(dependencySchemaId)
|
|
37
|
-
this.getSchemaDependencies(dependencySchemaId, dependencies)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return dependencies
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
insertSchemaBySchemaId (schema, schemaId) {
|
|
44
|
-
if (
|
|
45
|
-
this.schemas[schemaId] !== undefined &&
|
|
46
|
-
!deepEqual(schema, this.schemas[schemaId].schema)
|
|
47
|
-
) {
|
|
48
|
-
throw new Error(`There is already another schema with id ${schemaId}`)
|
|
49
|
-
}
|
|
50
|
-
this.schemas[schemaId] = { schema, anchors: {}, dependencies: [] }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
insertSchemaByAnchor (schema, schemaId, anchor) {
|
|
54
|
-
const { anchors } = this.schemas[schemaId]
|
|
55
|
-
if (
|
|
56
|
-
anchors[anchor] !== undefined &&
|
|
57
|
-
!deepEqual(schema, anchors[anchor])
|
|
58
|
-
) {
|
|
59
|
-
throw new Error(`There is already another schema with id ${schemaId}#${anchor}`)
|
|
60
|
-
}
|
|
61
|
-
anchors[anchor] = schema
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
insertSchemaSubschemas (schema, rootSchemaId) {
|
|
65
|
-
const schemaId = schema.$id
|
|
66
|
-
if (schemaId !== undefined && typeof schemaId === 'string') {
|
|
67
|
-
if (schemaId.charAt(0) === '#') {
|
|
68
|
-
this.insertSchemaByAnchor(schema, rootSchemaId, schemaId)
|
|
69
|
-
} else {
|
|
70
|
-
this.insertSchemaBySchemaId(schema, schemaId)
|
|
71
|
-
rootSchemaId = schemaId
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const ref = schema.$ref
|
|
76
|
-
if (ref !== undefined && typeof ref === 'string') {
|
|
77
|
-
if (ref.charAt(0) !== '#') {
|
|
78
|
-
const dependencySchemaId = ref.split('#')[0]
|
|
79
|
-
this.schemas[rootSchemaId].dependencies.push(dependencySchemaId)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const key in schema) {
|
|
84
|
-
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
85
|
-
this.insertSchemaSubschemas(schema[key], rootSchemaId)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getDataByJSONPointer (data, jsonPointer) {
|
|
92
|
-
const parts = jsonPointer.split('/')
|
|
93
|
-
let current = data
|
|
94
|
-
for (const part of parts) {
|
|
95
|
-
if (part === '' || part === '#') continue
|
|
96
|
-
if (typeof current !== 'object' || current === null) {
|
|
97
|
-
return undefined
|
|
98
|
-
}
|
|
99
|
-
current = current[part]
|
|
100
|
-
}
|
|
101
|
-
return current
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
module.exports = RefResolver
|