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.
@@ -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: daily
6
+ interval: "monthly"
11
7
  open-pull-requests-limit: 10
12
- - package-ecosystem: npm
13
- directory: '/'
8
+
9
+ - package-ecosystem: "npm"
10
+ directory: "/"
14
11
  schedule:
15
- interval: daily
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
- ref: ${{github.event.pull_request.head.ref}}
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="#anyof">`AnyOf` and `OneOf`</a>
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 stringifyArray = FJS(arraySchema)
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 (var i = 0; i < 10000; i++) {
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 < 1000; i++) {
124
- multiArray.push(obj)
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
- stringifyArray(multiArray)
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
@@ -1,5 +1,5 @@
1
1
  const moment = require('moment')
2
- const fastJson = require('fast-json-stringify')
2
+ const fastJson = require('.')
3
3
  const stringify = fastJson({
4
4
  title: 'Example Schema',
5
5
  type: 'object',
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
- dependenciesName.toString = function () {
175
- return dependenciesName.join('\n')
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://json-schema.org/latest/json-schema-validation.html#rfc.section.6
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 = $validateWithAjv(${JSON.stringify(i)}, obj)
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'}($validateWithAjv(${JSON.stringify(location.schema)}, ${testValue}))
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'}($validateWithAjv(${JSON.stringify(location.schema)}, ${testValue}))
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.restore = function (debugModeStr, options = {}) {
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', debugModeStr])
1367
- .apply(null, args))
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.1",
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": "^15.0.0",
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.6.2",
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
  },
@@ -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
+ })
@@ -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(2)
21
+ t.plan(3)
21
22
  const debugMode = build({ debugMode: true })
22
- t.type(debugMode, Array)
23
- t.match(debugMode.toString.toString(), 'join', 'to string override')
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(2)
30
+ t.plan(3)
31
+
28
32
  const debugMode = build({ debugMode: 'yes' })
29
- t.type(debugMode, Array)
30
- t.match(debugMode.toString.toString(), 'join', 'to string override')
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(2)
40
+ t.plan(4)
35
41
  const debugMode = build({ debugMode: 1 })
36
- t.type(debugMode, Array)
37
42
 
38
- const str = debugMode.toString()
39
- const compiled = fjs.restore(str)
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(2)
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
- const str = debugMode.toString()
62
- const compiled = fjs.restore(str)
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
- const str = debugMode.toString()
96
+ t.type(debugMode, 'object')
86
97
 
87
- const compiled = fjs.restore(str)
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
+ })