fast-json-stringify 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +7 -10
- package/.github/workflows/benchmark.yml +6 -1
- package/README.md +59 -3
- package/bench.js +69 -7
- package/example.js +1 -1
- package/index.js +87 -58
- package/package.json +5 -3
- package/test/array.test.js +46 -2
- package/test/debug-mode.test.js +28 -17
- package/test/typebox.test.js +36 -0
package/.github/dependabot.yml
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
version: 2
|
|
2
2
|
updates:
|
|
3
|
-
- package-ecosystem: github-actions
|
|
4
|
-
directory:
|
|
5
|
-
ignore:
|
|
6
|
-
- dependency-name: "actions/*"
|
|
7
|
-
update-types:
|
|
8
|
-
["version-update:semver-minor", "version-update:semver-patch"]
|
|
3
|
+
- package-ecosystem: "github-actions"
|
|
4
|
+
directory: "/"
|
|
9
5
|
schedule:
|
|
10
|
-
interval:
|
|
6
|
+
interval: "monthly"
|
|
11
7
|
open-pull-requests-limit: 10
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
|
|
9
|
+
- package-ecosystem: "npm"
|
|
10
|
+
directory: "/"
|
|
14
11
|
schedule:
|
|
15
|
-
interval:
|
|
12
|
+
interval: "weekly"
|
|
16
13
|
open-pull-requests-limit: 10
|
|
@@ -8,6 +8,8 @@ jobs:
|
|
|
8
8
|
benchmark:
|
|
9
9
|
if: ${{ github.event.label.name == 'benchmark' }}
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
11
13
|
outputs:
|
|
12
14
|
PR-BENCH-12: ${{ steps.benchmark-pr.outputs.BENCH_RESULT12 }}
|
|
13
15
|
PR-BENCH-14: ${{ steps.benchmark-pr.outputs.BENCH_RESULT14 }}
|
|
@@ -21,7 +23,8 @@ jobs:
|
|
|
21
23
|
steps:
|
|
22
24
|
- uses: actions/checkout@v2
|
|
23
25
|
with:
|
|
24
|
-
|
|
26
|
+
persist-credentials: false
|
|
27
|
+
ref: ${{github.event.pull_request.head.sha}}
|
|
25
28
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
|
26
29
|
|
|
27
30
|
- uses: actions/setup-node@v2
|
|
@@ -64,6 +67,8 @@ jobs:
|
|
|
64
67
|
output-benchmark:
|
|
65
68
|
needs: [benchmark]
|
|
66
69
|
runs-on: ubuntu-latest
|
|
70
|
+
permissions:
|
|
71
|
+
pull-requests: write
|
|
67
72
|
steps:
|
|
68
73
|
- name: Comment PR
|
|
69
74
|
uses: thollander/actions-comment-pull-request@v1
|
package/README.md
CHANGED
|
@@ -56,11 +56,12 @@ compile-json-stringify date format x 1,086,187 ops/sec ±0.16% (99 runs sampled)
|
|
|
56
56
|
- <a href="#missingFields">`Missing fields`</a>
|
|
57
57
|
- <a href="#patternProperties">`Pattern Properties`</a>
|
|
58
58
|
- <a href="#additionalProperties">`Additional Properties`</a>
|
|
59
|
-
- <a href="#
|
|
59
|
+
- <a href="#AnyOf-and-OneOf">`AnyOf` and `OneOf`</a>
|
|
60
60
|
- <a href="#ref">`Reuse - $ref`</a>
|
|
61
61
|
- <a href="#long">`Long integers`</a>
|
|
62
62
|
- <a href="#integer">`Integers`</a>
|
|
63
63
|
- <a href="#nullable">`Nullable`</a>
|
|
64
|
+
- <a href="#largearrays">`Large Arrays`</a>
|
|
64
65
|
- <a href="#security">`Security Notice`</a>
|
|
65
66
|
- <a href="#acknowledgements">`Acknowledgements`</a>
|
|
66
67
|
- <a href="#license">`License`</a>
|
|
@@ -117,6 +118,8 @@ const stringify = fastJson(mySchema, {
|
|
|
117
118
|
- `schema`: external schemas references by $ref property. [More details](#ref)
|
|
118
119
|
- `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof)
|
|
119
120
|
- `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer)
|
|
121
|
+
- `largeArrayMechanism`: set the mechanism that should be used to handle large
|
|
122
|
+
(by default `20000` or more items) arrays. [More details](#largearrays)
|
|
120
123
|
|
|
121
124
|
|
|
122
125
|
<a name="api"></a>
|
|
@@ -268,7 +271,7 @@ const stringify = fastJson({
|
|
|
268
271
|
const obj = {
|
|
269
272
|
nickname: 'nick',
|
|
270
273
|
matchfoo: 42,
|
|
271
|
-
otherfoo: 'str'
|
|
274
|
+
otherfoo: 'str',
|
|
272
275
|
matchnum: 3
|
|
273
276
|
}
|
|
274
277
|
|
|
@@ -311,7 +314,7 @@ const stringify = fastJson({
|
|
|
311
314
|
const obj = {
|
|
312
315
|
nickname: 'nick',
|
|
313
316
|
matchfoo: 42,
|
|
314
|
-
otherfoo: 'str'
|
|
317
|
+
otherfoo: 'str',
|
|
315
318
|
matchnum: 3,
|
|
316
319
|
nomatchstr: 'valar morghulis',
|
|
317
320
|
nomatchint: 313
|
|
@@ -582,6 +585,59 @@ Otherwise, instead of raising an error, null values will be coerced as follows:
|
|
|
582
585
|
- `string` -> `""`
|
|
583
586
|
- `boolean` -> `false`
|
|
584
587
|
|
|
588
|
+
<a name="largearrays"></a>
|
|
589
|
+
#### Large Arrays
|
|
590
|
+
|
|
591
|
+
Large arrays are, for the scope of this document, defined as arrays containing,
|
|
592
|
+
by default, `20000` elements or more. That value can be adjusted via the option
|
|
593
|
+
parameter `largeArraySize`.
|
|
594
|
+
|
|
595
|
+
At some point the overhead caused by the default mechanism used by
|
|
596
|
+
`fast-json-stringify` to handle arrays starts increasing exponentially, leading
|
|
597
|
+
to slow overall executions.
|
|
598
|
+
|
|
599
|
+
##### Settings
|
|
600
|
+
|
|
601
|
+
In order to improve that the user can set the `largeArrayMechanism` and
|
|
602
|
+
`largeArraySize` options.
|
|
603
|
+
|
|
604
|
+
`largeArrayMechanism`'s default value is `default`. Valid values for it are:
|
|
605
|
+
|
|
606
|
+
- `default` - This option is a compromise between performance and feature set by
|
|
607
|
+
still providing the expected functionality out of this lib but giving up some
|
|
608
|
+
possible performance gain. With this option set, **large arrays** would be
|
|
609
|
+
stringified by joining their stringified elements using `Array.join` instead of
|
|
610
|
+
string concatenation for better performance
|
|
611
|
+
- `json-stringify` - This option will remove support for schema validation
|
|
612
|
+
within **large arrays** completely. By doing so the overhead previously
|
|
613
|
+
mentioned is nulled, greatly improving execution time. Mind there's no change
|
|
614
|
+
in behavior for arrays not considered _large_
|
|
615
|
+
|
|
616
|
+
`largeArraySize`'s default value is `20000`. Valid values for it are
|
|
617
|
+
integer-like values, such as:
|
|
618
|
+
|
|
619
|
+
- `20000`
|
|
620
|
+
- `2e4`
|
|
621
|
+
- `'20000'`
|
|
622
|
+
- `'2e4'` - _note this will be converted to `2`, not `20000`_
|
|
623
|
+
- `1.5` - _note this will be converted to `1`_
|
|
624
|
+
|
|
625
|
+
##### Benchmarks
|
|
626
|
+
|
|
627
|
+
For reference, here goes some benchmarks for comparison over the three
|
|
628
|
+
mechanisms. Benchmarks conducted on an old machine.
|
|
629
|
+
|
|
630
|
+
- Machine: `ST1000LM024 HN-M 1TB HDD, Intel Core i7-3610QM @ 2.3GHz, 12GB RAM, 4C/8T`.
|
|
631
|
+
- Node.js `v16.13.1`
|
|
632
|
+
|
|
633
|
+
```
|
|
634
|
+
JSON.stringify large array x 157 ops/sec ±0.73% (86 runs sampled)
|
|
635
|
+
fast-json-stringify large array default x 48.72 ops/sec ±4.92% (48 runs sampled)
|
|
636
|
+
fast-json-stringify large array json-stringify x 157 ops/sec ±0.76% (86 runs sampled)
|
|
637
|
+
compile-json-stringify large array x 175 ops/sec ±4.47% (79 runs sampled)
|
|
638
|
+
AJV Serialize large array x 58.76 ops/sec ±4.59% (60 runs sampled)
|
|
639
|
+
```
|
|
640
|
+
|
|
585
641
|
<a name="security"></a>
|
|
586
642
|
## Security notice
|
|
587
643
|
|
package/bench.js
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
const benchmark = require('benchmark')
|
|
4
4
|
const suite = new benchmark.Suite()
|
|
5
5
|
|
|
6
|
+
const STR_LEN = 1e4
|
|
7
|
+
const LARGE_ARRAY_SIZE = 2e4
|
|
8
|
+
const MULTI_ARRAY_LENGHT = 1e3
|
|
9
|
+
|
|
6
10
|
const schema = {
|
|
7
11
|
title: 'Example Schema',
|
|
8
12
|
type: 'object',
|
|
@@ -89,7 +93,8 @@ const obj = {
|
|
|
89
93
|
|
|
90
94
|
const date = new Date()
|
|
91
95
|
|
|
92
|
-
const multiArray =
|
|
96
|
+
const multiArray = new Array(MULTI_ARRAY_LENGHT)
|
|
97
|
+
const largeArray = new Array(LARGE_ARRAY_SIZE)
|
|
93
98
|
|
|
94
99
|
const CJS = require('compile-json-stringify')
|
|
95
100
|
const CJSStringify = CJS(schemaCJS)
|
|
@@ -99,7 +104,10 @@ const CJSStringifyString = CJS({ type: 'string' })
|
|
|
99
104
|
|
|
100
105
|
const FJS = require('.')
|
|
101
106
|
const stringify = FJS(schema)
|
|
102
|
-
const
|
|
107
|
+
const stringifyArrayDefault = FJS(arraySchema)
|
|
108
|
+
const stringifyArrayJSONStringify = FJS(arraySchema, {
|
|
109
|
+
largeArrayMechanism: 'json-stringify'
|
|
110
|
+
})
|
|
103
111
|
const stringifyDate = FJS(dateFormatSchema)
|
|
104
112
|
const stringifyString = FJS({ type: 'string' })
|
|
105
113
|
let str = ''
|
|
@@ -110,18 +118,48 @@ const ajvSerialize = ajv.compileSerializer(schemaAJVJTD)
|
|
|
110
118
|
const ajvSerializeArray = ajv.compileSerializer(arraySchemaAJVJTD)
|
|
111
119
|
const ajvSerializeString = ajv.compileSerializer({ type: 'string' })
|
|
112
120
|
|
|
121
|
+
const getRandomString = (length) => {
|
|
122
|
+
if (!Number.isInteger(length)) {
|
|
123
|
+
throw new Error('Expected integer length')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const validCharacters = 'abcdefghijklmnopqrstuvwxyz'
|
|
127
|
+
const nValidCharacters = 26
|
|
128
|
+
|
|
129
|
+
let result = ''
|
|
130
|
+
for (let i = 0; i < length; ++i) {
|
|
131
|
+
result += validCharacters[Math.floor(Math.random() * nValidCharacters)]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result[0].toUpperCase() + result.slice(1)
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
// eslint-disable-next-line
|
|
114
|
-
for (
|
|
138
|
+
for (let i = 0; i < STR_LEN; i++) {
|
|
139
|
+
largeArray[i] = {
|
|
140
|
+
firstName: getRandomString(8),
|
|
141
|
+
lastName: getRandomString(6),
|
|
142
|
+
age: Math.ceil(Math.random() * 99)
|
|
143
|
+
}
|
|
144
|
+
|
|
115
145
|
str += i
|
|
116
146
|
if (i % 100 === 0) {
|
|
117
147
|
str += '"'
|
|
118
148
|
}
|
|
119
149
|
}
|
|
120
150
|
|
|
151
|
+
for (let i = STR_LEN; i < LARGE_ARRAY_SIZE; ++i) {
|
|
152
|
+
largeArray[i] = {
|
|
153
|
+
firstName: getRandomString(10),
|
|
154
|
+
lastName: getRandomString(4),
|
|
155
|
+
age: Math.ceil(Math.random() * 99)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
121
159
|
Number(str)
|
|
122
160
|
|
|
123
|
-
for (i = 0; i <
|
|
124
|
-
multiArray
|
|
161
|
+
for (let i = 0; i < MULTI_ARRAY_LENGHT; i++) {
|
|
162
|
+
multiArray[i] = obj
|
|
125
163
|
}
|
|
126
164
|
|
|
127
165
|
suite.add('FJS creation', function () {
|
|
@@ -138,8 +176,12 @@ suite.add('JSON.stringify array', function () {
|
|
|
138
176
|
JSON.stringify(multiArray)
|
|
139
177
|
})
|
|
140
178
|
|
|
141
|
-
suite.add('fast-json-stringify array', function () {
|
|
142
|
-
|
|
179
|
+
suite.add('fast-json-stringify array default', function () {
|
|
180
|
+
stringifyArrayDefault(multiArray)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
suite.add('fast-json-stringify array json-stringify', function () {
|
|
184
|
+
stringifyArrayJSONStringify(multiArray)
|
|
143
185
|
})
|
|
144
186
|
|
|
145
187
|
suite.add('compile-json-stringify array', function () {
|
|
@@ -150,6 +192,26 @@ suite.add('AJV Serialize array', function () {
|
|
|
150
192
|
ajvSerializeArray(multiArray)
|
|
151
193
|
})
|
|
152
194
|
|
|
195
|
+
suite.add('JSON.stringify large array', function () {
|
|
196
|
+
JSON.stringify(largeArray)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
suite.add('fast-json-stringify large array default', function () {
|
|
200
|
+
stringifyArrayDefault(largeArray)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
suite.add('fast-json-stringify large array json-stringify', function () {
|
|
204
|
+
stringifyArrayJSONStringify(largeArray)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
suite.add('compile-json-stringify large array', function () {
|
|
208
|
+
CJSStringifyArray(largeArray)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
suite.add('AJV Serialize large array', function () {
|
|
212
|
+
ajvSerializeArray(largeArray)
|
|
213
|
+
})
|
|
214
|
+
|
|
153
215
|
suite.add('JSON.stringify long string', function () {
|
|
154
216
|
JSON.stringify(str)
|
|
155
217
|
})
|
package/example.js
CHANGED
package/index.js
CHANGED
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
/* eslint no-prototype-builtins: 0 */
|
|
4
4
|
|
|
5
5
|
const Ajv = require('ajv')
|
|
6
|
+
const fastUri = require('fast-uri')
|
|
6
7
|
const ajvFormats = require('ajv-formats')
|
|
7
8
|
const merge = require('deepmerge')
|
|
8
9
|
const clone = require('rfdc')({ proto: true })
|
|
9
10
|
const fjsCloned = Symbol('fast-json-stringify.cloned')
|
|
11
|
+
const { randomUUID } = require('crypto')
|
|
10
12
|
|
|
11
13
|
const validate = require('./schema-validator')
|
|
14
|
+
|
|
15
|
+
let largeArraySize = 2e4
|
|
12
16
|
let stringSimilarity = null
|
|
17
|
+
let largeArrayMechanism = 'default'
|
|
18
|
+
const validLargeArrayMechanisms = [
|
|
19
|
+
'default',
|
|
20
|
+
'json-stringify'
|
|
21
|
+
]
|
|
13
22
|
|
|
14
23
|
const addComma = `
|
|
15
24
|
if (addComma) {
|
|
@@ -43,11 +52,17 @@ function mergeLocation (source, dest) {
|
|
|
43
52
|
|
|
44
53
|
const arrayItemsReferenceSerializersMap = new Map()
|
|
45
54
|
const objectReferenceSerializersMap = new Map()
|
|
55
|
+
let ajvInstance = null
|
|
46
56
|
|
|
47
57
|
function build (schema, options) {
|
|
48
58
|
arrayItemsReferenceSerializersMap.clear()
|
|
49
59
|
objectReferenceSerializersMap.clear()
|
|
60
|
+
|
|
50
61
|
options = options || {}
|
|
62
|
+
|
|
63
|
+
ajvInstance = new Ajv({ ...options.ajv, strictSchema: false, uriResolver: fastUri })
|
|
64
|
+
ajvFormats(ajvInstance)
|
|
65
|
+
|
|
51
66
|
isValidSchema(schema)
|
|
52
67
|
if (options.schema) {
|
|
53
68
|
// eslint-disable-next-line
|
|
@@ -65,6 +80,22 @@ function build (schema, options) {
|
|
|
65
80
|
}
|
|
66
81
|
}
|
|
67
82
|
|
|
83
|
+
if (options.largeArrayMechanism) {
|
|
84
|
+
if (validLargeArrayMechanisms.includes(options.largeArrayMechanism)) {
|
|
85
|
+
largeArrayMechanism = options.largeArrayMechanism
|
|
86
|
+
} else {
|
|
87
|
+
throw new Error(`Unsupported large array mechanism ${options.rounding}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (options.largeArraySize) {
|
|
92
|
+
if (!Number.isNaN(Number.parseInt(options.largeArraySize, 10))) {
|
|
93
|
+
largeArraySize = options.largeArraySize
|
|
94
|
+
} else {
|
|
95
|
+
throw new Error(`Unsupported large array size. Expected integer-like, got ${options.largeArraySize}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
68
99
|
/* eslint no-new-func: "off" */
|
|
69
100
|
let code = `
|
|
70
101
|
'use strict'
|
|
@@ -72,41 +103,6 @@ function build (schema, options) {
|
|
|
72
103
|
|
|
73
104
|
code += `
|
|
74
105
|
${asFunctions}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Used by schemas that are dependant on calling 'ajv.validate' during runtime,
|
|
79
|
-
* it stores the value of the '$id' property of the schema (if it has it) inside
|
|
80
|
-
* a cache which is used to figure out if the schema was compiled into a validator
|
|
81
|
-
* by ajv on a previous call, if it was then the '$id' string will be used to
|
|
82
|
-
* invoke 'ajv.validate', this allows:
|
|
83
|
-
*
|
|
84
|
-
* 1. Schemas that depend on ajv.validate calls to leverage ajv caching system.
|
|
85
|
-
* 2. To avoid errors, since directly invoking 'ajv.validate' with the same
|
|
86
|
-
* schema (that contains an '$id' property) twice will throw an error.
|
|
87
|
-
*/
|
|
88
|
-
const $validateWithAjv = (function() {
|
|
89
|
-
const cache = new Set()
|
|
90
|
-
|
|
91
|
-
return function (schema, target) {
|
|
92
|
-
const id = schema.$id
|
|
93
|
-
|
|
94
|
-
if (!id) {
|
|
95
|
-
return ajv.validate(schema, target)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const cached = cache.has(id)
|
|
99
|
-
|
|
100
|
-
if (cached) {
|
|
101
|
-
return ajv.validate(id, target)
|
|
102
|
-
} else {
|
|
103
|
-
cache.add(id)
|
|
104
|
-
return ajv.validate(schema, target)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
})()
|
|
108
|
-
|
|
109
|
-
|
|
110
106
|
function parseInteger(int) { return Math.${intParseFunctionName}(int) }
|
|
111
107
|
`
|
|
112
108
|
|
|
@@ -163,18 +159,17 @@ function build (schema, options) {
|
|
|
163
159
|
;
|
|
164
160
|
return ${main}
|
|
165
161
|
`
|
|
166
|
-
|
|
167
|
-
const ajvInstance = new Ajv(options.ajv)
|
|
168
|
-
ajvFormats(ajvInstance)
|
|
169
162
|
const dependencies = [ajvInstance]
|
|
170
163
|
const dependenciesName = ['ajv']
|
|
164
|
+
ajvInstance = null
|
|
165
|
+
|
|
171
166
|
dependenciesName.push(code)
|
|
172
167
|
|
|
173
168
|
if (options.debugMode) {
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
return {
|
|
170
|
+
code: dependenciesName.join('\n'),
|
|
171
|
+
ajv: dependencies[0]
|
|
176
172
|
}
|
|
177
|
-
return dependenciesName
|
|
178
173
|
}
|
|
179
174
|
|
|
180
175
|
arrayItemsReferenceSerializersMap.clear()
|
|
@@ -218,7 +213,7 @@ const numberKeywords = [
|
|
|
218
213
|
|
|
219
214
|
/**
|
|
220
215
|
* Infer type based on keyword in order to generate optimized code
|
|
221
|
-
* https://
|
|
216
|
+
* https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6
|
|
222
217
|
*/
|
|
223
218
|
function inferTypeByKeyword (schema) {
|
|
224
219
|
// eslint-disable-next-line
|
|
@@ -874,8 +869,11 @@ function addIfThenElse (location, name) {
|
|
|
874
869
|
let merged = merge(copy, then)
|
|
875
870
|
let mergedLocation = mergeLocation(location, { schema: merged })
|
|
876
871
|
|
|
872
|
+
const schemaKey = i.$id || randomUUID()
|
|
873
|
+
ajvInstance.addSchema(i, schemaKey)
|
|
874
|
+
|
|
877
875
|
code += `
|
|
878
|
-
valid =
|
|
876
|
+
valid = ajv.validate("${schemaKey}", obj)
|
|
879
877
|
if (valid) {
|
|
880
878
|
`
|
|
881
879
|
if (merged.if && merged.then) {
|
|
@@ -935,7 +933,7 @@ function buildObject (location, code, name) {
|
|
|
935
933
|
`
|
|
936
934
|
}
|
|
937
935
|
|
|
938
|
-
if (objectReferenceSerializersMap.has(schema)) {
|
|
936
|
+
if (objectReferenceSerializersMap.has(schema) && objectReferenceSerializersMap.get(schema) !== name) {
|
|
939
937
|
code += `
|
|
940
938
|
return ${objectReferenceSerializersMap.get(schema)}(input)
|
|
941
939
|
}
|
|
@@ -1054,6 +1052,11 @@ function buildArray (location, code, name, key = null) {
|
|
|
1054
1052
|
|
|
1055
1053
|
code += `
|
|
1056
1054
|
var l = obj.length
|
|
1055
|
+
if (l && l >= ${largeArraySize}) {`
|
|
1056
|
+
|
|
1057
|
+
const concatSnippet = `
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1057
1060
|
var jsonOutput= ''
|
|
1058
1061
|
for (var i = 0; i < l; i++) {
|
|
1059
1062
|
var json = ''
|
|
@@ -1065,7 +1068,25 @@ function buildArray (location, code, name, key = null) {
|
|
|
1065
1068
|
}
|
|
1066
1069
|
}
|
|
1067
1070
|
return \`[\${jsonOutput}]\`
|
|
1071
|
+
}`
|
|
1072
|
+
|
|
1073
|
+
switch (largeArrayMechanism) {
|
|
1074
|
+
case 'default':
|
|
1075
|
+
code += `
|
|
1076
|
+
return \`[\${obj.map(${result.mapFnName}).join(',')}]\``
|
|
1077
|
+
break
|
|
1078
|
+
|
|
1079
|
+
case 'json-stringify':
|
|
1080
|
+
code += `
|
|
1081
|
+
return JSON.stringify(obj)`
|
|
1082
|
+
break
|
|
1083
|
+
|
|
1084
|
+
default:
|
|
1085
|
+
throw new Error(`Unsupported large array mechanism ${largeArrayMechanism}`)
|
|
1068
1086
|
}
|
|
1087
|
+
|
|
1088
|
+
code += `
|
|
1089
|
+
${concatSnippet}
|
|
1069
1090
|
${result.laterCode}
|
|
1070
1091
|
`
|
|
1071
1092
|
|
|
@@ -1173,22 +1194,27 @@ function nested (laterCode, name, key, location, subKey, isArray) {
|
|
|
1173
1194
|
|
|
1174
1195
|
switch (type) {
|
|
1175
1196
|
case 'null':
|
|
1197
|
+
funcName = '$asNull'
|
|
1176
1198
|
code += `
|
|
1177
1199
|
json += $asNull()
|
|
1178
1200
|
`
|
|
1179
1201
|
break
|
|
1180
1202
|
case 'string': {
|
|
1203
|
+
funcName = '$asString'
|
|
1181
1204
|
const stringSerializer = getStringSerializer(schema.format)
|
|
1182
1205
|
code += nullable ? `json += obj${accessor} === null ? null : ${stringSerializer}(obj${accessor})` : `json += ${stringSerializer}(obj${accessor})`
|
|
1183
1206
|
break
|
|
1184
1207
|
}
|
|
1185
1208
|
case 'integer':
|
|
1209
|
+
funcName = '$asInteger'
|
|
1186
1210
|
code += nullable ? `json += obj${accessor} === null ? null : $asInteger(obj${accessor})` : `json += $asInteger(obj${accessor})`
|
|
1187
1211
|
break
|
|
1188
1212
|
case 'number':
|
|
1213
|
+
funcName = '$asNumber'
|
|
1189
1214
|
code += nullable ? `json += obj${accessor} === null ? null : $asNumber(obj${accessor})` : `json += $asNumber(obj${accessor})`
|
|
1190
1215
|
break
|
|
1191
1216
|
case 'boolean':
|
|
1217
|
+
funcName = '$asBoolean'
|
|
1192
1218
|
code += nullable ? `json += obj${accessor} === null ? null : $asBoolean(obj${accessor})` : `json += $asBoolean(obj${accessor})`
|
|
1193
1219
|
break
|
|
1194
1220
|
case 'object':
|
|
@@ -1206,6 +1232,7 @@ function nested (laterCode, name, key, location, subKey, isArray) {
|
|
|
1206
1232
|
`
|
|
1207
1233
|
break
|
|
1208
1234
|
case undefined:
|
|
1235
|
+
funcName = '$asNull'
|
|
1209
1236
|
if ('anyOf' in schema) {
|
|
1210
1237
|
// beware: dereferenceOfRefs has side effects and changes schema.anyOf
|
|
1211
1238
|
const anyOfLocations = dereferenceOfRefs(location, 'anyOf')
|
|
@@ -1224,8 +1251,12 @@ function nested (laterCode, name, key, location, subKey, isArray) {
|
|
|
1224
1251
|
// with the actual schema
|
|
1225
1252
|
// 2. `nested`, through `buildCode`, replaces any reference in object properties with the actual schema
|
|
1226
1253
|
// (see https://github.com/fastify/fast-json-stringify/blob/6da3b3e8ac24b1ca5578223adedb4083b7adf8db/index.js#L631)
|
|
1254
|
+
|
|
1255
|
+
const schemaKey = location.schema.$id || randomUUID()
|
|
1256
|
+
ajvInstance.addSchema(location.schema, schemaKey)
|
|
1257
|
+
|
|
1227
1258
|
code += `
|
|
1228
|
-
${index === 0 ? 'if' : 'else if'}(
|
|
1259
|
+
${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${testValue}))
|
|
1229
1260
|
${nestedResult.code}
|
|
1230
1261
|
`
|
|
1231
1262
|
laterCode = nestedResult.laterCode
|
|
@@ -1241,8 +1272,12 @@ function nested (laterCode, name, key, location, subKey, isArray) {
|
|
|
1241
1272
|
const testSerializer = getTestSerializer(location.schema.format)
|
|
1242
1273
|
const testValue = testSerializer !== undefined ? `${testSerializer}(obj${accessor}, true)` : `obj${accessor}`
|
|
1243
1274
|
// see comment on anyOf about dereferencing the schema before calling ajv.validate
|
|
1275
|
+
|
|
1276
|
+
const schemaKey = location.schema.$id || randomUUID()
|
|
1277
|
+
ajvInstance.addSchema(location.schema, schemaKey)
|
|
1278
|
+
|
|
1244
1279
|
code += `
|
|
1245
|
-
${index === 0 ? 'if' : 'else if'}(
|
|
1280
|
+
${index === 0 ? 'if' : 'else if'}(ajv.validate("${schemaKey}", ${testValue}))
|
|
1246
1281
|
${nestedResult.code}
|
|
1247
1282
|
`
|
|
1248
1283
|
laterCode = nestedResult.laterCode
|
|
@@ -1336,7 +1371,8 @@ function nested (laterCode, name, key, location, subKey, isArray) {
|
|
|
1336
1371
|
|
|
1337
1372
|
return {
|
|
1338
1373
|
code,
|
|
1339
|
-
laterCode
|
|
1374
|
+
laterCode,
|
|
1375
|
+
mapFnName: funcName
|
|
1340
1376
|
}
|
|
1341
1377
|
}
|
|
1342
1378
|
|
|
@@ -1352,17 +1388,10 @@ function isEmpty (schema) {
|
|
|
1352
1388
|
|
|
1353
1389
|
module.exports = build
|
|
1354
1390
|
|
|
1355
|
-
module.exports.
|
|
1356
|
-
const dependencies = [debugModeStr]
|
|
1357
|
-
const args = []
|
|
1358
|
-
if (debugModeStr.startsWith('ajv')) {
|
|
1359
|
-
dependencies.unshift('ajv')
|
|
1360
|
-
const ajvInstance = new Ajv(options.ajv)
|
|
1361
|
-
ajvFormats(ajvInstance)
|
|
1362
|
-
args.push(ajvInstance)
|
|
1363
|
-
}
|
|
1391
|
+
module.exports.validLargeArrayMechanisms = validLargeArrayMechanisms
|
|
1364
1392
|
|
|
1393
|
+
module.exports.restore = function ({ code, ajv }) {
|
|
1365
1394
|
// eslint-disable-next-line
|
|
1366
|
-
return (Function.apply(null, ['ajv',
|
|
1367
|
-
.apply(null,
|
|
1395
|
+
return (Function.apply(null, ['ajv', code])
|
|
1396
|
+
.apply(null, [ajv]))
|
|
1368
1397
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-json-stringify",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Stringify your JSON at max speed",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://github.com/fastify/fast-json-stringify#readme",
|
|
32
32
|
"devDependencies": {
|
|
33
|
+
"@sinclair/typebox": "^0.23.3",
|
|
33
34
|
"benchmark": "^2.1.4",
|
|
34
35
|
"compile-json-stringify": "^0.1.2",
|
|
35
36
|
"is-my-json-valid": "^2.20.0",
|
|
@@ -38,14 +39,15 @@
|
|
|
38
39
|
"proxyquire": "^2.1.3",
|
|
39
40
|
"semver": "^7.1.0",
|
|
40
41
|
"standard": "^16.0.1",
|
|
41
|
-
"tap": "^
|
|
42
|
+
"tap": "^16.0.1",
|
|
42
43
|
"typescript": "^4.0.2",
|
|
43
44
|
"webpack": "^5.40.0"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"ajv": "^8.
|
|
47
|
+
"ajv": "^8.10.0",
|
|
47
48
|
"ajv-formats": "^2.1.1",
|
|
48
49
|
"deepmerge": "^4.2.2",
|
|
50
|
+
"fast-uri": "^1.0.1",
|
|
49
51
|
"rfdc": "^1.2.0",
|
|
50
52
|
"string-similarity": "^4.0.1"
|
|
51
53
|
},
|
package/test/array.test.js
CHANGED
|
@@ -5,12 +5,12 @@ const test = require('tap').test
|
|
|
5
5
|
const validator = require('is-my-json-valid')
|
|
6
6
|
const build = require('..')
|
|
7
7
|
|
|
8
|
-
function buildTest (schema, toStringify) {
|
|
8
|
+
function buildTest (schema, toStringify, options) {
|
|
9
9
|
test(`render a ${schema.title} as JSON`, (t) => {
|
|
10
10
|
t.plan(3)
|
|
11
11
|
|
|
12
12
|
const validate = validator(schema)
|
|
13
|
-
const stringify = build(schema)
|
|
13
|
+
const stringify = build(schema, options)
|
|
14
14
|
const output = stringify(toStringify)
|
|
15
15
|
|
|
16
16
|
t.same(JSON.parse(output), toStringify)
|
|
@@ -319,3 +319,47 @@ test('object array with anyOf and symbol', (t) => {
|
|
|
319
319
|
])
|
|
320
320
|
t.equal(value, '[{"name":"name-0","option":"Foo"},{"name":"name-1","option":"Bar"}]')
|
|
321
321
|
})
|
|
322
|
+
|
|
323
|
+
const largeArray = new Array(2e4).fill({ a: 'test', b: 1 })
|
|
324
|
+
buildTest({
|
|
325
|
+
title: 'large array with default mechanism',
|
|
326
|
+
type: 'object',
|
|
327
|
+
properties: {
|
|
328
|
+
ids: {
|
|
329
|
+
type: 'array',
|
|
330
|
+
items: {
|
|
331
|
+
type: 'object',
|
|
332
|
+
properties: {
|
|
333
|
+
a: { type: 'string' },
|
|
334
|
+
b: { type: 'number' }
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}, {
|
|
340
|
+
ids: largeArray
|
|
341
|
+
}, {
|
|
342
|
+
largeArraySize: 2e4,
|
|
343
|
+
largeArrayMechanism: 'default'
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
buildTest({
|
|
347
|
+
title: 'large array with json-stringify mechanism',
|
|
348
|
+
type: 'object',
|
|
349
|
+
properties: {
|
|
350
|
+
ids: {
|
|
351
|
+
type: 'array',
|
|
352
|
+
items: {
|
|
353
|
+
type: 'object',
|
|
354
|
+
properties: {
|
|
355
|
+
a: { type: 'string' },
|
|
356
|
+
b: { type: 'number' }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}, {
|
|
362
|
+
ids: largeArray
|
|
363
|
+
}, {
|
|
364
|
+
largeArrayMechanism: 'json-stringify'
|
|
365
|
+
})
|
package/test/debug-mode.test.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const test = require('tap').test
|
|
4
4
|
const fjs = require('..')
|
|
5
|
+
const Ajv = require('ajv').default
|
|
5
6
|
|
|
6
7
|
function build (opts) {
|
|
7
8
|
return fjs({
|
|
@@ -17,32 +18,40 @@ function build (opts) {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
test('activate debug mode', t => {
|
|
20
|
-
t.plan(
|
|
21
|
+
t.plan(3)
|
|
21
22
|
const debugMode = build({ debugMode: true })
|
|
22
|
-
|
|
23
|
-
t.
|
|
23
|
+
|
|
24
|
+
t.type(debugMode, 'object')
|
|
25
|
+
t.ok(debugMode.ajv instanceof Ajv)
|
|
26
|
+
t.type(debugMode.code, 'string')
|
|
24
27
|
})
|
|
25
28
|
|
|
26
29
|
test('activate debug mode truthy', t => {
|
|
27
|
-
t.plan(
|
|
30
|
+
t.plan(3)
|
|
31
|
+
|
|
28
32
|
const debugMode = build({ debugMode: 'yes' })
|
|
29
|
-
|
|
30
|
-
t.
|
|
33
|
+
|
|
34
|
+
t.type(debugMode, 'object')
|
|
35
|
+
t.type(debugMode.code, 'string')
|
|
36
|
+
t.ok(debugMode.ajv instanceof Ajv)
|
|
31
37
|
})
|
|
32
38
|
|
|
33
39
|
test('to string auto-consistent', t => {
|
|
34
|
-
t.plan(
|
|
40
|
+
t.plan(4)
|
|
35
41
|
const debugMode = build({ debugMode: 1 })
|
|
36
|
-
t.type(debugMode, Array)
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
t.type(debugMode, 'object')
|
|
44
|
+
t.type(debugMode.code, 'string')
|
|
45
|
+
t.ok(debugMode.ajv instanceof Ajv)
|
|
46
|
+
|
|
47
|
+
const compiled = fjs.restore(debugMode)
|
|
40
48
|
const tobe = JSON.stringify({ firstName: 'Foo' })
|
|
41
49
|
t.same(compiled({ firstName: 'Foo', surname: 'bar' }), tobe, 'surname evicted')
|
|
42
50
|
})
|
|
43
51
|
|
|
44
52
|
test('to string auto-consistent with ajv', t => {
|
|
45
|
-
t.plan(
|
|
53
|
+
t.plan(4)
|
|
54
|
+
|
|
46
55
|
const debugMode = fjs({
|
|
47
56
|
title: 'object with multiple types field',
|
|
48
57
|
type: 'object',
|
|
@@ -56,16 +65,19 @@ test('to string auto-consistent with ajv', t => {
|
|
|
56
65
|
}
|
|
57
66
|
}
|
|
58
67
|
}, { debugMode: 1 })
|
|
59
|
-
t.type(debugMode, Array)
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
t.type(debugMode, 'object')
|
|
70
|
+
t.type(debugMode.code, 'string')
|
|
71
|
+
t.ok(debugMode.ajv instanceof Ajv)
|
|
72
|
+
|
|
73
|
+
const compiled = fjs.restore(debugMode)
|
|
63
74
|
const tobe = JSON.stringify({ str: 'Foo' })
|
|
64
75
|
t.same(compiled({ str: 'Foo', void: 'me' }), tobe)
|
|
65
76
|
})
|
|
66
77
|
|
|
67
78
|
test('to string auto-consistent with ajv-formats', t => {
|
|
68
79
|
t.plan(3)
|
|
80
|
+
|
|
69
81
|
const debugMode = fjs({
|
|
70
82
|
title: 'object with multiple types field and format keyword',
|
|
71
83
|
type: 'object',
|
|
@@ -80,11 +92,10 @@ test('to string auto-consistent with ajv-formats', t => {
|
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
94
|
}, { debugMode: 1 })
|
|
83
|
-
t.type(debugMode, Array)
|
|
84
95
|
|
|
85
|
-
|
|
96
|
+
t.type(debugMode, 'object')
|
|
86
97
|
|
|
87
|
-
const compiled = fjs.restore(
|
|
98
|
+
const compiled = fjs.restore(debugMode)
|
|
88
99
|
const tobe = JSON.stringify({ str: 'foo@bar.com' })
|
|
89
100
|
t.same(compiled({ str: 'foo@bar.com' }), tobe)
|
|
90
101
|
t.same(compiled({ str: 'foo' }), JSON.stringify({ str: null }), 'invalid format is ignored')
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const test = require('tap').test
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
test('nested object in pattern properties for typebox', (t) => {
|
|
7
|
+
const { Type } = require('@sinclair/typebox')
|
|
8
|
+
|
|
9
|
+
t.plan(1)
|
|
10
|
+
|
|
11
|
+
const nestedSchema = Type.Object({
|
|
12
|
+
nestedKey1: Type.String()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const RootSchema = Type.Object({
|
|
16
|
+
key1: Type.Record(Type.String(), nestedSchema),
|
|
17
|
+
key2: Type.Record(Type.String(), nestedSchema)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const schema = RootSchema
|
|
21
|
+
const stringify = build(schema)
|
|
22
|
+
|
|
23
|
+
const value = stringify({
|
|
24
|
+
key1: {
|
|
25
|
+
nestedKey: {
|
|
26
|
+
nestedKey1: 'value1'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
key2: {
|
|
30
|
+
nestedKey: {
|
|
31
|
+
nestedKey1: 'value2'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
t.equal(value, '{"key1":{"nestedKey":{"nestedKey1":"value1"}},"key2":{"nestedKey":{"nestedKey1":"value2"}}}')
|
|
36
|
+
})
|