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/test/oneof.test.js
CHANGED
|
@@ -348,6 +348,248 @@ test('oneOf and $ref - multiple external $ref', (t) => {
|
|
|
348
348
|
t.assert.equal(output, '{"obj":{"prop":{"prop2":"test"}}}')
|
|
349
349
|
})
|
|
350
350
|
|
|
351
|
+
test('oneOf with a bare top-level $ref branch validates directly against the registered schema, not a root-relative pointer', (t) => {
|
|
352
|
+
t.plan(3)
|
|
353
|
+
|
|
354
|
+
const schema = {
|
|
355
|
+
title: 'oneOf with bare $ref branches',
|
|
356
|
+
type: 'object',
|
|
357
|
+
properties: {
|
|
358
|
+
content: {
|
|
359
|
+
oneOf: [
|
|
360
|
+
{ $ref: 'Foo#' },
|
|
361
|
+
{ $ref: 'Bar#' }
|
|
362
|
+
]
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const externalSchema = {
|
|
367
|
+
Foo: {
|
|
368
|
+
type: 'object',
|
|
369
|
+
properties: { a: { type: 'string' } },
|
|
370
|
+
required: ['a'],
|
|
371
|
+
additionalProperties: false
|
|
372
|
+
},
|
|
373
|
+
Bar: {
|
|
374
|
+
type: 'object',
|
|
375
|
+
properties: { b: { type: 'string' } },
|
|
376
|
+
required: ['b'],
|
|
377
|
+
additionalProperties: false
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const code = build(schema, { mode: 'standalone', schema: externalSchema })
|
|
382
|
+
t.assert.ok(code.includes('validator.validate("Foo#"'), 'validates directly against the bare registered id')
|
|
383
|
+
t.assert.ok(code.includes('validator.validate("Bar#"'), 'validates directly against the bare registered id')
|
|
384
|
+
|
|
385
|
+
const stringify = build(schema, { schema: externalSchema })
|
|
386
|
+
t.assert.equal(stringify({ content: { b: 'yo' } }), '{"content":{"b":"yo"}}')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('oneOf with a bare top-level $ref branch to a $id schema validates directly against the registered schema', (t) => {
|
|
390
|
+
t.plan(2)
|
|
391
|
+
|
|
392
|
+
const schema = {
|
|
393
|
+
title: 'oneOf with bare $id $ref branch',
|
|
394
|
+
type: 'object',
|
|
395
|
+
properties: {
|
|
396
|
+
content: {
|
|
397
|
+
oneOf: [
|
|
398
|
+
{ $ref: 'urn:fjs:test:Foo#' },
|
|
399
|
+
{ type: 'null' }
|
|
400
|
+
]
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const externalSchema = {
|
|
405
|
+
Foo: {
|
|
406
|
+
$id: 'urn:fjs:test:Foo',
|
|
407
|
+
type: 'object',
|
|
408
|
+
properties: { a: { type: 'string' } },
|
|
409
|
+
required: ['a'],
|
|
410
|
+
additionalProperties: false
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const code = build(schema, { mode: 'standalone', schema: externalSchema })
|
|
415
|
+
t.assert.ok(code.includes('validator.validate("urn:fjs:test:Foo#"'), 'validates directly against the bare registered $id')
|
|
416
|
+
|
|
417
|
+
const stringify = build(schema, { schema: externalSchema })
|
|
418
|
+
t.assert.equal(stringify({ content: { a: 'yo' } }), '{"content":{"a":"yo"}}')
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
test('oneOf with a bare top-level $ref branch to a relative $id schema is left unchanged', (t) => {
|
|
422
|
+
t.plan(1)
|
|
423
|
+
|
|
424
|
+
const schema = {
|
|
425
|
+
title: 'oneOf with bare $ref branch to relative $id',
|
|
426
|
+
type: 'object',
|
|
427
|
+
properties: {
|
|
428
|
+
content: {
|
|
429
|
+
oneOf: [
|
|
430
|
+
{ $ref: 'Foo#' },
|
|
431
|
+
{ type: 'null' }
|
|
432
|
+
]
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const externalSchema = {
|
|
437
|
+
Foo: {
|
|
438
|
+
$id: '#foo',
|
|
439
|
+
type: 'object',
|
|
440
|
+
properties: { a: { type: 'string' } },
|
|
441
|
+
required: ['a']
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const code = build(schema, { mode: 'standalone', schema: externalSchema })
|
|
446
|
+
t.assert.ok(!code.includes('validator.validate("Foo#"'), 'relative $id schema is not redirected to the bare parent id')
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
test('oneOf with a $ref branch to a relative root $id validates directly against the registered schema', (t) => {
|
|
450
|
+
t.plan(2)
|
|
451
|
+
|
|
452
|
+
const schema = {
|
|
453
|
+
title: 'oneOf with $ref branch to relative root $id',
|
|
454
|
+
type: 'object',
|
|
455
|
+
properties: {
|
|
456
|
+
content: {
|
|
457
|
+
oneOf: [
|
|
458
|
+
{ $ref: 'Foo#foo' },
|
|
459
|
+
{ type: 'null' }
|
|
460
|
+
]
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const externalSchema = {
|
|
465
|
+
Foo: {
|
|
466
|
+
$id: '#foo',
|
|
467
|
+
type: 'object',
|
|
468
|
+
properties: { a: { type: 'string' } },
|
|
469
|
+
required: ['a']
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const code = build(schema, { mode: 'standalone', schema: externalSchema })
|
|
474
|
+
t.assert.ok(code.includes('validator.validate("Foo#foo"'), 'validates directly against the relative root $id')
|
|
475
|
+
|
|
476
|
+
const stringify = build(schema, { schema: externalSchema })
|
|
477
|
+
t.assert.equal(stringify({ content: { a: 'yo' } }), '{"content":{"a":"yo"}}')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
test('oneOf with a named-fragment $ref branch validates directly against the registered schema anchor', (t) => {
|
|
481
|
+
t.plan(2)
|
|
482
|
+
|
|
483
|
+
const schema = {
|
|
484
|
+
title: 'oneOf with named-fragment $ref branch',
|
|
485
|
+
type: 'object',
|
|
486
|
+
properties: {
|
|
487
|
+
content: {
|
|
488
|
+
oneOf: [
|
|
489
|
+
{ $ref: 'Foo#tag' },
|
|
490
|
+
{ type: 'null' }
|
|
491
|
+
]
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const externalSchema = {
|
|
496
|
+
Foo: {
|
|
497
|
+
$id: 'Foo',
|
|
498
|
+
$defs: {
|
|
499
|
+
tag: { $id: '#tag', type: 'string' }
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const code = build(schema, { mode: 'standalone', schema: externalSchema })
|
|
505
|
+
t.assert.ok(code.includes('validator.validate("Foo#tag"'), 'validates directly against the registered schema anchor')
|
|
506
|
+
|
|
507
|
+
const stringify = build(schema, { schema: externalSchema })
|
|
508
|
+
t.assert.equal(stringify({ content: 'yo' }), '{"content":"yo"}')
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
test('oneOf with a $ref into a JSON-pointer sub-path validates directly against that sub-path', (t) => {
|
|
512
|
+
t.plan(2)
|
|
513
|
+
|
|
514
|
+
const schema = {
|
|
515
|
+
title: 'oneOf with a sub-path $ref branch',
|
|
516
|
+
type: 'object',
|
|
517
|
+
properties: {
|
|
518
|
+
content: {
|
|
519
|
+
oneOf: [
|
|
520
|
+
{ $ref: 'Foo#/definitions/Bar' },
|
|
521
|
+
{ type: 'null' }
|
|
522
|
+
]
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const externalSchema = {
|
|
527
|
+
Foo: {
|
|
528
|
+
definitions: {
|
|
529
|
+
Bar: { type: 'object', properties: { b: { type: 'string' } } }
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const code = build(schema, { mode: 'standalone', schema: externalSchema })
|
|
535
|
+
t.assert.ok(code.includes('validator.validate("Foo#/definitions/Bar"'), 'validates directly against the sub-path ref')
|
|
536
|
+
|
|
537
|
+
const stringify = build(schema, { schema: externalSchema })
|
|
538
|
+
t.assert.equal(stringify({ content: { b: 'yo' } }), '{"content":{"b":"yo"}}')
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
test('oneOf with a $ref branch that has sibling keywords is left unchanged', (t) => {
|
|
542
|
+
t.plan(2)
|
|
543
|
+
|
|
544
|
+
const schema = {
|
|
545
|
+
title: 'oneOf with a $ref branch with sibling keywords',
|
|
546
|
+
type: 'object',
|
|
547
|
+
properties: {
|
|
548
|
+
content: {
|
|
549
|
+
oneOf: [
|
|
550
|
+
{ $ref: 'Foo#', required: ['c'] },
|
|
551
|
+
{ type: 'null' }
|
|
552
|
+
]
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const externalSchema = {
|
|
557
|
+
Foo: {
|
|
558
|
+
type: 'object',
|
|
559
|
+
properties: { a: { type: 'string' } },
|
|
560
|
+
required: ['a']
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const code = build(schema, { mode: 'standalone', schema: externalSchema })
|
|
565
|
+
t.assert.ok(!code.includes('validator.validate("Foo#"'), 'ref with sibling keywords is not redirected')
|
|
566
|
+
|
|
567
|
+
const stringify = build(schema, { schema: externalSchema })
|
|
568
|
+
t.assert.throws(() => stringify({ content: { a: 'yo' } }))
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
test('oneOf with an inline (non-$ref) branch is left unchanged', (t) => {
|
|
572
|
+
t.plan(1)
|
|
573
|
+
|
|
574
|
+
const schema = {
|
|
575
|
+
title: 'oneOf with an inline branch, no $ref',
|
|
576
|
+
type: 'object',
|
|
577
|
+
properties: {
|
|
578
|
+
content: {
|
|
579
|
+
oneOf: [
|
|
580
|
+
{ type: 'string' },
|
|
581
|
+
{ type: 'object', properties: { a: { type: 'string' } }, required: ['a'] }
|
|
582
|
+
]
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Should compile and serialize exactly as before -- this is a pure
|
|
588
|
+
// regression guard, not exercising the new code path at all.
|
|
589
|
+
const stringify = build(schema)
|
|
590
|
+
t.assert.equal(stringify({ content: 'hello' }), '{"content":"hello"}')
|
|
591
|
+
})
|
|
592
|
+
|
|
351
593
|
test('oneOf with enum with more than 100 entries', (t) => {
|
|
352
594
|
t.plan(1)
|
|
353
595
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
function compile (schema, mode) {
|
|
7
|
+
if (mode === 'normal') return build(schema)
|
|
8
|
+
const output = { exports: {} }
|
|
9
|
+
// eslint-disable-next-line no-new-func
|
|
10
|
+
new Function('require', 'module', build(schema, { mode: 'standalone' }))(require, output)
|
|
11
|
+
return output.exports
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const mode of ['normal', 'standalone']) {
|
|
15
|
+
test('optional property prefixes preserve omissions and escaped keys: ' + mode, t => {
|
|
16
|
+
const keys = ['first', 'quote"', 'back\\slash', 'line\nbreak']
|
|
17
|
+
const properties = Object.fromEntries(keys.map(key => [key, { type: 'integer' }]))
|
|
18
|
+
const stringify = compile({ type: 'object', properties, additionalProperties: false }, mode)
|
|
19
|
+
|
|
20
|
+
for (let mask = 0; mask < 16; mask++) {
|
|
21
|
+
const input = { ignored: true }
|
|
22
|
+
const expected = {}
|
|
23
|
+
for (let i = keys.length - 1; i >= 0; i--) {
|
|
24
|
+
input[keys[i]] = mask & (1 << i) ? i : undefined
|
|
25
|
+
}
|
|
26
|
+
for (let i = 0; i < keys.length; i++) {
|
|
27
|
+
if (mask & (1 << i)) expected[keys[i]] = i
|
|
28
|
+
}
|
|
29
|
+
t.assert.equal(stringify(input), JSON.stringify(expected))
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('optional property prefixes share comma state with defaults and extra properties: ' + mode, t => {
|
|
34
|
+
const stringify = compile({
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
first: { type: 'string' },
|
|
38
|
+
middle: { type: 'string', default: 'fallback' },
|
|
39
|
+
last: { type: 'boolean' }
|
|
40
|
+
},
|
|
41
|
+
patternProperties: { '^x': { type: 'integer' } },
|
|
42
|
+
additionalProperties: { type: 'string' }
|
|
43
|
+
}, mode)
|
|
44
|
+
|
|
45
|
+
t.assert.equal(stringify({}), '{"middle":"fallback"}')
|
|
46
|
+
t.assert.equal(stringify({ first: 'a', last: false, x: 2, extra: 'b' }), '{"first":"a","middle":"fallback","last":false,"x":2,"extra":"b"}')
|
|
47
|
+
t.assert.equal(stringify({ middle: 'b', extra: 'c' }), '{"middle":"b","extra":"c"}')
|
|
48
|
+
|
|
49
|
+
const noDefaults = compile({
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: { first: { type: 'string' } },
|
|
52
|
+
additionalProperties: true
|
|
53
|
+
}, mode)
|
|
54
|
+
t.assert.equal(noDefaults({ extra: 1 }), '{"extra":1}')
|
|
55
|
+
t.assert.equal(noDefaults({ first: 'a', extra: 1 }), '{"first":"a","extra":1}')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('optional property prefixes reset for nested objects and array items: ' + mode, t => {
|
|
59
|
+
const item = {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: { first: { type: 'integer' }, last: { type: 'integer' } }
|
|
62
|
+
}
|
|
63
|
+
const stringify = compile({
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
before: item,
|
|
67
|
+
items: { type: 'array', items: item },
|
|
68
|
+
after: item
|
|
69
|
+
}
|
|
70
|
+
}, mode)
|
|
71
|
+
const input = {
|
|
72
|
+
before: {},
|
|
73
|
+
items: [{ last: 1 }, {}, { first: 2, last: 3 }, { first: 4 }],
|
|
74
|
+
after: { last: 5 }
|
|
75
|
+
}
|
|
76
|
+
t.assert.equal(stringify(input), JSON.stringify(input))
|
|
77
|
+
t.assert.equal(stringify({ after: { first: 6 } }), '{"after":{"first":6}}')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('optional property prefixes preserve getter and coercion order: ' + mode, t => {
|
|
81
|
+
const stringify = compile({
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
first: { type: 'string' },
|
|
85
|
+
second: { type: 'integer' },
|
|
86
|
+
last: { type: 'boolean' }
|
|
87
|
+
}
|
|
88
|
+
}, mode)
|
|
89
|
+
const events = []
|
|
90
|
+
const failure = new Error('coercion failed')
|
|
91
|
+
let fail = false
|
|
92
|
+
const input = {
|
|
93
|
+
get first () {
|
|
94
|
+
events.push('first')
|
|
95
|
+
return {
|
|
96
|
+
toString () {
|
|
97
|
+
events.push('coerce first')
|
|
98
|
+
if (fail) throw failure
|
|
99
|
+
return 'a'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
get second () {
|
|
104
|
+
events.push('second')
|
|
105
|
+
return 2
|
|
106
|
+
},
|
|
107
|
+
get last () {
|
|
108
|
+
events.push('last')
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
t.assert.equal(stringify(input), '{"first":"a","second":2}')
|
|
113
|
+
t.assert.deepStrictEqual(events, ['first', 'coerce first', 'second', 'last'])
|
|
114
|
+
events.length = 0
|
|
115
|
+
fail = true
|
|
116
|
+
t.assert.throws(() => stringify(input), error => error === failure)
|
|
117
|
+
t.assert.deepStrictEqual(events, ['first', 'coerce first'])
|
|
118
|
+
})
|
|
119
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
// the schema ref of a property with anyOf, oneOf or if goes into the generated code as a
|
|
7
|
+
// string and into a JSON pointer, so a name that needs escaping in either broke the build
|
|
8
|
+
const names = ['new\nline', 'q"uote', 'back\\slash', 'a/b', 'a~b', 'a%b', 'a#b', 'a b', 'tick`', 'dollar$' + '{x}', '\\u', "single'quote"]
|
|
9
|
+
const branches = {
|
|
10
|
+
oneOf: { oneOf: [{ type: 'string' }, { type: 'object', properties: { k: { type: 'integer' } }, required: ['k'] }] },
|
|
11
|
+
anyOf: { anyOf: [{ type: 'string' }, { type: 'object', properties: { k: { type: 'integer' } }, required: ['k'] }] },
|
|
12
|
+
if: { if: { type: 'string' }, then: { type: 'string' }, else: { type: 'integer' } }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for (const name of names) {
|
|
16
|
+
for (const [keyword, subschema] of Object.entries(branches)) {
|
|
17
|
+
test(`a property named ${JSON.stringify(name)} with ${keyword}`, (t) => {
|
|
18
|
+
t.plan(2)
|
|
19
|
+
|
|
20
|
+
const stringify = build({ type: 'object', properties: { [name]: subschema } })
|
|
21
|
+
t.assert.equal(stringify({ [name]: 'v' }), JSON.stringify({ [name]: 'v' }))
|
|
22
|
+
t.assert.throws(() => stringify({ [name]: {} }), keyword === 'if' ? Error : new TypeError(`The value of '#/properties/${name.replace(/~/g, '~0').replace(/\//g, '~1')}' does not match schema definition.`))
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// the ref also goes on a comment line of the generated code, above the function of a nested
|
|
28
|
+
// object or array, where a line terminator in the name ends the comment early
|
|
29
|
+
for (const name of ['new' + String.fromCharCode(10) + 'line', 'line' + String.fromCharCode(0x2028) + 'sep', 'para' + String.fromCharCode(0x2029) + 'graph']) {
|
|
30
|
+
test(`a property named ${JSON.stringify(name)} with a nested object and array`, (t) => {
|
|
31
|
+
t.plan(1)
|
|
32
|
+
|
|
33
|
+
const stringify = build({
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
[name]: { type: 'object', properties: { list: { type: 'array', items: { type: 'integer' } } }, nullable: true }
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
t.assert.equal(stringify({ [name]: { list: [1] } }), JSON.stringify({ [name]: { list: [1] } }))
|
|
40
|
+
})
|
|
41
|
+
}
|
package/test/regex.test.js
CHANGED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const build = require('..')
|
|
5
|
+
|
|
6
|
+
function createNestedSchema (depth) {
|
|
7
|
+
let schema = { type: 'string' }
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < depth; i++) {
|
|
10
|
+
schema = {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: { value: schema }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return schema
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createNestedNotSchema (depth) {
|
|
20
|
+
let schema = { type: 'string' }
|
|
21
|
+
for (let i = 0; i < depth; i++) schema = { not: schema }
|
|
22
|
+
return { anyOf: [schema] }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createReferencedNotSchema (depth) {
|
|
26
|
+
const definitions = {}
|
|
27
|
+
for (let i = 0; i < depth; i++) {
|
|
28
|
+
definitions[`schema-${i}`] = {
|
|
29
|
+
not: i + 1 < depth
|
|
30
|
+
? { $ref: `#/definitions/schema-${i + 1}` }
|
|
31
|
+
: { type: 'string' }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
definitions,
|
|
36
|
+
anyOf: [{ $ref: '#/definitions/schema-0' }]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('rejects schemas deeper than the default maximum without exhausting the stack', (t) => {
|
|
41
|
+
t.assert.throws(
|
|
42
|
+
() => build(createNestedSchema(101)),
|
|
43
|
+
(error) => {
|
|
44
|
+
t.assert.equal(error.constructor, Error)
|
|
45
|
+
t.assert.equal(error.message, 'schema exceeds maximum depth of 100')
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
t.assert.doesNotThrow(() => build(createNestedSchema(100)))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('accepted boundary is safe for eager and lazy Ajv compilation', (t) => {
|
|
53
|
+
for (const createSchema of [createNestedNotSchema, createReferencedNotSchema]) {
|
|
54
|
+
for (const compileValidators of [false, true]) {
|
|
55
|
+
const stringify = build(createSchema(99), { compileValidators })
|
|
56
|
+
t.assert.doesNotThrow(() => stringify(42))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
t.assert.throws(
|
|
61
|
+
() => build(createReferencedNotSchema(100)),
|
|
62
|
+
{ message: 'schema exceeds maximum depth of 100' }
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('rejects deeply nested non-schema values before dependency traversal', (t) => {
|
|
67
|
+
let defaultValue = 'value'
|
|
68
|
+
for (let i = 0; i < 2000; i++) defaultValue = { value: defaultValue }
|
|
69
|
+
|
|
70
|
+
t.assert.throws(
|
|
71
|
+
() => build({ allOf: [{ type: 'object', default: defaultValue }] }),
|
|
72
|
+
{ message: 'schema exceeds maximum depth of 100' }
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('supports a configurable maximum schema depth', (t) => {
|
|
77
|
+
t.assert.throws(
|
|
78
|
+
() => build(createNestedSchema(3), { maxDepth: 2 }),
|
|
79
|
+
{ message: 'schema exceeds maximum depth of 2' }
|
|
80
|
+
)
|
|
81
|
+
t.assert.throws(
|
|
82
|
+
() => build({ not: { not: { not: { type: 'string' } } } }, { maxDepth: 2 }),
|
|
83
|
+
{ message: 'schema exceeds maximum depth of 2' }
|
|
84
|
+
)
|
|
85
|
+
t.assert.doesNotThrow(() => build(createNestedSchema(3), { maxDepth: 3 }))
|
|
86
|
+
t.assert.doesNotThrow(() => build({ type: 'string' }, { maxDepth: 0 }))
|
|
87
|
+
t.assert.doesNotThrow(() => build(false, { maxDepth: 0 }))
|
|
88
|
+
t.assert.throws(
|
|
89
|
+
() => build({ type: 'object', properties: { value: true } }, { maxDepth: 0 }),
|
|
90
|
+
{ message: 'schema exceeds maximum depth of 0' }
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('rejects invalid maximum schema depths', (t) => {
|
|
95
|
+
t.assert.throws(() => build(null), { message: /^schema is invalid:/ })
|
|
96
|
+
t.assert.throws(
|
|
97
|
+
() => build({}, { maxDepth: '100' }),
|
|
98
|
+
{ message: 'Unsupported max schema depth 100. Expected an integer between 0 and 100.' }
|
|
99
|
+
)
|
|
100
|
+
t.assert.throws(
|
|
101
|
+
() => build({}, { maxDepth: -1 }),
|
|
102
|
+
{ message: 'Unsupported max schema depth -1. Expected an integer between 0 and 100.' }
|
|
103
|
+
)
|
|
104
|
+
t.assert.throws(
|
|
105
|
+
() => build({}, { maxDepth: 101 }),
|
|
106
|
+
{ message: 'Unsupported max schema depth 101. Expected an integer between 0 and 100.' }
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('checks schema dependencies without treating property lists as schemas', (t) => {
|
|
111
|
+
t.assert.doesNotThrow(() => build({
|
|
112
|
+
type: 'object',
|
|
113
|
+
dependencies: {
|
|
114
|
+
enabled: { properties: { value: { type: 'string' } } },
|
|
115
|
+
value: ['enabled']
|
|
116
|
+
}
|
|
117
|
+
}, { maxDepth: 2 }))
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('checks external schemas before validating them recursively', (t) => {
|
|
121
|
+
t.assert.throws(
|
|
122
|
+
() => build({ type: 'string' }, {
|
|
123
|
+
maxDepth: 1,
|
|
124
|
+
schema: { external: createNestedSchema(2) }
|
|
125
|
+
}),
|
|
126
|
+
{ message: '"external" schema exceeds maximum depth of 1' }
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('counts nested schema levels reached through external references', (t) => {
|
|
131
|
+
const externalSchemas = {
|
|
132
|
+
first: {
|
|
133
|
+
$id: 'first',
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: { value: { $ref: 'second' } }
|
|
136
|
+
},
|
|
137
|
+
second: {
|
|
138
|
+
$id: 'second',
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: { value: { $ref: 'third' } }
|
|
141
|
+
},
|
|
142
|
+
third: {
|
|
143
|
+
$id: 'third',
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: { value: { type: 'string' } }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
t.assert.throws(
|
|
150
|
+
() => build({ $ref: 'first' }, { maxDepth: 1, schema: externalSchemas }),
|
|
151
|
+
{ message: 'schema exceeds maximum depth of 1' }
|
|
152
|
+
)
|
|
153
|
+
t.assert.doesNotThrow(() => build({ $ref: 'first' }, { maxDepth: 3, schema: externalSchemas }))
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('limits reference chains reached through validator-only keywords', (t) => {
|
|
157
|
+
const externalSchemas = {}
|
|
158
|
+
const dependencyCount = 1000
|
|
159
|
+
|
|
160
|
+
for (let i = 0; i < dependencyCount; i++) {
|
|
161
|
+
externalSchemas[`dependency-${i}`] = {
|
|
162
|
+
$id: `dependency-${i}`,
|
|
163
|
+
...(i + 1 < dependencyCount
|
|
164
|
+
? { $ref: `dependency-${i + 1}` }
|
|
165
|
+
: { type: 'string' })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const schema = {
|
|
170
|
+
anyOf: [{ not: { $ref: 'dependency-0' } }]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const compileValidators of [false, true]) {
|
|
174
|
+
t.assert.throws(
|
|
175
|
+
() => build(schema, { compileValidators, schema: externalSchemas }),
|
|
176
|
+
{ message: 'schema exceeds maximum depth of 100' }
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('walks references in non-schema values without recursive dependency discovery', (t) => {
|
|
182
|
+
const externalSchemas = {}
|
|
183
|
+
const dependencyCount = 10000
|
|
184
|
+
|
|
185
|
+
for (let i = 0; i < dependencyCount; i++) {
|
|
186
|
+
externalSchemas[`annotation-${i}`] = {
|
|
187
|
+
$id: `annotation-${i}`,
|
|
188
|
+
$ref: i + 1 < dependencyCount ? `annotation-${i + 1}` : 'root'
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
t.assert.doesNotThrow(() => build({
|
|
193
|
+
$id: 'root',
|
|
194
|
+
type: 'string',
|
|
195
|
+
anyOf: [{ type: 'string' }],
|
|
196
|
+
default: { $ref: 'annotation-0' }
|
|
197
|
+
}, { schema: externalSchemas }))
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('limits and detects reference-only chains', (t) => {
|
|
201
|
+
const externalSchemas = {
|
|
202
|
+
first: { $id: 'first', $ref: 'second' },
|
|
203
|
+
second: { $id: 'second', $ref: 'third' },
|
|
204
|
+
third: { $id: 'third', type: 'string' }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
t.assert.throws(
|
|
208
|
+
() => build({ $ref: 'first' }, { maxDepth: 1, schema: externalSchemas }),
|
|
209
|
+
{ message: 'schema exceeds maximum depth of 1' }
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
externalSchemas.third = { $id: 'third', $ref: 'first' }
|
|
213
|
+
t.assert.throws(
|
|
214
|
+
() => build({ $ref: 'first' }, { schema: externalSchemas }),
|
|
215
|
+
{ message: 'Cannot resolve circular reference "second"' }
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('rejects circular object graphs before schema validation', (t) => {
|
|
220
|
+
const schema = { type: 'object', properties: {} }
|
|
221
|
+
schema.properties.self = schema
|
|
222
|
+
|
|
223
|
+
t.assert.throws(
|
|
224
|
+
() => build(schema),
|
|
225
|
+
{ message: 'schema contains a circular object reference; use $ref instead' }
|
|
226
|
+
)
|
|
227
|
+
})
|