Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const stringify = fastJson(mySchema, {
- `schema`: external schemas references by $ref property. [More details](#ref)
- `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof)
- `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer)
- `inlineValidators`: when using standalone mode, embed Ajv-generated validator functions in the output instead of compiling schemas at runtime. [More details](#standalone)
- `largeArrayMechanism`: set the mechanism that should be used to handle large
(by default `20000` or more items) arrays. [More details](#largearrays)

Expand Down Expand Up @@ -724,13 +725,21 @@ const code = fastJson({
type: 'string'
}
}
}, { mode: 'standalone' })
}, { mode: 'standalone', inlineValidators: true })

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

Set `inlineValidators` to `true` to include the Ajv-generated functions used by
`anyOf`, `oneOf`, and `if/then/else` in the same output file. This avoids
rebuilding an Ajv instance and compiling their schemas at runtime. The
generated module still requires `fast-json-stringify` for its serializer and
may require Ajv runtime helpers used by the generated validation code. Custom
Ajv formats used by these schemas must support Ajv's standalone code
generation.

<a name="acknowledgments"></a>
## Acknowledgments

Expand Down
8 changes: 7 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ function build (schema, options) {
refResolver: new RefResolver(),
rootSchemaId: schema.$id || `__fjs_root_${schemaIdCounter++}`,
validatorSchemasIds: new Set(),
validatorSchemaRefs: new Set(),
mergedSchemasIds: new Map(),
recursiveSchemas: new Set(),
recursivePaths: new Set(),
Expand Down Expand Up @@ -208,7 +209,10 @@ function build (schema, options) {
}

const serializer = new Serializer(options)
const validator = new Validator(options.ajv)
const validator = new Validator(
options.ajv,
options.mode === 'standalone' && options.inlineValidators
)

for (const schemaId of context.validatorSchemasIds) {
const schema = context.refResolver.getSchema(schemaId)
Expand Down Expand Up @@ -1149,6 +1153,7 @@ function buildOneOf (context, location, input) {

const nestedResult = buildValue(context, mergedLocation, input)
const schemaRef = optionLocation.getSchemaRef()
context.validatorSchemaRefs.add(schemaRef)

code += `
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) {
Expand Down Expand Up @@ -1182,6 +1187,7 @@ function buildIfThenElse (context, location, input) {

const ifLocation = location.getPropertyLocation('if')
const ifSchemaRef = ifLocation.getSchemaRef()
context.validatorSchemaRefs.add(ifSchemaRef)

const thenLocation = location.getPropertyLocation('then')
let thenMergedSchemaId = context.mergedSchemasIds.get(thenSchema)
Expand Down
25 changes: 22 additions & 3 deletions lib/standalone.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,28 @@
function buildStandaloneCode (contextFunc, context, serializer, validator) {
let ajvDependencyCode = ''
if (context.validatorSchemasIds.size > 0) {
ajvDependencyCode += 'const Validator = require(\'fast-json-stringify/lib/validator\')\n'
ajvDependencyCode += `const validatorState = ${JSON.stringify(validator.getState())}\n`
ajvDependencyCode += 'const validator = Validator.restoreFromState(validatorState)\n'
if (context.options.inlineValidators) {
const standaloneCode = require('ajv/dist/standalone').default
const schemaRefs = Object.fromEntries(
[...context.validatorSchemaRefs].map(schemaRef => [schemaRef, schemaRef])
)

ajvDependencyCode += `const validator = (() => {
const validators = {}
const exports = validators
${standaloneCode(validator.ajv, schemaRefs)}
return {
validate (schemaRef, data) {
const validate = validators[schemaRef]
return validate(data)
}
}
})()\n`
} else {
ajvDependencyCode += 'const Validator = require(\'fast-json-stringify/lib/validator\')\n'
ajvDependencyCode += `const validatorState = ${JSON.stringify(validator.getState())}\n`
ajvDependencyCode += 'const validator = Validator.restoreFromState(validatorState)\n'
}
} else {
ajvDependencyCode += 'const validator = null\n'
}
Expand Down
13 changes: 10 additions & 3 deletions lib/validator.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
'use strict'

const Ajv = require('ajv')
const { _ } = Ajv
const fastUri = require('fast-uri')
const ajvFormats = require('ajv-formats')
const clone = require('rfdc')({ proto: true })

class Validator {
constructor (ajvOptions) {
constructor (ajvOptions, inlineValidators) {
const codeOptions = inlineValidators
? { code: { ...ajvOptions?.code, source: true, esm: false } }
: {}

this.ajv = new Ajv({
...ajvOptions,
...codeOptions,
strictSchema: false,
validateSchema: false,
allowUnionTypes: true,
Expand All @@ -21,8 +27,9 @@ class Validator {
keyword: 'fjs_type',
type: 'object',
errors: false,
validate: (_type, data) => {
return data && typeof data.toJSON === 'function'
code (context) {
const { data } = context
context.fail(_`!${data} || typeof ${data}.toJSON !== "function"`)
}
})

Expand Down
111 changes: 109 additions & 2 deletions test/standalone-mode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ test('activate standalone mode', async (t) => {
})

test('test ajv schema', async (t) => {
t.plan(3)
t.plan(4)

after(async () => {
await fs.promises.rm(destination, { force: true })
})

const code = build({ mode: 'standalone' }, {
const code = build({ mode: 'standalone', inlineValidators: false }, {
type: 'object',
properties: {
},
Expand Down Expand Up @@ -94,6 +94,7 @@ test('test ajv schema', async (t) => {
})
t.assert.ok(typeof code === 'string')
t.assert.equal(code.indexOf('ajv') > 0, true)
t.assert.match(code, /fast-json-stringify\/lib\/validator/)

const destination = path.resolve(tmpDir, 'standalone2.js')

Expand Down Expand Up @@ -217,3 +218,109 @@ test('no need to keep external schemas once compiled - with oneOf validator', as
t.assert.equal(stringify({ oneOfSchema: { baz: 5 } }), '{"oneOfSchema":{"baz":5}}')
t.assert.equal(stringify({ oneOfSchema: { bar: 'foo' } }), '{"oneOfSchema":{"bar":"foo"}}')
})

test('inline validators with if/then/else', async (t) => {
t.plan(6)

after(async () => {
await fs.promises.rm(destination, { force: true })
})

const ajvOptions = {
code: { source: false, esm: true, lines: true }
}
const code = fjs({
type: 'object',
properties: {},
if: {
type: 'object',
properties: {
kind: { const: 'foo' }
},
required: ['kind']
},
then: {
properties: {
kind: { type: 'string' },
foo: { type: 'string' }
}
},
else: {
properties: {
kind: { type: 'string' },
bar: { type: 'integer' }
}
}
}, {
mode: 'standalone',
inlineValidators: true,
ajv: ajvOptions
})

t.assert.ok(typeof code === 'string')
t.assert.doesNotMatch(code, /fast-json-stringify\/lib\/validator/)
t.assert.match(code, /function validate\d/)
t.assert.deepEqual(ajvOptions, {
code: { source: false, esm: true, lines: true }
})

const destination = path.resolve(tmpDir, 'standalone-inline-if.js')

await fs.promises.writeFile(destination, code)
const stringify = require(destination)

t.assert.equal(stringify({ kind: 'foo', foo: 'FOO', bar: 42 }), '{"kind":"foo","foo":"FOO"}')
t.assert.equal(stringify({ kind: 'bar', foo: 'FOO', bar: 42 }), '{"kind":"bar","bar":42}')
})

test('inline validators with external oneOf refs and toJSON values', async (t) => {
t.plan(4)

after(async () => {
await fs.promises.rm(destination, { force: true })
})

const code = fjs({
type: 'object',
properties: {
value: {
oneOf: [
{ $ref: 'values#/definitions/timestamp' },
{ $ref: 'values#/definitions/count' }
]
}
}
}, {
mode: 'standalone',
inlineValidators: true,
schema: {
values: {
definitions: {
timestamp: { type: 'string', format: 'date-time' },
count: { type: 'integer' }
}
}
}
})

t.assert.doesNotMatch(code, /fast-json-stringify\/lib\/validator/)

const destination = path.resolve(tmpDir, 'standalone-inline-oneOf.js')

await fs.promises.writeFile(destination, code)
const stringify = require(destination)

t.assert.equal(
stringify({ value: new Date('2020-01-02T03:04:05.000Z') }),
'{"value":"2020-01-02T03:04:05.000Z"}'
)
t.assert.equal(stringify({ value: 42 }), '{"value":42}')
t.assert.throws(() => stringify({ value: true }), /does not match schema definition/)
})

test('does not emit unused inline validators', (t) => {
const code = build({ mode: 'standalone', inlineValidators: true })

t.assert.doesNotMatch(code, /function validate\d/)
t.assert.match(code, /const validator = null/)
})
6 changes: 6 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ declare namespace build {
* Running mode of fast-json-stringify
*/
mode?: 'debug' | 'standalone'
/**
* Embed Ajv-generated validators in standalone output instead of compiling schemas at runtime
*
* @default false
*/
inlineValidators?: boolean
/**
* Large arrays are defined as arrays containing, by default, `20000`
* elements or more. That value can be adjusted via the option parameter
Expand Down
4 changes: 4 additions & 0 deletions types/index.tst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ expect(build).type.not.toBeCallableWith({
type: 'number'
}, { rounding: 'invalid' })

build({} as Schema, { inlineValidators: true })
build({} as Schema, { inlineValidators: false })
expect(build).type.not.toBeCallableWith({} as Schema, { inlineValidators: 'true' })

// String schema
build({
type: 'string'
Expand Down
Loading