Skip to content

Commit 01de4dd

Browse files
authored
add support for date-time formats (#195)
1 parent 047c05a commit 01de4dd

File tree

7 files changed

+315
-16
lines changed

7 files changed

+315
-16
lines changed

README.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
44
![Ci Workflow](https://github.com/fastify/fast-json-stringify/workflows/CI%20workflow/badge.svg)
5-
[![NPM downloads](https://img.shields.io/npm/dm/fast-json-stringify.svg?style=flat)](https://www.npmjs.com/package/fast-json-stringify)
5+
[![NPM downloads](https://img.shields.io/npm/dm/fast-json-stringify.svg?style=flat)](https://www.npmjs.com/package/fast-json-stringify)
66

77

88
__fast-json-stringify__ is significantly faster than `JSON.stringify()` for small payloads. Its performance advantage shrinks as your payload grows. It pairs well with [__flatstr__](https://www.npmjs.com/package/flatstr), which triggers a V8 optimization that improves performance when eventually converting the string to a `Buffer`.
@@ -105,11 +105,34 @@ And nested ones, too.
105105
<a name="specific"></a>
106106
#### Specific use cases
107107

108-
| Instance | Serialized as |
109-
| -----------|------------------------------|
110-
| `Date` | `string` via `toISOString()` |
111-
| `RegExp` | `string` |
112-
| `BigInt` | `integer` via `toString` |
108+
| Instance | Serialized as |
109+
| -------- | ---------------------------- |
110+
| `Date` | `string` via `toISOString()` |
111+
| `RegExp` | `string` |
112+
| `BigInt` | `integer` via `toString` |
113+
114+
[JSON Schema built-in formats](https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats) for dates are supported and will be serialized as:
115+
116+
| Format | Serialized format example |
117+
| ----------- | -------------------------- |
118+
| `date-time` | `2020-04-03T09:11:08.615Z` |
119+
| `date` | `2020-04-03` |
120+
| `time` | `09:11:08` |
121+
122+
Example with a MomentJS object:
123+
124+
```javascript
125+
const moment = require('moment')
126+
127+
const stringify = fastJson({
128+
title: 'Example Schema with string date-time field',
129+
type: 'string',
130+
format: 'date-time'
131+
}
132+
133+
console.log(stringify(moment())) // '"YYYY-MM-DDTHH:mm:ss.sssZ"'
134+
```
135+
113136
114137
<a name="required"></a>
115138
#### Required
@@ -444,7 +467,7 @@ const stringify = fastJson(schema, { schema: externalSchema })
444467
445468
<a name="long"></a>
446469
#### Long integers
447-
By default the library will handle automatically [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) from Node.js v10.3 and above.
470+
By default the library will handle automatically [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) from Node.js v10.3 and above.
448471
If you can't use BigInts in your environment, long integers (64-bit) are also supported using the [long](https://github.com/dcodeIO/long.js) module.
449472
Example:
450473
```javascript

example.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict'
22

3+
const moment = require('moment')
34
const fastJson = require('.')
45
const stringify = fastJson({
56
title: 'Example Schema',
@@ -18,6 +19,10 @@ const stringify = fastJson({
1819
now: {
1920
type: 'string'
2021
},
22+
birthdate: {
23+
type: ['string'],
24+
format: 'date-time'
25+
},
2126
reg: {
2227
type: 'string'
2328
},
@@ -48,6 +53,10 @@ const stringify = fastJson({
4853
},
4954
test: {
5055
type: 'number'
56+
},
57+
date: {
58+
type: 'string',
59+
format: 'date-time'
5160
}
5261
},
5362
additionalProperties: {
@@ -60,10 +69,12 @@ console.log(stringify({
6069
lastName: 'Collina',
6170
age: 32,
6271
now: new Date(),
72+
birthdate: moment(),
6373
reg: /"([^"]|\\")*"/,
6474
foo: 'hello',
6575
numfoo: 42,
6676
test: 42,
77+
date: moment(),
6778
strtest: '23',
6879
arr: [{ str: 'stark' }, { str: 'lannister' }],
6980
obj: { bool: true },

index.js

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
var Ajv = require('ajv')
66
var merge = require('deepmerge')
7+
78
var util = require('util')
89
var validate = require('./schema-validator')
910
var stringSimilarity = null
@@ -52,9 +53,13 @@ function build (schema, options) {
5253
`
5354

5455
code += `
56+
${$pad2Zeros.toString()}
5557
${$asString.toString()}
5658
${$asStringNullable.toString()}
5759
${$asStringSmall.toString()}
60+
${$asDatetime.toString()}
61+
${$asDate.toString()}
62+
${$asTime.toString()}
5863
${$asNumber.toString()}
5964
${$asNumberNullable.toString()}
6065
${$asIntegerNullable.toString()}
@@ -94,7 +99,7 @@ function build (schema, options) {
9499
code = buildObject(schema, code, main, options.schema, fullSchema)
95100
break
96101
case 'string':
97-
main = schema.nullable ? $asStringNullable.name : $asString.name
102+
main = schema.nullable ? $asStringNullable.name : getStringSerializer(schema.format)
98103
break
99104
case 'integer':
100105
main = schema.nullable ? $asIntegerNullable.name : $asInteger.name
@@ -209,6 +214,21 @@ function hasIf (schema) {
209214
return /"if":{/.test(str) && /"then":{/.test(str)
210215
}
211216

217+
const stringSerializerMap = {
218+
'date-time': '$asDatetime',
219+
date: '$asDate',
220+
time: '$asTime'
221+
}
222+
223+
function getStringSerializer (format) {
224+
return stringSerializerMap[format] || '$asString'
225+
}
226+
227+
function $pad2Zeros (num) {
228+
var s = '00' + num
229+
return s[s.length - 2] + s[s.length - 1]
230+
}
231+
212232
function $asNull () {
213233
return 'null'
214234
}
@@ -248,6 +268,42 @@ function $asBooleanNullable (bool) {
248268
return bool === null ? null : $asBoolean(bool)
249269
}
250270

271+
function $asDatetime (date) {
272+
if (date instanceof Date) {
273+
return '"' + date.toISOString() + '"'
274+
} else if (typeof date.toISOString === 'function') {
275+
return '"' + date.toISOString() + '"'
276+
} else {
277+
return $asString(date)
278+
}
279+
}
280+
281+
function $asDate (date) {
282+
if (date instanceof Date) {
283+
var year = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
284+
var month = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
285+
var day = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
286+
return '"' + year + '-' + month + '-' + day + '"'
287+
} else if (typeof date.format === 'function') {
288+
return '"' + date.format('YYYY-MM-DD') + '"'
289+
} else {
290+
return $asString(date)
291+
}
292+
}
293+
294+
function $asTime (date) {
295+
if (date instanceof Date) {
296+
var hour = new Intl.DateTimeFormat('en', { hour: 'numeric', hour12: false }).format(date)
297+
var minute = new Intl.DateTimeFormat('en', { minute: 'numeric' }).format(date)
298+
var second = new Intl.DateTimeFormat('en', { second: 'numeric' }).format(date)
299+
return '"' + $pad2Zeros(hour) + ':' + $pad2Zeros(minute) + ':' + $pad2Zeros(second) + '"'
300+
} else if (typeof date.format === 'function') {
301+
return '"' + date.format('HH:mm:ss') + '"'
302+
} else {
303+
return $asString(date)
304+
}
305+
}
306+
251307
function $asString (str) {
252308
if (str instanceof Date) {
253309
return '"' + str.toISOString() + '"'
@@ -317,6 +373,8 @@ function addPatternProperties (schema, externalSchema, fullSchema) {
317373
pp[regex] = refFinder(pp[regex].$ref, fullSchema, externalSchema)
318374
}
319375
var type = pp[regex].type
376+
var format = pp[regex].format
377+
var stringSerializer = getStringSerializer(format)
320378
code += `
321379
if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {
322380
`
@@ -340,7 +398,7 @@ function addPatternProperties (schema, externalSchema, fullSchema) {
340398
} else if (type === 'string') {
341399
code += `
342400
${addComma}
343-
json += $asString(keys[i]) + ':' + $asString(obj[keys[i]])
401+
json += $asString(keys[i]) + ':' + ${stringSerializer}(obj[keys[i]])
344402
`
345403
} else if (type === 'integer') {
346404
code += `
@@ -394,6 +452,8 @@ function additionalProperty (schema, externalSchema, fullSchema) {
394452
}
395453

396454
var type = ap.type
455+
var format = ap.format
456+
var stringSerializer = getStringSerializer(format)
397457
if (type === 'object') {
398458
code += buildObject(ap, '', 'buildObjectAP', externalSchema)
399459
code += `
@@ -414,7 +474,7 @@ function additionalProperty (schema, externalSchema, fullSchema) {
414474
} else if (type === 'string') {
415475
code += `
416476
${addComma}
417-
json += $asString(keys[i]) + ':' + $asString(obj[keys[i]])
477+
json += $asString(keys[i]) + ':' + ${stringSerializer}(obj[keys[i]])
418478
`
419479
} else if (type === 'integer') {
420480
code += `
@@ -941,7 +1001,8 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
9411001
`
9421002
break
9431003
case 'string':
944-
code += nullable ? `json += obj${accessor} === null ? null : $asString(obj${accessor})` : `json += $asString(obj${accessor})`
1004+
var stringSerializer = getStringSerializer(schema.format)
1005+
code += nullable ? `json += obj${accessor} === null ? null : ${stringSerializer}(obj${accessor})` : `json += ${stringSerializer}(obj${accessor})`
9451006
break
9461007
case 'integer':
9471008
code += nullable ? `json += obj${accessor} === null ? null : $asInteger(obj${accessor})` : `json += $asInteger(obj${accessor})`
@@ -1010,7 +1071,7 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
10101071
var nestedResult = nested(laterCode, name, key, tempSchema, externalSchema, fullSchema, subKey)
10111072
if (type === 'string') {
10121073
code += `
1013-
${index === 0 ? 'if' : 'else if'}(typeof obj${accessor} === "${type}" || obj${accessor} instanceof Date || obj${accessor} instanceof RegExp)
1074+
${index === 0 ? 'if' : 'else if'}(typeof obj${accessor} === "${type}" || obj${accessor} instanceof Date || typeof obj${accessor}.toISOString === "function" || obj${accessor} instanceof RegExp)
10141075
${nestedResult.code}
10151076
`
10161077
} else if (type === 'null') {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"is-my-json-valid": "^2.20.0",
3636
"json-strify": "^0.1.7",
3737
"long": "^4.0.0",
38+
"moment": "^2.24.0",
3839
"pre-commit": "^1.2.2",
3940
"proxyquire": "^2.1.3",
4041
"semver": "^7.1.0",

test/array.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict'
22

3+
const moment = require('moment')
34
const test = require('tap').test
45
const validator = require('is-my-json-valid')
56
const build = require('..')
@@ -164,3 +165,28 @@ test('invalid items throw', (t) => {
164165
const stringify = build(schema)
165166
t.throws(() => stringify({ args: ['invalid'] }))
166167
})
168+
169+
test('moment array', (t) => {
170+
t.plan(1)
171+
const schema = {
172+
type: 'object',
173+
properties: {
174+
times: {
175+
type: 'array',
176+
items: {
177+
type: 'string',
178+
format: 'date-time'
179+
}
180+
}
181+
}
182+
}
183+
const stringify = build(schema)
184+
try {
185+
const value = stringify({
186+
times: [moment('2018-04-21T07:52:31.017Z')]
187+
})
188+
t.is(value, '{"times":["2018-04-21T07:52:31.017Z"]}')
189+
} catch (e) {
190+
t.fail(e)
191+
}
192+
})

0 commit comments

Comments
 (0)