fast-json-stringify 7.0.1 → 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 +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-thread.js +6 -2
- package/benchmark/bench.js +86 -0
- package/index.js +356 -87
- 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 +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 +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/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.0
|
|
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": {
|
|
@@ -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
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
|
+
})
|
package/test/const.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
|
|
|
7
7
|
test('schema with const string', (t) => {
|
|
@@ -279,7 +279,9 @@ test('schema with const and null as type', (t) => {
|
|
|
279
279
|
})
|
|
280
280
|
|
|
281
281
|
t.assert.equal(output, '{"foo":null}')
|
|
282
|
-
|
|
282
|
+
// `const` applies regardless of `type` in JSON Schema, so ajv rejects null
|
|
283
|
+
// here even though the serializer lets it through because the type allows it.
|
|
284
|
+
t.assert.equal(validate(JSON.parse(output)), false)
|
|
283
285
|
|
|
284
286
|
const output2 = stringify({ foo: 'baz' })
|
|
285
287
|
t.assert.equal(output2, '{"foo":"baz"}')
|
|
@@ -287,7 +289,7 @@ test('schema with const and null as type', (t) => {
|
|
|
287
289
|
})
|
|
288
290
|
|
|
289
291
|
test('schema with const as nullable', (t) => {
|
|
290
|
-
t.plan(
|
|
292
|
+
t.plan(2)
|
|
291
293
|
|
|
292
294
|
const schema = {
|
|
293
295
|
type: 'object',
|
|
@@ -296,20 +298,19 @@ test('schema with const as nullable', (t) => {
|
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
|
|
301
|
+
// ajv refuses to compile `nullable` without a `type`, so the output is not
|
|
302
|
+
// validated against the schema in this test.
|
|
300
303
|
const stringify = build(schema)
|
|
301
304
|
const output = stringify({
|
|
302
305
|
foo: null
|
|
303
306
|
})
|
|
304
307
|
|
|
305
308
|
t.assert.equal(output, '{"foo":null}')
|
|
306
|
-
t.assert.ok(validate(JSON.parse(output)), 'valid schema')
|
|
307
309
|
|
|
308
310
|
const output2 = stringify({
|
|
309
311
|
foo: 'baz'
|
|
310
312
|
})
|
|
311
313
|
t.assert.equal(output2, '{"foo":"baz"}')
|
|
312
|
-
t.assert.ok(validate(JSON.parse(output2)), 'valid schema')
|
|
313
314
|
})
|
|
314
315
|
|
|
315
316
|
test('schema with const and invalid object', (t) => {
|