fast-json-stringify 7.0.1 → 7.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +1 -0
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lock-threads.yml +1 -1
- package/.github/workflows/release.yml +24 -0
- package/README.md +20 -3
- package/benchmark/bench-cmp-lib.js +63 -38
- package/benchmark/bench-thread.js +8 -2
- package/benchmark/bench.js +86 -0
- package/benchmark/fixtures/api-responses.js +266 -0
- package/index.js +366 -98
- package/lib/location.js +3 -1
- package/lib/serializer.js +41 -37
- package/lib/standalone.js +22 -3
- package/lib/validator.js +22 -3
- package/package.json +4 -5
- package/test/additionalProperties.test.js +40 -0
- package/test/anyof.test.js +42 -0
- package/test/array.test.js +26 -1
- package/test/basic.test.js +1 -1
- package/test/code-generation-fallbacks.test.js +158 -0
- package/test/compile-validators.test.js +110 -0
- package/test/const.test.js +7 -6
- package/test/date.test.js +28 -1
- package/test/debug-mode.test.js +8 -0
- package/test/deopt.test.js +251 -0
- package/test/if-then-else.test.js +145 -0
- package/test/inferType.test.js +1 -1
- package/test/integer.test.js +1 -1
- package/test/oneof.test.js +242 -0
- package/test/optional-property-prefix.test.js +119 -0
- package/test/property-names.test.js +41 -0
- package/test/regex.test.js +1 -1
- package/test/schema-depth.test.js +227 -0
- package/test/standalone-mode.test.js +109 -2
- package/test/string.test.js +8 -0
- package/test/surrogate.test.js +1 -1
- package/test/typesArray.test.js +59 -0
- package/test/utils/validator.js +20 -0
- package/types/index.d.ts +20 -0
- package/types/index.tst.ts +13 -0
package/lib/location.js
CHANGED
|
@@ -8,10 +8,12 @@ class Location {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
getPropertyLocation (propertyName) {
|
|
11
|
+
// a JSON pointer segment, so a property name with a / or a ~ still points at the property
|
|
12
|
+
const segment = String(propertyName).replace(/~/g, '~0').replace(/\//g, '~1')
|
|
11
13
|
const propertyLocation = new Location(
|
|
12
14
|
this.schema[propertyName],
|
|
13
15
|
this.schemaId,
|
|
14
|
-
this.jsonPointer + '/' +
|
|
16
|
+
this.jsonPointer + '/' + segment
|
|
15
17
|
)
|
|
16
18
|
return propertyLocation
|
|
17
19
|
}
|
package/lib/serializer.js
CHANGED
|
@@ -3,6 +3,43 @@
|
|
|
3
3
|
// eslint-disable-next-line
|
|
4
4
|
const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/
|
|
5
5
|
|
|
6
|
+
function asString (str) {
|
|
7
|
+
const len = str.length
|
|
8
|
+
if (len === 0) {
|
|
9
|
+
return '""'
|
|
10
|
+
} else if (len < 42) {
|
|
11
|
+
// magically escape strings for json
|
|
12
|
+
// relying on their charCodeAt
|
|
13
|
+
// everything below 32 needs JSON.stringify()
|
|
14
|
+
// every string that contain surrogate needs JSON.stringify()
|
|
15
|
+
// 34 and 92 happens all the time, so we
|
|
16
|
+
// have a fast case for them
|
|
17
|
+
let result = ''
|
|
18
|
+
let last = -1
|
|
19
|
+
let point = 255
|
|
20
|
+
for (let i = 0; i < len; i++) {
|
|
21
|
+
point = str.charCodeAt(i)
|
|
22
|
+
if (
|
|
23
|
+
point === 0x22 || // '"'
|
|
24
|
+
point === 0x5c // '\'
|
|
25
|
+
) {
|
|
26
|
+
last === -1 && (last = 0)
|
|
27
|
+
result += str.slice(last, i) + '\\'
|
|
28
|
+
last = i
|
|
29
|
+
} else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
|
|
30
|
+
// The current character is non-printable characters or a surrogate.
|
|
31
|
+
return JSON.stringify(str)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
|
|
35
|
+
} else if (len < 5000 && STR_ESCAPE.test(str) === false) {
|
|
36
|
+
// Only use the regular expression for shorter input. The overhead is otherwise too much.
|
|
37
|
+
return '"' + str + '"'
|
|
38
|
+
} else {
|
|
39
|
+
return JSON.stringify(str)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
6
43
|
module.exports = class Serializer {
|
|
7
44
|
constructor (options) {
|
|
8
45
|
switch (options && options.rounding) {
|
|
@@ -63,7 +100,7 @@ module.exports = class Serializer {
|
|
|
63
100
|
return '"' + date.toISOString() + '"'
|
|
64
101
|
}
|
|
65
102
|
if (typeof date === 'string') {
|
|
66
|
-
return
|
|
103
|
+
return asString(date)
|
|
67
104
|
}
|
|
68
105
|
throw new Error(`The value "${date}" cannot be converted to a date-time.`)
|
|
69
106
|
}
|
|
@@ -74,7 +111,7 @@ module.exports = class Serializer {
|
|
|
74
111
|
return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + '"'
|
|
75
112
|
}
|
|
76
113
|
if (typeof date === 'string') {
|
|
77
|
-
return
|
|
114
|
+
return asString(date)
|
|
78
115
|
}
|
|
79
116
|
throw new Error(`The value "${date}" cannot be converted to a date.`)
|
|
80
117
|
}
|
|
@@ -85,46 +122,13 @@ module.exports = class Serializer {
|
|
|
85
122
|
return '"' + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + '"'
|
|
86
123
|
}
|
|
87
124
|
if (typeof date === 'string') {
|
|
88
|
-
return
|
|
125
|
+
return asString(date)
|
|
89
126
|
}
|
|
90
127
|
throw new Error(`The value "${date}" cannot be converted to a time.`)
|
|
91
128
|
}
|
|
92
129
|
|
|
93
130
|
asString (str) {
|
|
94
|
-
|
|
95
|
-
if (len === 0) {
|
|
96
|
-
return '""'
|
|
97
|
-
} else if (len < 42) {
|
|
98
|
-
// magically escape strings for json
|
|
99
|
-
// relying on their charCodeAt
|
|
100
|
-
// everything below 32 needs JSON.stringify()
|
|
101
|
-
// every string that contain surrogate needs JSON.stringify()
|
|
102
|
-
// 34 and 92 happens all the time, so we
|
|
103
|
-
// have a fast case for them
|
|
104
|
-
let result = ''
|
|
105
|
-
let last = -1
|
|
106
|
-
let point = 255
|
|
107
|
-
for (let i = 0; i < len; i++) {
|
|
108
|
-
point = str.charCodeAt(i)
|
|
109
|
-
if (
|
|
110
|
-
point === 0x22 || // '"'
|
|
111
|
-
point === 0x5c // '\'
|
|
112
|
-
) {
|
|
113
|
-
last === -1 && (last = 0)
|
|
114
|
-
result += str.slice(last, i) + '\\'
|
|
115
|
-
last = i
|
|
116
|
-
} else if (point < 32 || (point >= 0xD800 && point <= 0xDFFF)) {
|
|
117
|
-
// The current character is non-printable characters or a surrogate.
|
|
118
|
-
return JSON.stringify(str)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return (last === -1 && ('"' + str + '"')) || ('"' + result + str.slice(last) + '"')
|
|
122
|
-
} else if (len < 5000 && STR_ESCAPE.test(str) === false) {
|
|
123
|
-
// Only use the regular expression for shorter input. The overhead is otherwise too much.
|
|
124
|
-
return '"' + str + '"'
|
|
125
|
-
} else {
|
|
126
|
-
return JSON.stringify(str)
|
|
127
|
-
}
|
|
131
|
+
return asString(str)
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
asUnsafeString (str) {
|
package/lib/standalone.js
CHANGED
|
@@ -3,9 +3,28 @@
|
|
|
3
3
|
function buildStandaloneCode (contextFunc, context, serializer, validator) {
|
|
4
4
|
let ajvDependencyCode = ''
|
|
5
5
|
if (context.validatorSchemasIds.size > 0) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
}
|
|
9
28
|
} else {
|
|
10
29
|
ajvDependencyCode += 'const validator = null\n'
|
|
11
30
|
}
|
package/lib/validator.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const Ajv = require('ajv')
|
|
4
|
+
const { _ } = Ajv
|
|
4
5
|
const fastUri = require('fast-uri')
|
|
5
6
|
const ajvFormats = require('ajv-formats')
|
|
6
7
|
const clone = require('rfdc')({ proto: true })
|
|
7
8
|
|
|
8
9
|
class Validator {
|
|
9
|
-
constructor (ajvOptions) {
|
|
10
|
+
constructor (ajvOptions, inlineValidators) {
|
|
11
|
+
const codeOptions = inlineValidators
|
|
12
|
+
? { code: { ...ajvOptions?.code, source: true, esm: false } }
|
|
13
|
+
: {}
|
|
14
|
+
|
|
10
15
|
this.ajv = new Ajv({
|
|
11
16
|
...ajvOptions,
|
|
17
|
+
...codeOptions,
|
|
12
18
|
strictSchema: false,
|
|
13
19
|
validateSchema: false,
|
|
14
20
|
allowUnionTypes: true,
|
|
@@ -21,8 +27,9 @@ class Validator {
|
|
|
21
27
|
keyword: 'fjs_type',
|
|
22
28
|
type: 'object',
|
|
23
29
|
errors: false,
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
code (context) {
|
|
31
|
+
const { data } = context
|
|
32
|
+
context.fail(_`!${data} || typeof ${data}.toJSON !== "function"`)
|
|
26
33
|
}
|
|
27
34
|
})
|
|
28
35
|
|
|
@@ -47,6 +54,18 @@ class Validator {
|
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
// Ajv compiles a schema lazily, on the first `validate` call for a given ref.
|
|
58
|
+
// That cost would otherwise be paid at serialization time, on the first
|
|
59
|
+
// document that exercises the union/if-then-else branch. Eagerly resolving
|
|
60
|
+
// every ref the generated code can validate against moves it to build time.
|
|
61
|
+
// It must run after every `addSchema` call: compiling resolves $refs, so
|
|
62
|
+
// doing it earlier breaks on forward references.
|
|
63
|
+
compileSchemas (schemaRefs) {
|
|
64
|
+
for (const schemaRef of schemaRefs) {
|
|
65
|
+
this.ajv.getSchema(schemaRef)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
validate (schemaRef, data) {
|
|
51
70
|
return this.ajv.validate(schemaRef, data)
|
|
52
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-json-stringify",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.1",
|
|
4
4
|
"description": "Stringify your JSON at max speed",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"lint": "eslint",
|
|
14
14
|
"lint:fix": "eslint --fix",
|
|
15
15
|
"test:typescript": "tstyche",
|
|
16
|
-
"test:unit": "c8 node --test",
|
|
16
|
+
"test:unit": "c8 --100 --all --include=index.js --include=lib/**/*.js --exclude=lib/schema-validator.js node --test",
|
|
17
17
|
"test": "npm run test:unit && npm run test:typescript"
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
@@ -62,15 +62,14 @@
|
|
|
62
62
|
}
|
|
63
63
|
],
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"c8": "^
|
|
65
|
+
"c8": "^12.0.0",
|
|
66
66
|
"cli-select": "^1.1.2",
|
|
67
67
|
"compile-json-stringify": "^0.1.2",
|
|
68
68
|
"eslint": "^9.39.2",
|
|
69
69
|
"fast-json-stringify": ".",
|
|
70
|
-
"is-my-json-valid": "^2.20.6",
|
|
71
70
|
"json-accelerator": "^0.1.7",
|
|
72
71
|
"neostandard": "^0.13.0",
|
|
73
|
-
"simple-git": "^
|
|
72
|
+
"simple-git": "^4.0.2",
|
|
74
73
|
"tinybench": "^6.0.0",
|
|
75
74
|
"tstyche": "^7.1.0",
|
|
76
75
|
"typebox": "^1.0.81",
|
|
@@ -377,3 +377,43 @@ test('required key not in properties + additionalProperties produces valid JSON'
|
|
|
377
377
|
t.assert.equal(out, '{"str":"x"}')
|
|
378
378
|
t.assert.deepStrictEqual(JSON.parse(out), { str: 'x' })
|
|
379
379
|
})
|
|
380
|
+
|
|
381
|
+
test('additionalProperties set to false ignores properties not matched by patternProperties', (t) => {
|
|
382
|
+
// Regression: `additionalProperties: false` is documented to drop every
|
|
383
|
+
// property that is not listed in `properties` or matched by
|
|
384
|
+
// `patternProperties`. Combined with `patternProperties`, the generated
|
|
385
|
+
// code used to fall through to the additionalProperties branch with a
|
|
386
|
+
// boolean `false` schema, which serialized unmatched properties with
|
|
387
|
+
// `JSON.stringify(value)` and leaked them into the output:
|
|
388
|
+
// {"nickname":"nick","matchnum":3,"leaked":"secret"}
|
|
389
|
+
t.plan(2)
|
|
390
|
+
const stringify = build({
|
|
391
|
+
type: 'object',
|
|
392
|
+
properties: {
|
|
393
|
+
nickname: { type: 'string' }
|
|
394
|
+
},
|
|
395
|
+
patternProperties: {
|
|
396
|
+
num: { type: 'number' }
|
|
397
|
+
},
|
|
398
|
+
additionalProperties: false
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
const out = stringify({ nickname: 'nick', matchnum: 3, leaked: 'secret' })
|
|
402
|
+
t.assert.equal(out, '{"nickname":"nick","matchnum":3}')
|
|
403
|
+
t.assert.deepStrictEqual(JSON.parse(out), { nickname: 'nick', matchnum: 3 })
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test('additionalProperties set to false without declared properties ignores unmatched properties', (t) => {
|
|
407
|
+
t.plan(2)
|
|
408
|
+
const stringify = build({
|
|
409
|
+
type: 'object',
|
|
410
|
+
patternProperties: {
|
|
411
|
+
'^str': { type: 'string' }
|
|
412
|
+
},
|
|
413
|
+
additionalProperties: false
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
const out = stringify({ str1: 'a', leaked: 'secret' })
|
|
417
|
+
t.assert.equal(out, '{"str1":"a"}')
|
|
418
|
+
t.assert.deepStrictEqual(JSON.parse(out), { str1: 'a' })
|
|
419
|
+
})
|
package/test/anyof.test.js
CHANGED
|
@@ -387,6 +387,48 @@ test('anyOf and $ref - multiple external $ref', (t) => {
|
|
|
387
387
|
t.assert.equal(output, '{"obj":{"prop":{"prop2":"test"}}}')
|
|
388
388
|
})
|
|
389
389
|
|
|
390
|
+
test('anyOf with registered $ref branches validates directly against the ref target', (t) => {
|
|
391
|
+
t.plan(3)
|
|
392
|
+
|
|
393
|
+
const schema = {
|
|
394
|
+
title: 'anyOf with registered $ref branches',
|
|
395
|
+
type: 'object',
|
|
396
|
+
properties: {
|
|
397
|
+
content: {
|
|
398
|
+
anyOf: [
|
|
399
|
+
{ $ref: 'Foo#' },
|
|
400
|
+
{ $ref: 'Bar#/definitions/Baz' }
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const externalSchema = {
|
|
406
|
+
Foo: {
|
|
407
|
+
type: 'object',
|
|
408
|
+
properties: { a: { type: 'string' } },
|
|
409
|
+
required: ['a'],
|
|
410
|
+
additionalProperties: false
|
|
411
|
+
},
|
|
412
|
+
Bar: {
|
|
413
|
+
definitions: {
|
|
414
|
+
Baz: {
|
|
415
|
+
type: 'object',
|
|
416
|
+
properties: { b: { type: 'string' } },
|
|
417
|
+
required: ['b'],
|
|
418
|
+
additionalProperties: false
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const code = build(schema, { mode: 'standalone', schema: externalSchema })
|
|
425
|
+
t.assert.ok(code.includes('validator.validate("Foo#"'), 'validates directly against the bare registered id')
|
|
426
|
+
t.assert.ok(code.includes('validator.validate("Bar#/definitions/Baz"'), 'validates directly against the registered sub-path ref')
|
|
427
|
+
|
|
428
|
+
const stringify = build(schema, { schema: externalSchema })
|
|
429
|
+
t.assert.equal(stringify({ content: { b: 'yo' } }), '{"content":{"b":"yo"}}')
|
|
430
|
+
})
|
|
431
|
+
|
|
390
432
|
test('anyOf looks for all of the array items', (t) => {
|
|
391
433
|
t.plan(1)
|
|
392
434
|
|
package/test/array.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { test } = require('node:test')
|
|
4
|
-
const validator = require('
|
|
4
|
+
const validator = require('./utils/validator')
|
|
5
5
|
const build = require('..')
|
|
6
6
|
const Ajv = require('ajv')
|
|
7
7
|
|
|
@@ -23,6 +23,31 @@ test('error on invalid largeArrayMechanism', (t) => {
|
|
|
23
23
|
}), Error('Unsupported large array mechanism invalid'))
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
+
test('does not leak largeArrayMechanism across build() calls', (t) => {
|
|
27
|
+
t.plan(2)
|
|
28
|
+
|
|
29
|
+
const users = []
|
|
30
|
+
for (let i = 0; i < 100; i++) users.push({ name: 'user' + i, password: 'secret' + i })
|
|
31
|
+
|
|
32
|
+
const schema = {
|
|
33
|
+
type: 'array',
|
|
34
|
+
items: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
name: { type: 'string' }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// An unrelated build() opts into json-stringify for its own schema.
|
|
43
|
+
const optedIn = build({ type: 'array' }, { largeArrayMechanism: 'json-stringify', largeArraySize: 100 })
|
|
44
|
+
t.assert.equal(optedIn(users).includes('password'), true)
|
|
45
|
+
|
|
46
|
+
// A later build() without options must not inherit the mechanism.
|
|
47
|
+
const stringify = build(schema)
|
|
48
|
+
t.assert.equal(stringify(users).includes('password'), false)
|
|
49
|
+
})
|
|
50
|
+
|
|
26
51
|
function buildTest (schema, toStringify, options) {
|
|
27
52
|
test(`render a ${schema.title} as JSON`, (t) => {
|
|
28
53
|
t.plan(3)
|
package/test/basic.test.js
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
|
|
5
|
+
const OriginalLocation = require('../lib/location')
|
|
6
|
+
|
|
7
|
+
const buildPath = require.resolve('..')
|
|
8
|
+
const locationPath = require.resolve('../lib/location')
|
|
9
|
+
|
|
10
|
+
function loadBuildWithLocation (Location) {
|
|
11
|
+
require.cache[locationPath].exports = Location
|
|
12
|
+
delete require.cache[buildPath]
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
return require(buildPath)
|
|
16
|
+
} finally {
|
|
17
|
+
require.cache[locationPath].exports = OriginalLocation
|
|
18
|
+
delete require.cache[buildPath]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class LocationWithoutSchemaId {
|
|
23
|
+
constructor (schema, _schemaId, jsonPointer = '#') {
|
|
24
|
+
this.schema = schema
|
|
25
|
+
this.schemaId = ''
|
|
26
|
+
this.jsonPointer = _schemaId ? _schemaId + jsonPointer : jsonPointer
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getPropertyLocation (propertyName) {
|
|
30
|
+
return new LocationWithoutSchemaId(
|
|
31
|
+
this.schema[propertyName],
|
|
32
|
+
'',
|
|
33
|
+
this.jsonPointer + '/' + propertyName
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getSchemaRef () {
|
|
38
|
+
return this.schemaId + this.jsonPointer
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class LocationWithoutJsonPointer {
|
|
43
|
+
constructor (schema, schemaId) {
|
|
44
|
+
this.schema = schema
|
|
45
|
+
this.schemaId = schemaId
|
|
46
|
+
this.jsonPointer = ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getPropertyLocation (propertyName) {
|
|
50
|
+
return new LocationWithoutJsonPointer(this.schema[propertyName], this.schemaId)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getSchemaRef () {
|
|
54
|
+
return this.schemaId
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class LocationWithoutSchemaRef extends OriginalLocation {
|
|
59
|
+
getSchemaRef () {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('inline object generation without schema IDs', t => {
|
|
63
|
+
t.plan(3)
|
|
64
|
+
|
|
65
|
+
const build = loadBuildWithLocation(LocationWithoutSchemaId)
|
|
66
|
+
const stringify = build({
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
value: { type: 'string' }
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
const stringifyNullable = build({ type: 'object', nullable: true })
|
|
73
|
+
|
|
74
|
+
t.assert.equal(stringify({ value: 'test' }), '{"value":"test"}')
|
|
75
|
+
t.assert.equal(stringify(null), '{}')
|
|
76
|
+
t.assert.equal(stringifyNullable(null), 'null')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('inline array generation without schema IDs', t => {
|
|
80
|
+
t.plan(6)
|
|
81
|
+
|
|
82
|
+
const build = loadBuildWithLocation(LocationWithoutSchemaId)
|
|
83
|
+
const stringify = build({ type: 'array', items: { type: 'string' } }, { largeArrayMechanism: 'default' })
|
|
84
|
+
const stringifyNullable = build({ type: 'array', nullable: true })
|
|
85
|
+
const stringifyTuple = build({
|
|
86
|
+
type: 'array',
|
|
87
|
+
items: [{ $ref: 'item' }, { type: 'number' }],
|
|
88
|
+
additionalItems: true
|
|
89
|
+
}, {
|
|
90
|
+
schema: {
|
|
91
|
+
item: { type: 'string' }
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
const stringifyFixedTuple = build({
|
|
95
|
+
type: 'array',
|
|
96
|
+
items: [{ type: 'string' }],
|
|
97
|
+
additionalItems: false
|
|
98
|
+
})
|
|
99
|
+
const stringifyLargeArray = build({ type: 'array', items: { type: 'number' } }, {
|
|
100
|
+
largeArrayMechanism: 'json-stringify',
|
|
101
|
+
largeArraySize: 1
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
t.assert.equal(stringify(['one', 'two']), '["one","two"]')
|
|
105
|
+
t.assert.equal(stringifyNullable(null), 'null')
|
|
106
|
+
t.assert.equal(stringifyTuple(['one', 2, true]), '["one",2,true]')
|
|
107
|
+
t.assert.throws(() => stringifyFixedTuple(['one', 'two']), /Item at 1/)
|
|
108
|
+
t.assert.equal(stringifyLargeArray([1, 2]), '[1,2]')
|
|
109
|
+
t.assert.throws(() => stringify('not-an-array'), /does not match schema definition/)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('code generation reference fallbacks', t => {
|
|
113
|
+
t.plan(3)
|
|
114
|
+
|
|
115
|
+
const buildWithoutPointer = loadBuildWithLocation(LocationWithoutJsonPointer)
|
|
116
|
+
const stringifyObject = buildWithoutPointer({ type: 'object' })
|
|
117
|
+
const stringifyArray = buildWithoutPointer({ type: 'array' })
|
|
118
|
+
|
|
119
|
+
const buildWithoutRef = loadBuildWithLocation(LocationWithoutSchemaRef)
|
|
120
|
+
const stringifyWithoutRef = buildWithoutRef({ type: 'object' })
|
|
121
|
+
|
|
122
|
+
t.assert.equal(stringifyObject({}), '{}')
|
|
123
|
+
t.assert.equal(stringifyArray([]), '[]')
|
|
124
|
+
t.assert.equal(stringifyWithoutRef({}), '{}')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('required-property fallback tolerates unexpected property ordering', t => {
|
|
128
|
+
t.plan(2)
|
|
129
|
+
|
|
130
|
+
const originalSort = Array.prototype.sort
|
|
131
|
+
// Deliberately fault-inject unexpected ordering to exercise the defensive branch.
|
|
132
|
+
// eslint-disable-next-line no-extend-native
|
|
133
|
+
Array.prototype.sort = function (compareFn) {
|
|
134
|
+
const result = originalSort.call(this, compareFn)
|
|
135
|
+
if (this.length === 2 && this.includes('optional') && this.includes('required')) {
|
|
136
|
+
result.reverse()
|
|
137
|
+
}
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const build = loadBuildWithLocation(OriginalLocation)
|
|
143
|
+
const stringify = build({
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: {
|
|
146
|
+
optional: { type: 'string' },
|
|
147
|
+
required: { type: 'string' }
|
|
148
|
+
},
|
|
149
|
+
required: ['required']
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
t.assert.equal(stringify({ optional: 'one', required: 'two' }), '{"optional":"one","required":"two"}')
|
|
153
|
+
t.assert.throws(() => stringify({ optional: 'one' }), /"required" is required/)
|
|
154
|
+
} finally {
|
|
155
|
+
// eslint-disable-next-line no-extend-native
|
|
156
|
+
Array.prototype.sort = originalSort
|
|
157
|
+
}
|
|
158
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const fjs = require('..')
|
|
5
|
+
|
|
6
|
+
const schema = {
|
|
7
|
+
title: 'compile validators',
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
union: {
|
|
11
|
+
anyOf: [
|
|
12
|
+
{ type: 'string' },
|
|
13
|
+
{ type: 'number' }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
choice: {
|
|
17
|
+
oneOf: [
|
|
18
|
+
{ type: 'object', properties: { a: { type: 'string' } }, required: ['a'] },
|
|
19
|
+
{ type: 'object', properties: { b: { type: 'number' } }, required: ['b'] }
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
conditional: {
|
|
23
|
+
if: { type: 'string' },
|
|
24
|
+
then: { type: 'string' },
|
|
25
|
+
else: { type: 'number' }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The refs the generated code validates against, straight out of the emitted
|
|
31
|
+
// source, so the assertions cannot drift from what fjs actually calls.
|
|
32
|
+
function getValidatorRefs (code) {
|
|
33
|
+
return [...code.matchAll(/validator\.validate\("([^"]+)"/g)].map(match => match[1])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Ajv stores a compiled schema as a SchemaEnv with a `validate` function.
|
|
37
|
+
// `getSchema()` is not used here because it compiles on demand, which is the
|
|
38
|
+
// very thing under test.
|
|
39
|
+
function isCompiled (ajv, ref) {
|
|
40
|
+
const schemaEnv = ajv.refs[ref] || ajv.schemas[ref]
|
|
41
|
+
return schemaEnv !== undefined && typeof schemaEnv.validate === 'function'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('validators are compiled lazily by default', t => {
|
|
45
|
+
t.plan(2)
|
|
46
|
+
|
|
47
|
+
const { code, ajv } = fjs(schema, { mode: 'debug' })
|
|
48
|
+
const refs = getValidatorRefs(code)
|
|
49
|
+
|
|
50
|
+
t.assert.ok(refs.length > 0)
|
|
51
|
+
t.assert.deepStrictEqual(refs.filter(ref => isCompiled(ajv, ref)), [])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('compileValidators compiles every validator used by the generated code', t => {
|
|
55
|
+
t.plan(2)
|
|
56
|
+
|
|
57
|
+
const { code, ajv } = fjs(schema, { mode: 'debug', compileValidators: true })
|
|
58
|
+
const refs = getValidatorRefs(code)
|
|
59
|
+
|
|
60
|
+
t.assert.ok(refs.length > 0)
|
|
61
|
+
t.assert.deepStrictEqual(refs.filter(ref => !isCompiled(ajv, ref)), [])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('compileValidators does not change the serialization output', t => {
|
|
65
|
+
t.plan(2)
|
|
66
|
+
|
|
67
|
+
const input = {
|
|
68
|
+
union: 'foo',
|
|
69
|
+
choice: { b: 42 },
|
|
70
|
+
conditional: 'bar'
|
|
71
|
+
}
|
|
72
|
+
const expected = JSON.stringify({ union: 'foo', choice: { b: 42 }, conditional: 'bar' })
|
|
73
|
+
|
|
74
|
+
t.assert.equal(fjs(schema)(input), expected)
|
|
75
|
+
t.assert.equal(fjs(schema, { compileValidators: true })(input), expected)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('compileValidators works with external schemas resolved after registration', t => {
|
|
79
|
+
t.plan(3)
|
|
80
|
+
|
|
81
|
+
const externalSchema = {
|
|
82
|
+
branch: {
|
|
83
|
+
$id: 'branch',
|
|
84
|
+
definitions: {
|
|
85
|
+
str: { type: 'string' },
|
|
86
|
+
num: { type: 'number' }
|
|
87
|
+
},
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
value: {
|
|
91
|
+
anyOf: [
|
|
92
|
+
{ $ref: 'branch#/definitions/str' },
|
|
93
|
+
{ $ref: 'branch#/definitions/num' }
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const rootSchema = { $ref: 'branch#' }
|
|
101
|
+
const options = { schema: externalSchema, compileValidators: true }
|
|
102
|
+
|
|
103
|
+
const { code, ajv } = fjs(rootSchema, { ...options, mode: 'debug' })
|
|
104
|
+
const refs = getValidatorRefs(code)
|
|
105
|
+
|
|
106
|
+
t.assert.ok(refs.length > 0)
|
|
107
|
+
t.assert.deepStrictEqual(refs.filter(ref => !isCompiled(ajv, ref)), [])
|
|
108
|
+
|
|
109
|
+
t.assert.equal(fjs(rootSchema, options)({ value: 7 }), JSON.stringify({ value: 7 }))
|
|
110
|
+
})
|