Skip to content

Commit

Permalink
add support for date-time formats (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilaif authored Apr 4, 2020
1 parent 047c05a commit 01de4dd
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 16 deletions.
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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


__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`.
Expand Down Expand Up @@ -105,11 +105,34 @@ And nested ones, too.
<a name="specific"></a>
#### Specific use cases

| Instance | Serialized as |
| -----------|------------------------------|
| `Date` | `string` via `toISOString()` |
| `RegExp` | `string` |
| `BigInt` | `integer` via `toString` |
| Instance | Serialized as |
| -------- | ---------------------------- |
| `Date` | `string` via `toISOString()` |
| `RegExp` | `string` |
| `BigInt` | `integer` via `toString` |

[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:

| Format | Serialized format example |
| ----------- | -------------------------- |
| `date-time` | `2020-04-03T09:11:08.615Z` |
| `date` | `2020-04-03` |
| `time` | `09:11:08` |

Example with a MomentJS object:

```javascript
const moment = require('moment')

const stringify = fastJson({
title: 'Example Schema with string date-time field',
type: 'string',
format: 'date-time'
}

console.log(stringify(moment())) // '"YYYY-MM-DDTHH:mm:ss.sssZ"'
```
<a name="required"></a>
#### Required
Expand Down Expand Up @@ -444,7 +467,7 @@ const stringify = fastJson(schema, { schema: externalSchema })
<a name="long"></a>
#### Long integers
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.
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.
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.
Example:
```javascript
Expand Down
11 changes: 11 additions & 0 deletions example.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const moment = require('moment')
const fastJson = require('.')
const stringify = fastJson({
title: 'Example Schema',
Expand All @@ -18,6 +19,10 @@ const stringify = fastJson({
now: {
type: 'string'
},
birthdate: {
type: ['string'],
format: 'date-time'
},
reg: {
type: 'string'
},
Expand Down Expand Up @@ -48,6 +53,10 @@ const stringify = fastJson({
},
test: {
type: 'number'
},
date: {
type: 'string',
format: 'date-time'
}
},
additionalProperties: {
Expand All @@ -60,10 +69,12 @@ console.log(stringify({
lastName: 'Collina',
age: 32,
now: new Date(),
birthdate: moment(),
reg: /"([^"]|\\")*"/,
foo: 'hello',
numfoo: 42,
test: 42,
date: moment(),
strtest: '23',
arr: [{ str: 'stark' }, { str: 'lannister' }],
obj: { bool: true },
Expand Down
71 changes: 66 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

var Ajv = require('ajv')
var merge = require('deepmerge')

var util = require('util')
var validate = require('./schema-validator')
var stringSimilarity = null
Expand Down Expand Up @@ -52,9 +53,13 @@ function build (schema, options) {
`

code += `
${$pad2Zeros.toString()}
${$asString.toString()}
${$asStringNullable.toString()}
${$asStringSmall.toString()}
${$asDatetime.toString()}
${$asDate.toString()}
${$asTime.toString()}
${$asNumber.toString()}
${$asNumberNullable.toString()}
${$asIntegerNullable.toString()}
Expand Down Expand Up @@ -94,7 +99,7 @@ function build (schema, options) {
code = buildObject(schema, code, main, options.schema, fullSchema)
break
case 'string':
main = schema.nullable ? $asStringNullable.name : $asString.name
main = schema.nullable ? $asStringNullable.name : getStringSerializer(schema.format)
break
case 'integer':
main = schema.nullable ? $asIntegerNullable.name : $asInteger.name
Expand Down Expand Up @@ -209,6 +214,21 @@ function hasIf (schema) {
return /"if":{/.test(str) && /"then":{/.test(str)
}

const stringSerializerMap = {
'date-time': '$asDatetime',
date: '$asDate',
time: '$asTime'
}

function getStringSerializer (format) {
return stringSerializerMap[format] || '$asString'
}

function $pad2Zeros (num) {
var s = '00' + num
return s[s.length - 2] + s[s.length - 1]
}

function $asNull () {
return 'null'
}
Expand Down Expand Up @@ -248,6 +268,42 @@ function $asBooleanNullable (bool) {
return bool === null ? null : $asBoolean(bool)
}

function $asDatetime (date) {
if (date instanceof Date) {
return '"' + date.toISOString() + '"'
} else if (typeof date.toISOString === 'function') {
return '"' + date.toISOString() + '"'
} else {
return $asString(date)
}
}

function $asDate (date) {
if (date instanceof Date) {
var year = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date)
var month = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(date)
var day = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date)
return '"' + year + '-' + month + '-' + day + '"'
} else if (typeof date.format === 'function') {
return '"' + date.format('YYYY-MM-DD') + '"'
} else {
return $asString(date)
}
}

function $asTime (date) {
if (date instanceof Date) {
var hour = new Intl.DateTimeFormat('en', { hour: 'numeric', hour12: false }).format(date)
var minute = new Intl.DateTimeFormat('en', { minute: 'numeric' }).format(date)
var second = new Intl.DateTimeFormat('en', { second: 'numeric' }).format(date)
return '"' + $pad2Zeros(hour) + ':' + $pad2Zeros(minute) + ':' + $pad2Zeros(second) + '"'
} else if (typeof date.format === 'function') {
return '"' + date.format('HH:mm:ss') + '"'
} else {
return $asString(date)
}
}

function $asString (str) {
if (str instanceof Date) {
return '"' + str.toISOString() + '"'
Expand Down Expand Up @@ -317,6 +373,8 @@ function addPatternProperties (schema, externalSchema, fullSchema) {
pp[regex] = refFinder(pp[regex].$ref, fullSchema, externalSchema)
}
var type = pp[regex].type
var format = pp[regex].format
var stringSerializer = getStringSerializer(format)
code += `
if (/${regex.replace(/\\*\//g, '\\/')}/.test(keys[i])) {
`
Expand All @@ -340,7 +398,7 @@ function addPatternProperties (schema, externalSchema, fullSchema) {
} else if (type === 'string') {
code += `
${addComma}
json += $asString(keys[i]) + ':' + $asString(obj[keys[i]])
json += $asString(keys[i]) + ':' + ${stringSerializer}(obj[keys[i]])
`
} else if (type === 'integer') {
code += `
Expand Down Expand Up @@ -394,6 +452,8 @@ function additionalProperty (schema, externalSchema, fullSchema) {
}

var type = ap.type
var format = ap.format
var stringSerializer = getStringSerializer(format)
if (type === 'object') {
code += buildObject(ap, '', 'buildObjectAP', externalSchema)
code += `
Expand All @@ -414,7 +474,7 @@ function additionalProperty (schema, externalSchema, fullSchema) {
} else if (type === 'string') {
code += `
${addComma}
json += $asString(keys[i]) + ':' + $asString(obj[keys[i]])
json += $asString(keys[i]) + ':' + ${stringSerializer}(obj[keys[i]])
`
} else if (type === 'integer') {
code += `
Expand Down Expand Up @@ -941,7 +1001,8 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
`
break
case 'string':
code += nullable ? `json += obj${accessor} === null ? null : $asString(obj${accessor})` : `json += $asString(obj${accessor})`
var stringSerializer = getStringSerializer(schema.format)
code += nullable ? `json += obj${accessor} === null ? null : ${stringSerializer}(obj${accessor})` : `json += ${stringSerializer}(obj${accessor})`
break
case 'integer':
code += nullable ? `json += obj${accessor} === null ? null : $asInteger(obj${accessor})` : `json += $asInteger(obj${accessor})`
Expand Down Expand Up @@ -1010,7 +1071,7 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
var nestedResult = nested(laterCode, name, key, tempSchema, externalSchema, fullSchema, subKey)
if (type === 'string') {
code += `
${index === 0 ? 'if' : 'else if'}(typeof obj${accessor} === "${type}" || obj${accessor} instanceof Date || obj${accessor} instanceof RegExp)
${index === 0 ? 'if' : 'else if'}(typeof obj${accessor} === "${type}" || obj${accessor} instanceof Date || typeof obj${accessor}.toISOString === "function" || obj${accessor} instanceof RegExp)
${nestedResult.code}
`
} else if (type === 'null') {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"is-my-json-valid": "^2.20.0",
"json-strify": "^0.1.7",
"long": "^4.0.0",
"moment": "^2.24.0",
"pre-commit": "^1.2.2",
"proxyquire": "^2.1.3",
"semver": "^7.1.0",
Expand Down
26 changes: 26 additions & 0 deletions test/array.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const moment = require('moment')
const test = require('tap').test
const validator = require('is-my-json-valid')
const build = require('..')
Expand Down Expand Up @@ -164,3 +165,28 @@ test('invalid items throw', (t) => {
const stringify = build(schema)
t.throws(() => stringify({ args: ['invalid'] }))
})

test('moment array', (t) => {
t.plan(1)
const schema = {
type: 'object',
properties: {
times: {
type: 'array',
items: {
type: 'string',
format: 'date-time'
}
}
}
}
const stringify = build(schema)
try {
const value = stringify({
times: [moment('2018-04-21T07:52:31.017Z')]
})
t.is(value, '{"times":["2018-04-21T07:52:31.017Z"]}')
} catch (e) {
t.fail(e)
}
})
Loading

0 comments on commit 01de4dd

Please sign in to comment.