Skip to content

Commit

Permalink
feat: standalone mode (#460)
Browse files Browse the repository at this point in the history
* feat: standalone mode

* docs: update toc
  • Loading branch information
climba03003 authored Jun 10, 2022
1 parent b57abf1 commit 658411c
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 200 deletions.
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ compile-json-stringify date format x 1,086,187 ops/sec ±0.16% (99 runs sampled)
- <a href="#nullable">`Nullable`</a>
- <a href="#largearrays">`Large Arrays`</a>
- <a href="#security">`Security Notice`</a>
- <a href="#debug">`Debug Mode`</a>
- <a href="#standalone">`Standalone Mode`</a>
- <a href="#acknowledgements">`Acknowledgements`</a>
- <a href="#license">`License`</a>

Expand Down Expand Up @@ -663,13 +665,37 @@ const debugCompiled = fastJson({
type: 'string'
}
}
}, { debugMode: true })
}, { mode: 'debug' })

console.log(debugCompiled) // it is an array of functions that can create your `stringify` function
console.log(debugCompiled.toString()) // print a "ready to read" string function, you can save it to a file
console.log(debugCompiled) // it is a object contain code, ajv instance
const rawString = debugCompiled.code // it is the generated code
console.log(rawString)

const rawString = debugCompiled.toString()
const stringify = fastJson.restore(rawString) // use the generated string to get back the `stringify` function
const stringify = fastJson.restore(debugCompiled) // use the generated string to get back the `stringify` function
console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
```

<a name="standalone"></a>
### Standalone Mode

The standalone mode is used to compile the code that can be directly run by `node`
itself. You need to install `fast-json-stringify`, `ajv`, `fast-uri` and `ajv-formats`
in order to let the standalone code works.

```js
const fs = require('fs')
const code = fastJson({
title: 'default string',
type: 'object',
properties: {
firstName: {
type: 'string'
}
}
}, { mode: 'standalone' })

