fast-json-stringify 7.0.0 → 7.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.
Files changed (39) hide show
  1. package/.github/dependabot.yml +43 -2
  2. package/.github/workflows/ci.yml +1 -1
  3. package/.github/workflows/lock-threads.yml +1 -1
  4. package/.github/workflows/release.yml +24 -0
  5. package/README.md +25 -8
  6. package/benchmark/bench-thread.js +6 -2
  7. package/benchmark/bench.js +86 -0
  8. package/index.js +360 -94
  9. package/lib/location.js +3 -1
  10. package/lib/serializer.js +41 -37
  11. package/lib/standalone.js +22 -3
  12. package/lib/validator.js +22 -3
  13. package/package.json +6 -7
  14. package/test/additionalProperties.test.js +87 -0
  15. package/test/anyof.test.js +42 -0
  16. package/test/array.test.js +1 -1
  17. package/test/basic.test.js +1 -1
  18. package/test/code-generation-fallbacks.test.js +158 -0
  19. package/test/compile-validators.test.js +110 -0
  20. package/test/const.test.js +27 -6
  21. package/test/date.test.js +28 -1
  22. package/test/debug-mode.test.js +8 -0
  23. package/test/deopt.test.js +251 -0
  24. package/test/if-then-else.test.js +145 -0
  25. package/test/inferType.test.js +1 -1
  26. package/test/integer.test.js +1 -1
  27. package/test/oneof.test.js +242 -0
  28. package/test/optional-property-prefix.test.js +119 -0
  29. package/test/patternProperties.test.js +24 -0
  30. package/test/property-names.test.js +41 -0
  31. package/test/regex.test.js +1 -1
  32. package/test/schema-depth.test.js +227 -0
  33. package/test/standalone-mode.test.js +109 -2
  34. package/test/string.test.js +8 -0
  35. package/test/surrogate.test.js +1 -1
  36. package/test/typesArray.test.js +59 -0
  37. package/test/utils/validator.js +20 -0
  38. package/types/index.d.ts +20 -0
  39. package/types/index.tst.ts +13 -0
@@ -39,13 +39,13 @@ test('activate standalone mode', async (t) => {
39
39
  })
40
40
 
