fast-json-stringify 5.0.6 → 5.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/ajv.js CHANGED
@@ -7,29 +7,22 @@ const ajvFormats = require('ajv-formats')
7
7
  module.exports = buildAjv
8
8
 
9
9
  function buildAjv (options) {
10
- const ajvInstance = new Ajv({ ...options, strictSchema: false, validateSchema: false, uriResolver: fastUri })
11
- ajvFormats(ajvInstance)
10
+ const ajvInstance = new Ajv({
11
+ ...options,
12
+ strictSchema: false,
13
+ validateSchema: false,
14
+ allowUnionTypes: true,
15
+ uriResolver: fastUri
16
+ })
12
17
 
13
- const validateDateTimeFormat = ajvFormats.get('date-time').validate
14
- const validateDateFormat = ajvFormats.get('date').validate
15
- const validateTimeFormat = ajvFormats.get('time').validate
18
+ ajvFormats(ajvInstance)
16
19
 
17
20
  ajvInstance.addKeyword({
18
- keyword: 'fjs_date_type',
19
- validate: (schema, date) => {
20
- if (date instanceof Date) {
21
- return true
22
- }
23
- if (schema === 'date-time') {
24
- return validateDateTimeFormat(date)
25
- }
26
- if (schema === 'date') {
27
- return validateDateFormat(date)
28
- }
29
- if (schema === 'time') {
30
- return validateTimeFormat(date)
31
- }
32
- return false
21
+ keyword: 'fjs_type',
22
+ type: 'object',
23
+ errors: false,
24
+ validate: (type, date) => {
25
+ return date instanceof Date
33
26
  }
34
27
  })
35
28
 
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  /* eslint no-prototype-builtins: 0 */
4
4
 
5
- const merge = require('deepmerge')
5
+ const merge = require('@fastify/deepmerge')()
6
6
  const clone = require('rfdc')({ proto: true })
7
7
  const fjsCloned = Symbol('fast-json-stringify.cloned')
8
8
  const { randomUUID } = require('crypto')
@@ -243,15 +243,6 @@ function inferTypeByKeyword (schema) {
243
243
  return schema.type
244
244
  }
245
245
 
246
- function getStringSerializer (format, nullable) {
247
- switch (format) {
248
- case 'date-time': return nullable ? 'serializer.asDatetimeNullable.bind(serializer)' : 'serializer.asDatetime.bind(serializer)'
249
- case 'date': return nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
250
- case 'time': return nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
251
- default: return nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
252
- }
253
- }
254
-
255
246
  function addPatternProperties (location) {
256
247
  const schema = location.schema
257
248
  const pp = schema.patternProperties
@@ -484,6 +475,16 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
484
475
  mergedSchema.anyOf.push(...allOfSchema.anyOf)
485
476
  }
486
477
 
478
+ if (allOfSchema.fjs_type !== undefined) {
479
+ if (
480
+ mergedSchema.fjs_type !== undefined &&
481
+ mergedSchema.fjs_type !== allOfSchema.fjs_type
482
+ ) {
483
+ throw new Error('allOf schemas have different fjs_type values')
484
+ }
485
+ mergedSchema.fjs_type = allOfSchema.fjs_type
486
+ }
487
+
487
488
  if (allOfSchema.allOf !== undefined) {
488
489
  mergeAllOfSchema(location, allOfSchema, mergedSchema)
489
490
  }
@@ -771,6 +772,10 @@ function generateFuncName () {
771
772
  function buildValue (location, input) {
772
773
  let schema = location.schema
773
774
 
775
+ if (typeof schema === 'boolean') {
776
+ return `json += JSON.stringify(${input})`
777
+ }
778
+
774
779
  if (schema.$ref) {
775
780
  location = resolveRef(location, schema.$ref)
776
781
  schema = location.schema
@@ -790,20 +795,22 @@ function buildValue (location, input) {
790
795
  location.schema = mergedSchema
791
796
  }
792
797
 
793
- const type = schema.type
798
+ let type = schema.type
794
799
  const nullable = schema.nullable === true
795
800
 
796
801
  let code = ''
797
802
  let funcName
798
803
 
804
+ if (schema.fjs_type === 'string' && schema.format === undefined && Array.isArray(schema.type) && schema.type.length === 2) {
805
+ type = 'string'
806
+ }
807
+
799
808
  switch (type) {
800
809
  case 'null':
801
- code += `
802
- json += serializer.asNull()
803
- `
810
+ code += 'json += serializer.asNull()'
804
811
  break
805
812
  case 'string': {
806
- funcName = getStringSerializer(schema.format, nullable)
813
+ funcName = nullable ? 'serializer.asStringNullable.bind(serializer)' : 'serializer.asString.bind(serializer)'
807
814
  code += `json += ${funcName}(${input})`
808
815
  break
809
816
  }
@@ -820,7 +827,15 @@ function buildValue (location, input) {
820
827
  code += `json += ${funcName}(${input})`
821
828
  break
822
829
  case 'object':
823
- funcName = buildObject(location)
830
+ if (schema.format === 'date-time') {
831
+ funcName = nullable ? 'serializer.asDateTimeNullable.bind(serializer)' : 'serializer.asDateTime.bind(serializer)'
832
+ } else if (schema.format === 'date') {
833
+ funcName = nullable ? 'serializer.asDateNullable.bind(serializer)' : 'serializer.asDate.bind(serializer)'
834
+ } else if (schema.format === 'time') {
835
+ funcName = nullable ? 'serializer.asTimeNullable.bind(serializer)' : 'serializer.asTime.bind(serializer)'
836
+ } else {
837
+ funcName = buildObject(location)
838
+ }
824
839
  code += `json += ${funcName}(${input})`
825
840
  break
826
841
  case 'array':
@@ -828,11 +843,7 @@ function buildValue (location, input) {
828
843
  code += `json += ${funcName}(${input})`
829
844
  break
830
845
  case undefined:
831
- if (schema.fjs_date_type) {
832
- funcName = getStringSerializer(schema.fjs_date_type, nullable)
833
- code += `json += ${funcName}(${input})`
834
- break
835
- } else if (schema.anyOf || schema.oneOf) {
846
+ if (schema.anyOf || schema.oneOf) {
836
847
  // beware: dereferenceOfRefs has side effects and changes schema.anyOf
837
848
  const type = schema.anyOf ? 'anyOf' : 'oneOf'
838
849
  const anyOfLocation = mergeLocation(location, type)
@@ -890,7 +901,7 @@ function buildValue (location, input) {
890
901
  switch (type) {
891
902
  case 'string': {
892
903
  code += `
893
- ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof Date || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
904
+ ${statement}(${input} === null || typeof ${input} === "${type}" || ${input} instanceof RegExp || (typeof ${input} === "object" && Object.hasOwnProperty.call(${input}, "toString")))
894
905
  ${nestedResult}
895
906
  `
896
907
  break
@@ -909,6 +920,20 @@ function buildValue (location, input) {
909
920
  `
910
921
  break
911
922
  }
923
+ case 'object': {
924
+ if (schema.fjs_type) {
925
+ code += `
926
+ ${statement}(${input} instanceof Date || ${input} === null)
927
+ ${nestedResult}
928
+ `
929
+ } else {
930
+ code += `
931
+ ${statement}(typeof ${input} === "object" || ${input} === null)
932
+ ${nestedResult}
933
+ `
934
+ }
935
+ break
936
+ }
912
937
  default: {
913
938
  code += `
914
939
  ${statement}(typeof ${input} === "${type}" || ${input} === null)
@@ -936,15 +961,21 @@ function buildValue (location, input) {
936
961
  }
937
962
 
938
963
  // Ajv does not support js date format. In order to properly validate objects containing a date,
939
- // it needs to replace all occurrences of the string date format with a custom keyword fjs_date_type.
964
+ // it needs to replace all occurrences of the string date format with a custom keyword fjs_type.
940
965
  // (see https://github.com/fastify/fast-json-stringify/pull/441)
941
966
  function extendDateTimeType (schema) {
942
967
  if (schema === null) return
943
968
 
944
- if (schema.type === 'string' && ['date-time', 'date', 'time'].includes(schema.format)) {
945
- schema.fjs_date_type = schema.format
946
- delete schema.type
947
- delete schema.format
969
+ if (schema.type === 'string') {
970
+ schema.fjs_type = 'string'
971
+ schema.type = ['string', 'object']
972
+ } else if (
973
+ Array.isArray(schema.type) &&
974
+ schema.type.includes('string') &&
975
+ !schema.type.includes('object')
976
+ ) {
977
+ schema.fjs_type = 'string'
978
+ schema.type.push('object')
948
979
  }
949
980
  for (const property in schema) {
950
981
  if (typeof schema[property] === 'object') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "5.0.6",
3
+ "version": "5.1.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -49,9 +49,9 @@
49
49
  "webpack": "^5.40.0"
50
50
  },
51
51
  "dependencies": {
52
+ "@fastify/deepmerge": "^1.0.0",
52
53
  "ajv": "^8.10.0",
53
54
  "ajv-formats": "^2.1.1",
54
- "deepmerge": "^4.2.2",
55
55
  "fast-uri": "^2.1.0",
56
56
  "rfdc": "^1.2.0"
57
57
  },
package/serializer.js CHANGED
@@ -69,24 +69,24 @@ module.exports = class Serializer {
69
69
  return bool === null ? 'null' : this.asBoolean(bool)
70
70
  }
71
71
 
72
- asDatetime (date) {
73
- const quotes = '"'
72
+ asDateTime (date) {
73
+ if (date === null) return '""'
74
74
  if (date instanceof Date) {
75
- return quotes + date.toISOString() + quotes
75
+ return '"' + date.toISOString() + '"'
76
76
  }
77
- return this.asString(date)
77
+ throw new Error(`The value "${date}" cannot be converted to a date-time.`)
78
78
  }
79
79
 
80
- asDatetimeNullable (date) {
81
- return date === null ? 'null' : this.asDatetime(date)
80
+ asDateTimeNullable (date) {
81
+ return date === null ? 'null' : this.asDateTime(date)
82
82
  }
83
83
 
84
84
  asDate (date) {
85
- const quotes = '"'
85
+ if (date === null) return '""'
86
86
  if (date instanceof Date) {
87
- return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + quotes
87
+ return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"'
88
88
  }
89
- return this.asString(date)
89
+ throw new Error(`The value "${date}" cannot be converted to a date.`)
90
90
  }
91
91
 
92
92
  asDateNullable (date) {
@@ -94,11 +94,11 @@ module.exports = class Serializer {
94
94
  }
95
95
 
96
96
  asTime (date) {
97
- const quotes = '"'
97
+ if (date === null) return '""'
98
98
  if (date instanceof Date) {
99
- return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + quotes
99
+ return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"'
100
100
  }
101
- return this.asString(date)
101
+ throw new Error(`The value "${date}" cannot be converted to a time.`)
102
102
  }
103
103
 
104
104
  asTimeNullable (date) {
@@ -359,3 +359,16 @@ test('render a single quote as JSON', (t) => {
359
359
  t.equal(output, JSON.stringify(toStringify))
360
360
  t.ok(validate(JSON.parse(output)), 'valid schema')
361
361
  })
362
+
363
+ test('returns JSON.stringify if schema type is boolean', t => {
364
+ t.plan(1)
365
+
366
+ const schema = {
367
+ type: 'array',
368
+ items: true
369
+ }
370
+
371
+ const array = [1, true, 'test']
372
+ const stringify = build(schema)
373
+ t.equal(stringify(array), JSON.stringify(array))
374
+ })
package/test/date.test.js CHANGED
@@ -308,7 +308,7 @@ test('serializing null value', t => {
308
308
  })
309
309
 
310
310
  t.test('type::array', t => {
311
- t.plan(3)
311
+ t.plan(6)
312
312
 
313
313
  t.test('format::date-time', t => {
314
314
  t.plan(2)
@@ -348,13 +348,13 @@ test('serializing null value', t => {
348
348
  t.notOk(validate(JSON.parse(output)), 'an empty string is not a date format')
349
349
  })
350
350
 
351
- t.test('format::time', t => {
351
+ t.test('format::date', t => {
352
352
  t.plan(2)
353
353
 
354
354
  const prop = {
355
355
  updatedAt: {
356
356
  type: ['string'],
357
- format: 'time'
357
+ format: 'date'
358
358
  }
359
359
  }
360
360
 
@@ -364,7 +364,66 @@ test('serializing null value', t => {
364
364
  } = serialize(createSchema(prop), input)
365
365
 
366
366
  t.equal(output, '{"updatedAt":""}')
367
- t.notOk(validate(JSON.parse(output)), 'an empty string is not a time format')
367
+ t.notOk(validate(JSON.parse(output)), 'an empty string is not a date format')
368
+ })
369
+
370
+ t.test('format::time, Date object', t => {
371
+ t.plan(1)
372
+
373
+ const schema = {
374
+ oneOf: [
375
+ {
376
+ type: 'object',
377
+ properties: {
378
+ updatedAt: {
379
+ type: ['string', 'number'],
380
+ format: 'time'
381
+ }
382
+ }
383
+ }
384
+ ]
385
+ }
386
+
387
+ const date = new Date()
388
+ const input = { updatedAt: date }
389
+ const { output } = serialize(schema, input)
390
+
391
+ t.equal(output, JSON.stringify({ updatedAt: DateTime.fromJSDate(date).toFormat('HH:mm:ss') }))
392
+ })
393
+
394
+ t.test('format::time, Date object', t => {
395
+ t.plan(1)
396
+
397
+ const schema = {
398
+ oneOf: [
399
+ {
400
+ type: ['string', 'number'],
401
+ format: 'time'
402
+ }
403
+ ]
404
+ }
405
+
406
+ const date = new Date()
407
+ const { output } = serialize(schema, date)
408
+
409
+ t.equal(output, `"${DateTime.fromJSDate(date).toFormat('HH:mm:ss')}"`)
410
+ })
411
+
412
+ t.test('format::time, Date object', t => {
413
+ t.plan(1)
414
+
415
+ const schema = {
416
+ oneOf: [
417
+ {
418
+ type: ['string', 'number'],
419
+ format: 'time'
420
+ }
421
+ ]
422
+ }
423
+
424
+ const { output } = serialize(schema, 42)
425
+
426
+ t.equal(output, JSON.stringify(42))
368
427
  })
369
428
  })
370
429
 
@@ -429,3 +488,40 @@ test('serializing null value', t => {
429
488
  })
430
489
  })
431
490
  })
491
+
492
+ test('Validate Date object as string type', (t) => {
493
+ t.plan(1)
494
+
495
+ const schema = {
496
+ oneOf: [
497
+ { type: 'string' }
498
+ ]
499
+ }
500
+ const toStringify = new Date()
501
+
502
+ const stringify = build(schema)
503
+ const output = stringify(toStringify)
504
+
505
+ t.equal(output, JSON.stringify(toStringify))
506
+ })
507
+
508
+ test('nullable date', (t) => {
509
+ t.plan(1)
510
+
511
+ const schema = {
512
+ anyOf: [
513
+ {
514
+ format: 'date',
515
+ type: 'string',
516
+ nullable: true
517
+ }
518
+ ]
519
+ }
520
+
521
+ const stringify = build(schema)
522
+
523
+ const data = new Date()
524
+ const result = stringify(data)
525
+
526
+ t.same(result, `"${DateTime.fromJSDate(data).toISODate()}"`)
527
+ })