fs.writeFileSync('stringify.js', code)
const stringify = require('stringify.js')
console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
```

Expand Down
37 changes: 37 additions & 0 deletions ajv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict'

const Ajv = require('ajv')
const fastUri = require('fast-uri')
const ajvFormats = require('ajv-formats')

module.exports = buildAjv

function buildAjv (options) {
const ajvInstance = new Ajv({ ...options, strictSchema: false, uriResolver: fastUri })
ajvFormats(ajvInstance)

const validateDateTimeFormat = ajvFormats.get('date-time').validate
const validateDateFormat = ajvFormats.get('date').validate
const validateTimeFormat = ajvFormats.get('time').validate

ajvInstance.addKeyword({
keyword: 'fjs_date_type',
validate: (schema, date) => {
if (date instanceof Date) {
return true
}
if (schema === 'date-time') {
return validateDateTimeFormat(date)
}
if (schema === 'date') {
return validateDateFormat(date)
}
if (schema === 'time') {
return validateTimeFormat(date)
}
return false
}
})

return ajvInstance
}
218 changes: 23 additions & 195 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

/* eslint no-prototype-builtins: 0 */

const Ajv = require('ajv')
const fastUri = require('fast-uri')
const ajvFormats = require('ajv-formats')
const merge = require('deepmerge')
const clone = require('rfdc')({ proto: true })
const fjsCloned = Symbol('fast-json-stringify.cloned')
const { randomUUID } = require('crypto')

const validate = require('./schema-validator')
const Serializer = require('./serializer')
const buildAjv = require('./ajv')

let largeArraySize = 2e4
let stringSimilarity = null
Expand Down Expand Up @@ -57,173 +56,6 @@ const schemaReferenceMap = new Map()
let ajvInstance = null
let contextFunctions = null

class Serializer {
constructor (options = {}) {
switch (options.rounding) {
case 'floor':
this.parseInteger = Math.floor
break
case 'ceil':
this.parseInteger = Math.ceil
break
case 'round':
this.parseInteger = Math.round
break
default:
this.parseInteger = Math.trunc
break
}
}

asAny (i) {
return JSON.stringify(i)
}

asNull () {
return 'null'
}

asInteger (i) {
if (typeof i === 'bigint') {
return i.toString()
} else if (Number.isInteger(i)) {
return '' + i
} else {
/* eslint no-undef: "off" */
const integer = this.parseInteger(i)
if (Number.isNaN(integer)) {
throw new Error(`The value "${i}" cannot be converted to an integer.`)
} else {
return '' + integer
}
}
}

asIntegerNullable (i) {
return i === null ? 'null' : this.asInteger(i)
}

asNumber (i) {
const num = Number(i)
if (Number.isNaN(num)) {
throw new Error(`The value "${i}" cannot be converted to a number.`)
} else {
return '' + num
}
}

asNumberNullable (i) {
return i === null ? 'null' : this.asNumber(i)
}

asBoolean (bool) {
return bool && 'true' || 'false' // eslint-disable-line
}

asBooleanNullable (bool) {
return bool === null ? 'null' : this.asBoolean(bool)
}

asDatetime (date, skipQuotes) {
const quotes = skipQuotes === true ? '' : '"'
if (date instanceof Date) {
return quotes + date.toISOString() + quotes
}
return this.asString(date, skipQuotes)
}

asDatetimeNullable (date, skipQuotes) {
return date === null ? 'null' : this.asDatetime(date, skipQuotes)
}

asDate (date, skipQuotes) {
const quotes = skipQuotes === true ? '' : '"'
if (date instanceof Date) {
return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + quotes
}
return this.asString(date, skipQuotes)
}

asDateNullable (date, skipQuotes) {
return date === null ? 'null' : this.asDate(date, skipQuotes)
}

asTime (date, skipQuotes) {
const quotes = skipQuotes === true ? '' : '"'
if (date instanceof Date) {
return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + quotes
}
return this.asString(date, skipQuotes)
}

asTimeNullable (date, skipQuotes) {
return date === null ? 'null' : this.asTime(date, skipQuotes)
}

asString (str, skipQuotes) {
const quotes = skipQuotes === true ? '' : '"'
if (str instanceof Date) {
return quotes + str.toISOString() + quotes
} else if (str === null) {
return quotes + quotes
} else if (str instanceof RegExp) {
str = str.source
} else if (typeof str !== 'string') {
str = str.toString()
}
// If we skipQuotes it means that we are using it as test
// no need to test the string length for the render
if (skipQuotes) {
return str
}

if (str.length < 42) {
return this.asStringSmall(str)
} else {
return JSON.stringify(str)
}
}

asStringNullable (str) {
return str === null ? 'null' : this.asString(str)
}

// magically escape strings for json
// relying on their charCodeAt
// everything below 32 needs JSON.stringify()
// every string that contain surrogate needs JSON.stringify()
// 34 and 92 happens all the time, so we
// have a fast case for them
asStringSmall (str) {
const l = str.length
let result = ''
let last = 0
let found = false
let surrogateFound = false
let point = 255
// eslint-disable-next-line
for (var i = 0; i < l && point >= 32; i++) {
point = str.charCodeAt(i)
if (point >= 0xD800 && point <= 0xDFFF) {
// The current character is a surrogate.
surrogateFound = true
}
if (point === 34 || point === 92) {
result += str.slice(last, i) + '\\'
last = i
found = true
}
}

if (!found) {
result = str
} else {
result += str.slice(last)
}
return ((point < 32) || (surrogateFound === true)) ? JSON.stringify(str) : '"' + result + '"'
}
}

function build (schema, options) {
arrayItemsReferenceSerializersMap.clear()
objectReferenceSerializersMap.clear()
Expand All @@ -232,31 +64,7 @@ function build (schema, options) {
contextFunctions = []
options = options || {}

ajvInstance = new Ajv({ ...options.ajv, strictSchema: false, uriResolver: fastUri })
ajvFormats(ajvInstance)

const validateDateTimeFormat = ajvFormats.get('date-time').validate
const validateDateFormat = ajvFormats.get('date').validate
const validateTimeFormat = ajvFormats.get('time').validate

ajvInstance.addKeyword({
keyword: 'fjs_date_type',
validate: (schema, date) => {
if (date instanceof Date) {
return true
}
if (schema === 'date-time') {
return validateDateTimeFormat(date)
}
if (schema === 'date') {
return validateDateFormat(date)
}
if (schema === 'time') {
return validateTimeFormat(date)
}
return false
}
})
ajvInstance = buildAjv(options.ajv)

isValidSchema(schema)
if (options.schema) {
Expand Down Expand Up @@ -320,9 +128,29 @@ function build (schema, options) {
const dependenciesName = ['ajv', 'serializer', contextFunctionCode]

if (options.debugMode) {
options.mode = 'debug'
}

if (options.mode === 'debug') {
return { code: dependenciesName.join('\n'), ajv: ajvInstance }
}

if (options.mode === 'standalone') {
return `
'use strict'
const Serializer = require('fast-json-stringify/serializer')
const buildAjv = require('fast-json-stringify/ajv')
const serializer = new Serializer(${JSON.stringify(options || {})})
const ajv = buildAjv(${JSON.stringify(options.ajv || {})})
${contextFunctionCode.replace('return main', '')}
module.exports = main
`
}

/* eslint no-new-func: "off" */
const contextFunc = new Function('ajv', 'serializer', contextFunctionCode)
const stringifyFunc = contextFunc(ajvInstance, serializer)
Expand Down
Loading

0 comments on commit 658411c

Please sign in to comment.