Skip to content

Commit e02b5bb

Browse files
RelevantShannonTony133
authored andcommitted
feat: inline validators in standalone output
1 parent b926f53 commit e02b5bb

7 files changed

Lines changed: 165 additions & 10 deletions

File tree

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ const stringify = fastJson(mySchema, {
128128
- `schema`: external schemas references by $ref property. [More details](#ref)
129129
- `ajv`: [ajv v8 instance's settings](https://ajv.js.org/options.html) for those properties that require `ajv`. [More details](#anyof)
130130
- `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer)
131+
- `inlineValidators`: when using standalone mode, embed Ajv-generated validator functions in the output instead of compiling schemas at runtime. [More details](#standalone)
131132
- `largeArrayMechanism`: set the mechanism that should be used to handle large
132133
(by default `20000` or more items) arrays. [More details](#largearrays)
133134
- `compileValidators`: when `true`, the `ajv` validators used by `anyOf`, `oneOf` and
@@ -728,13 +729,21 @@ const code = fastJson({
728729
type: 'string'
729730
}
730731
}
731-
}, { mode: 'standalone' })
732+
}, { mode: 'standalone', inlineValidators: true })
732733

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

739+
Set `inlineValidators` to `true` to include the Ajv-generated functions used by
740+
`anyOf`, `oneOf`, and `if/then/else` in the same output file. This avoids
741+
rebuilding an Ajv instance and compiling their schemas at runtime. The
742+
generated module still requires `fast-json-stringify` for its serializer and
743+
may require Ajv runtime helpers used by the generated validation code. Custom
744+
Ajv formats used by these schemas must support Ajv's standalone code
745+
generation.
746+
738747
<a name="acknowledgments"></a>
739748
## Acknowledgments
740749

index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,10 @@ function build (schema, options) {
256256
}
257257

258258
const serializer = new Serializer(options)
259-
const validator = new Validator(options.ajv)
259+
const validator = new Validator(
260+
options.ajv,
261+
options.mode === 'standalone' && options.inlineValidators
262+
)
260263

261264
for (const schemaId of context.validatorSchemasIds) {
262265
const schema = context.refResolver.getSchema(schemaId)

lib/standalone.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,28 @@
33
function buildStandaloneCode (contextFunc, context, serializer, validator) {
44
let ajvDependencyCode = ''
55
if (context.validatorSchemasIds.size > 0) {
6-
ajvDependencyCode += 'const Validator = require(\'fast-json-stringify/lib/validator\')\n'
7-
ajvDependencyCode += `const validatorState = ${JSON.stringify(validator.getState())}\n`
8-
ajvDependencyCode += 'const validator = Validator.restoreFromState(validatorState)\n'
6+
if (context.options.inlineValidators) {
7+
const standaloneCode = require('ajv/dist/standalone').default
8+
const schemaRefs = Object.fromEntries(
9+
[...context.validatorSchemaRefs].map(schemaRef => [schemaRef, schemaRef])
10+
)
11+
12+
ajvDependencyCode += `const validator = (() => {
13+
const validators = {}
14+
const exports = validators
15+
${standaloneCode(validator.ajv, schemaRefs)}
16+
return {
17+
validate (schemaRef, data) {
18+
const validate = validators[schemaRef]
19+
return validate(data)
20+
}
21+
}
22+
})()\n`
23+
} else {
24+
ajvDependencyCode += 'const Validator = require(\'fast-json-stringify/lib/validator\')\n'
25+
ajvDependencyCode += `const validatorState = ${JSON.stringify(validator.getState())}\n`
26+
ajvDependencyCode += 'const validator = Validator.restoreFromState(validatorState)\n'
27+
}
928
} else {
1029
ajvDependencyCode += 'const validator = null\n'
1130
}

lib/validator.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
'use strict'
22

33
const Ajv = require('ajv')
4+
const { _ } = Ajv
45
const fastUri = require('fast-uri')
56
const ajvFormats = require('ajv-formats')
67
const clone = require('rfdc')({ proto: true })
78

89
class Validator {
9-
constructor (ajvOptions) {
10+
constructor (ajvOptions, inlineValidators) {
11+
const codeOptions = inlineValidators
12+
? { code: { ...ajvOptions?.code, source: true, esm: false } }
13+
: {}
14+
1015
this.ajv = new Ajv({
1116
...ajvOptions,
17+
...codeOptions,
1218
strictSchema: false,
1319
validateSchema: false,
1420
allowUnionTypes: true,
@@ -21,8 +27,9 @@ class Validator {
2127
keyword: 'fjs_type',
2228
type: 'object',
2329
errors: false,
24-
validate: (_type, data) => {
25-
return data && typeof data.toJSON === 'function'
30+
code (context) {
31+
const { data } = context
32+
context.fail(_`!${data} || typeof ${data}.toJSON !== "function"`)
2633
}
2734
})
2835

test/standalone-mode.test.js

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ test('activate standalone mode', async (t) => {
3939
})
4040

4141
test('test ajv schema', async (t) => {
42-
t.plan(3)
42+
t.plan(4)
4343

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

48-
const code = build({ mode: 'standalone' }, {
48+
const code = build({ mode: 'standalone', inlineValidators: false }, {
4949
type: 'object',
5050
properties: {
5151
},
@@ -94,6 +94,7 @@ test('test ajv schema', async (t) => {
9494
})
9595
t.assert.ok(typeof code === 'string')
9696
t.assert.equal(code.indexOf('ajv') > 0, true)
97+
t.assert.match(code, /fast-json-stringify\/lib\/validator/)
9798

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

@@ -217,3 +218,109 @@ test('no need to keep external schemas once compiled - with oneOf validator', as
217218
t.assert.equal(stringify({ oneOfSchema: { baz: 5 } }), '{"oneOfSchema":{"baz":5}}')
218219
t.assert.equal(stringify({ oneOfSchema: { bar: 'foo' } }), '{"oneOfSchema":{"bar":"foo"}}')
219220
})
221+
222+
test('inline validators with if/then/else', async (t) => {
223+
t.plan(6)
224+
225+
after(async () => {
226+
await fs.promises.rm(destination, { force: true })
227+
})
228+
229+
const ajvOptions = {
230+
code: { source: false, esm: true, lines: true }
231+
}
232+
const code = fjs({
233+
type: 'object',
234+
properties: {},
235+
if: {
236+
type: 'object',
237+
properties: {
238+
kind: { const: 'foo' }
239+
},
240+
required: ['kind']
241+
},
242+
then: {
243+
properties: {
244+
kind: { type: 'string' },
245+
foo: { type: 'string' }
246+
}
247+
},
248+
else: {
249+
properties: {
250+
kind: { type: 'string' },
251+
bar: { type: 'integer' }
252+
}
253+
}
254+
}, {
255+
mode: 'standalone',
256+
inlineValidators: true,
257+
ajv: ajvOptions
258+
})
259+
260+
t.assert.ok(typeof code === 'string')
261+
t.assert.doesNotMatch(code, /fast-json-stringify\/lib\/validator/)
262+
t.assert.match(code, /function validate\d/)
263+
t.assert.deepEqual(ajvOptions, {
264+
code: { source: false, esm: true, lines: true }
265+
})
266+
267+
const destination = path.resolve(tmpDir, 'standalone-inline-if.js')
268+
269+
await fs.promises.writeFile(destination, code)
270+
const stringify = require(destination)
271+
272+
t.assert.equal(stringify({ kind: 'foo', foo: 'FOO', bar: 42 }), '{"kind":"foo","foo":"FOO"}')
273+
t.assert.equal(stringify({ kind: 'bar', foo: 'FOO', bar: 42 }), '{"kind":"bar","bar":42}')
274+
})
275+
276+
test('inline validators with external oneOf refs and toJSON values', async (t) => {
277+
t.plan(4)
278+
279+
after(async () => {
280+
await fs.promises.rm(destination, { force: true })
281+
})
282+
283+
const code = fjs({
284+
type: 'object',
285+
properties: {
286+
value: {
287+
oneOf: [
288+
{ $ref: 'values#/definitions/timestamp' },
289+
{ $ref: 'values#/definitions/count' }
290+
]
291+
}
292+
}
293+
}, {
294+
mode: 'standalone',
295+
inlineValidators: true,
296+
schema: {
297+
values: {
298+
definitions: {
299+
timestamp: { type: 'string', format: 'date-time' },
300+
count: { type: 'integer' }
301+
}
302+
}
303+
}
304+
})
305+
306+
t.assert.doesNotMatch(code, /fast-json-stringify\/lib\/validator/)
307+
308+
const destination = path.resolve(tmpDir, 'standalone-inline-oneOf.js')
309+
310+
await fs.promises.writeFile(destination, code)
311+
const stringify = require(destination)
312+
313+
t.assert.equal(
314+
stringify({ value: new Date('2020-01-02T03:04:05.000Z') }),
315+
'{"value":"2020-01-02T03:04:05.000Z"}'
316+
)
317+
t.assert.equal(stringify({ value: 42 }), '{"value":42}')
318+
t.assert.throws(() => stringify({ value: true }), /does not match schema definition/)
319+
})
320+
321+
test('does not emit unused inline validators', (t) => {
322+
const code = build({ mode: 'standalone', inlineValidators: true })
323+
324+
t.assert.doesNotMatch(code, /function validate\d/)
325+
t.assert.match(code, /const validator = null/)
326+
})

types/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ declare namespace build {
166166
* Running mode of fast-json-stringify
167167
*/
168168
mode?: 'debug' | 'standalone'
169+
/**
170+
* Embed Ajv-generated validators in standalone output instead of compiling schemas at runtime
171+
*
172+
* @default false
173+
*/
174+
inlineValidators?: boolean
169175
/**
170176
* Large arrays are defined as arrays containing, by default, `20000`
171177
* elements or more. That value can be adjusted via the option parameter

types/index.tst.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ expect(build).type.not.toBeCallableWith({
3131
type: 'number'
3232
}, { rounding: 'invalid' })
3333

34+
build({} as Schema, { inlineValidators: true })
35+
build({} as Schema, { inlineValidators: false })
36+
expect(build).type.not.toBeCallableWith({} as Schema, { inlineValidators: 'true' })
37+
3438
// String schema
3539
build({
3640
type: 'string'

0 commit comments

Comments
 (0)