41
41
  test('test ajv schema', async (t) => {
42
- t.plan(3)
42
+ t.plan(4)
43
43
 
44
44
  after(async () => {
45
45
  await fs.promises.rm(destination, { force: true })
46
46
  })
47
47
 
48
- const code = build({ mode: 'standalone' }, {
48
+ const code = build({ mode: 'standalone', inlineValidators: false }, {
49
49
  type: 'object',
50
50
  properties: {
51
51
  },
@@ -94,6 +94,7 @@ test('test ajv schema', async (t) => {
94
94
  })
95
95
  t.assert.ok(typeof code === 'string')
96
96
  t.assert.equal(code.indexOf('ajv') > 0, true)
97
+ t.assert.match(code, /fast-json-stringify\/lib\/validator/)
97
98
 
98
99
  const destination = path.resolve(tmpDir, 'standalone2.js')
99
100
 
@@ -217,3 +218,109 @@ test('no need to keep external schemas once compiled - with oneOf validator', as
217
218
  t.assert.equal(stringify({ oneOfSchema: { baz: 5 } }), '{"oneOfSchema":{"baz":5}}')
218
219
  t.assert.equal(stringify({ oneOfSchema: { bar: 'foo' } }), '{"oneOfSchema":{"bar":"foo"}}')
219
220
  })
221
+
222
+ test('inline validators with if/then/else', async (t) => {
223
+ t.plan(6)
224
+
225
+ after(async () => {
226
+ await fs.promises.rm(destination, { force: true })
227
+ })
228
+
229
+ const ajvOptions = {
230
+ code: { source: false, esm: true, lines: true }
231
+ }
232
+ const code = fjs({
233
+ type: 'object',
234
+ properties: {},
235
+ if: {
236
+ type: 'object',
237
+ properties: {
238
+ kind: { const: 'foo' }
239
+ },
240
+ required: ['kind']
241
+ },
242
+ then: {
243
+ properties: {
244
+ kind: { type: 'string' },
245
+ foo: { type: 'string' }
246
+ }
247
+ },
248
+ else: {
249
+ properties: {
250
+ kind: { type: 'string' },
251
+ bar: { type: 'integer' }
252
+ }
253
+ }
254
+ }, {
255
+ mode: 'standalone',
256
+ inlineValidators: true,
257
+ ajv: ajvOptions
258
+ })
259
+
260
+ t.assert.ok(typeof code === 'string')
261
+ t.assert.doesNotMatch(code, /fast-json-stringify\/lib\/validator/)
262
+ t.assert.match(code, /function validate\d/)
263
+ t.assert.deepEqual(ajvOptions, {
264
+ code: { source: false, esm: true, lines: true }
265
+ })
266
+
267
+ const destination = path.resolve(tmpDir, 'standalone-inline-if.js')
268
+
269
+ await fs.promises.writeFile(destination, code)
270
+ const stringify = require(destination)
271
+
272
+ t.assert.equal(stringify({ kind: 'foo', foo: 'FOO', bar: 42 }), '{"kind":"foo","foo":"FOO"}')
273
+ t.assert.equal(stringify({ kind: 'bar', foo: 'FOO', bar: 42 }), '{"kind":"bar","bar":42}')
274
+ })
275
+
276
+ test('inline validators with external oneOf refs and toJSON values', async (t) => {
277
+ t.plan(4)
278
+
279
+ after(async () => {
280
+ await fs.promises.rm(destination, { force: true })
281
+ })
282
+
283
+ const code = fjs({
284
+ type: 'object',
285
+ properties: {
286
+ value: {
287
+ oneOf: [
288
+ { $ref: 'values#/definitions/timestamp' },
289
+ { $ref: 'values#/definitions/count' }
290
+ ]
291
+ }
292
+ }
293
+ }, {
294
+ mode: 'standalone',
295
+ inlineValidators: true,
296
+ schema: {
297
+ values: {
298
+ definitions: {
299
+ timestamp: { type: 'string', format: 'date-time' },
300
+ count: { type: 'integer' }
301
+ }
302
+ }
303
+ }
304
+ })
305
+
306
+ t.assert.doesNotMatch(code, /fast-json-stringify\/lib\/validator/)
307
+
308
+ const destination = path.resolve(tmpDir, 'standalone-inline-oneOf.js')
309
+
310
+ await fs.promises.writeFile(destination, code)
311
+ const stringify = require(destination)
312
+
313
+ t.assert.equal(
314
+ stringify({ value: new Date('2020-01-02T03:04:05.000Z') }),
315
+ '{"value":"2020-01-02T03:04:05.000Z"}'
316
+ )
317
+ t.assert.equal(stringify({ value: 42 }), '{"value":42}')
318
+ t.assert.throws(() => stringify({ value: true }), /does not match schema definition/)
319
+ })
320
+
321
+ test('does not emit unused inline validators', (t) => {
322
+ const code = build({ mode: 'standalone', inlineValidators: true })
323
+
324
+ t.assert.doesNotMatch(code, /function validate\d/)
325
+ t.assert.match(code, /const validator = null/)
326
+ })
@@ -4,6 +4,14 @@ const { test } = require('node:test')
4
4
 
5
5
  const build = require('..')
6
6
 
7
+ test('serialize an empty string', (t) => {
8
+ t.plan(1)
9
+
10
+ const stringify = build({ type: 'string' })
11
+
12
+ t.assert.equal(stringify(''), '""')
13
+ })
14
+
7
15
  test('serialize short string', (t) => {
8
16
  t.plan(2)
9
17
 
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { test } = require('node:test')
4
- const validator = require('is-my-json-valid')
4
+ const validator = require('./utils/validator')
5
5
  const build = require('..')
6
6
 
7
7
  test('render a string with surrogate pairs as JSON:test 1', (t) => {
@@ -548,3 +548,62 @@ test('throw an error if none of types matches', (t) => {
548
548
  const stringify = build(schema)
549
549
  t.assert.throws(() => stringify({ data: 'string' }), 'The value "string" does not match schema definition.')
550
550
  })
551
+
552
+ test('multi-type object listed before array serializes array input as an array', (t) => {
553
+ t.plan(2)
554
+
555
+ const schema = {
556
+ type: ['object', 'array'],
557
+ properties: {
558
+ a: { type: 'integer' }
559
+ },
560
+ items: { type: 'integer' }
561
+ }
562
+
563
+ const stringify = build(schema, { ajv: { allowUnionTypes: true } })
564
+
565
+ // Array input must match the `array` member, not be captured by `object`.
566
+ t.assert.equal(stringify([1, 2, 3]), '[1,2,3]')
567
+
568
+ // Object input for the same schema still serializes as an object.
569
+ t.assert.equal(stringify({ a: 4 }), '{"a":4}')
570
+ })
571
+
572
+ test('multi-type [object, array] round-trips an array of objects (JSON:API data)', (t) => {
573
+ t.plan(2)
574
+
575
+ const schema = {
576
+ type: 'object',
577
+ properties: {
578
+ data: {
579
+ type: ['object', 'array'],
580
+ properties: {
581
+ id: { type: 'string' },
582
+ type: { type: 'string' }
583
+ },
584
+ items: {
585
+ type: 'object',
586
+ properties: {
587
+ id: { type: 'string' },
588
+ type: { type: 'string' }
589
+ }
590
+ }
591
+ }
592
+ }
593
+ }
594
+
595
+ const stringify = build(schema, { ajv: { allowUnionTypes: true } })
596
+
597
+ const arrayInput = {
598
+ data: [
599
+ { id: '1', type: 'article' },
600
+ { id: '2', type: 'article' }
601
+ ]
602
+ }
603
+ t.assert.deepEqual(JSON.parse(stringify(arrayInput)), arrayInput)
604
+
605
+ const objectInput = {
606
+ data: { id: '1', type: 'article' }
607
+ }
608
+ t.assert.deepEqual(JSON.parse(stringify(objectInput)), objectInput)
609
+ })
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const Ajv = require('ajv')
4
+ const ajvFormats = require('ajv-formats')
5
+
6
+ // Compiles a JSON Schema into a `validate(data) => boolean` function with
7
+ // ajv, the validator the library itself uses, so tests check serialized
8
+ // output against the same rules. A fresh instance is created per schema so
9
+ // tests that reuse an `$id` do not collide.
10
+ module.exports = function validator (schema) {
11
+ const ajv = new Ajv({ strictSchema: false, allowUnionTypes: true })
12
+ ajvFormats(ajv)
13
+ // `unsafe` is a fast-json-stringify format that only disables escaping.
14
+ ajv.addFormat('unsafe', true)
15
+ // The serializer renders `format: time` as `HH:mm:ss` with no timezone
16
+ // (see `asTime` in lib/serializer.js), which ajv-formats rejects because
17
+ // RFC 3339 requires one. Validate the shape the library documents.
18
+ ajv.addFormat('time', /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/)
19
+ return ajv.compile(schema)
20
+ }
package/types/index.d.ts CHANGED
@@ -162,10 +162,22 @@ declare namespace build {
162
162
  * @default 'trunc'
163
163
  */
164
164
  rounding?: 'ceil' | 'floor' | 'round' | 'trunc'
165
+ /**
166
+ * Maximum number of nested schema levels allowed during compilation (0–100)
167
+ *
168
+ * @default 100
169
+ */
170
+ maxDepth?: number
165
171
  /**
166
172
  * Running mode of fast-json-stringify
167
173
  */
168
174
  mode?: 'debug' | 'standalone'
175
+ /**
176
+ * Embed Ajv-generated validators in standalone output instead of compiling schemas at runtime
177
+ *
178
+ * @default false
179
+ */
180
+ inlineValidators?: boolean
169
181
  /**
170
182
  * Large arrays are defined as arrays containing, by default, `20000`
171
183
  * elements or more. That value can be adjusted via the option parameter
@@ -180,6 +192,14 @@ declare namespace build {
180
192
  * @default 'default'
181
193
  */
182
194
  largeArrayMechanism?: 'default' | 'json-stringify'
195
+ /**
196
+ * Eagerly compile the Ajv validators used by `anyOf`, `oneOf` and
197
+ * `if/then/else` at build time instead of on the first serialization
198
+ * that reaches them.
199
+ *
200
+ * @default false
201
+ */
202
+ compileValidators?: boolean
183
203
  }
184
204
 
185
205
  export const validLargeArrayMechanisms: string[]
@@ -31,6 +31,10 @@ expect(build).type.not.toBeCallableWith({
31
31
  type: 'number'
32
32
  }, { rounding: 'invalid' })
33
33
 
34
+ build({} as Schema, { inlineValidators: true })
35
+ build({} as Schema, { inlineValidators: false })
36
+ expect(build).type.not.toBeCallableWith({} as Schema, { inlineValidators: 'true' })
37
+
34
38
  // String schema
35
39
  build({
36
40
  type: 'string'
@@ -257,3 +261,12 @@ build({}, { largeArraySize: 2000 })
257
261
  build({}, { largeArraySize: '2e4' })
258
262
  build({}, { largeArraySize: 2n })
259
263
  expect(build).type.not.toBeCallableWith({} as Schema, { largeArraySize: ['asdf'] })
264
+
265
+ // maxDepth
266
+ build({}, { maxDepth: 100 })
267
+ expect(build).type.not.toBeCallableWith({} as Schema, { maxDepth: '500' })
268
+
269
+ // compileValidators
270
+ build({}, { compileValidators: true })
271
+ build({}, { compileValidators: false })
272
+ expect(build).type.not.toBeCallableWith({} as Schema, { compileValidators: 'yes' })