fast-json-stringify 7.0.0 → 7.1.0
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 +43 -2
- 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 +25 -8
- package/benchmark/bench-thread.js +6 -2
- package/benchmark/bench.js +86 -0
- package/index.js +360 -94
- 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 +6 -7
- package/test/additionalProperties.test.js +87 -0
- package/test/anyof.test.js +42 -0
- package/test/array.test.js +1 -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 +27 -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/patternProperties.test.js +24 -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/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.0",
|
|
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": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"contributors": [
|
|
31
31
|
{
|
|
32
32
|
"name": "Tomas Della Vedova",
|
|
33
|
-
"url": "
|
|
33
|
+
"url": "https://delvedor.dev"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"name": "Aras Abbasi",
|
|
@@ -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",
|
|
@@ -81,7 +80,7 @@
|
|
|
81
80
|
"@fastify/merge-json-schemas": "^0.2.0",
|
|
82
81
|
"ajv": "^8.12.0",
|
|
83
82
|
"ajv-formats": "^3.0.1",
|
|
84
|
-
"fast-uri": "^
|
|
83
|
+
"fast-uri": "^4.0.0",
|
|
85
84
|
"json-schema-ref-resolver": "^3.0.0",
|
|
86
85
|
"rfdc": "^1.2.0"
|
|
87
86
|
},
|
|
@@ -330,3 +330,90 @@ test('function and symbol references are not serialized as undefined', (t) => {
|
|
|
330
330
|
const obj = { str: 'x', test: 'test', meth: () => 'x', sym: Symbol('x') }
|
|
331
331
|
t.assert.equal(stringify(obj), '{"str":"x","test":"test"}')
|
|
332
332
|
})
|
|
333
|
+
|
|
334
|
+
test('required + additionalProperties without declared properties produces valid JSON', (t) => {
|
|
335
|
+
// Regression: when `required` is non-empty but `properties` is absent
|
|
336
|
+
// (e.g. a record-style schema with only `additionalProperties`), the
|
|
337
|
+
// serializer used to emit a stray leading comma:
|
|
338
|
+
// {"obj":{,"a":1,"b":2}}
|
|
339
|
+
// because the "skip first comma" optimization assumed a declared
|
|
340
|
+
// property would anchor it. With no `properties`, the additionalProperties
|
|
341
|
+
// branch would write the separator before its first entry.
|
|
342
|
+
t.plan(2)
|
|
343
|
+
const stringify = build({
|
|
344
|
+
type: 'object',
|
|
345
|
+
properties: {
|
|
346
|
+
obj: {
|
|
347
|
+
type: 'object',
|
|
348
|
+
propertyNames: { type: 'string', enum: ['a', 'b'] },
|
|
349
|
+
additionalProperties: { type: 'number' },
|
|
350
|
+
required: ['a', 'b']
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
const out = stringify({ obj: { a: 1, b: 2 } })
|
|
356
|
+
t.assert.equal(out, '{"obj":{"a":1,"b":2}}')
|
|
357
|
+
t.assert.deepStrictEqual(JSON.parse(out), { obj: { a: 1, b: 2 } })
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
test('required key not in properties + additionalProperties produces valid JSON', (t) => {
|
|
361
|
+
// Regression: declared `properties` is non-empty but `required` only lists
|
|
362
|
+
// keys that are not in `properties`. After sorting, propertiesKeys[0] is
|
|
363
|
+
// not required, so the "first declared property anchors the comma" premise
|
|
364
|
+
// does not hold. The serializer used to emit '{,"str":"x"}' for input
|
|
365
|
+
// missing the (non-required) declared `num`.
|
|
366
|
+
t.plan(2)
|
|
367
|
+
const stringify = build({
|
|
368
|
+
type: 'object',
|
|
369
|
+
properties: {
|
|
370
|
+
num: { type: 'number' }
|
|
371
|
+
},
|
|
372
|
+
additionalProperties: true,
|
|
373
|
+
required: ['str']
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
const out = stringify({ str: 'x' })
|
|
377
|
+
t.assert.equal(out, '{"str":"x"}')
|
|
378
|
+
t.assert.deepStrictEqual(JSON.parse(out), { str: 'x' })
|
|
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
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
|
+
})
|