Skip to content

Commit 628627c

Browse files
P0lipDaniel A. White
and
Daniel A. White
authoredNov 15, 2023
feat(tree): account for resolveInlineRef behavior when $ref has siblings (#25)
* fix(tree): account for resolveInlineRef behavior when $ref has siblings * feat(walker): adds a max depth option for refs --------- Co-authored-by: Daniel A. White <[email protected]>
1 parent 5cf2801 commit 628627c

File tree

8 files changed

+116
-5
lines changed

8 files changed

+116
-5
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"title": "Thing",
3+
"allOf": [
4+
{
5+
"$ref": "#/definitions/User"
6+
}
7+
],
8+
"description": "baz",
9+
"definitions": {
10+
"User": {
11+
"type": "object",
12+
"description": "user",
13+
"properties": {
14+
"manager": {
15+
"$ref": "#/definitions/Boss"
16+
}
17+
}
18+
},
19+
"Boss": {
20+
"$ref": "#/definitions/User",
21+
"description": "xyz"
22+
}
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"oneOf": [
3+
{
4+
"$ref": "#/definitions/User"
5+
}
6+
],
7+
"description": "User Model",
8+
"definitions": {
9+
"User": {
10+
"type": "object",
11+
"description": "Plain User",
12+
"properties": {
13+
"manager": {
14+
"$ref": "#/definitions/Admin"
15+
}
16+
}
17+
},
18+
"Admin": {
19+
"$ref": "#/definitions/User",
20+
"description": "Admin User"
21+
}
22+
}
23+
}

‎src/__tests__/__snapshots__/tree.spec.ts.snap

+23
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,29 @@ exports[`SchemaTree output should generate valid tree for references/nullish.jso
12931293
"
12941294
`;
12951295

1296+
exports[`SchemaTree output should generate valid tree for references/with-overrides.json 1`] = `
1297+
"└─ #
1298+
├─ combiners
1299+
│ └─ 0: oneOf
1300+
└─ children
1301+
└─ 0
1302+
└─ #/oneOf/0
1303+
├─ types
1304+
│ └─ 0: object
1305+
├─ primaryType: object
1306+
└─ children
1307+
└─ 0
1308+
└─ #/oneOf/0/properties/manager
1309+
├─ types
1310+
│ └─ 0: object
1311+
├─ primaryType: object
1312+
└─ children
1313+
└─ 0
1314+
└─ #/oneOf/0/properties/manager/properties/manager
1315+
└─ mirrors: #/oneOf/0/properties/manager
1316+
"
1317+
`;
1318+
12961319
exports[`SchemaTree output should generate valid tree for tickets.schema.json 1`] = `
12971320
"└─ #
12981321
├─ types

‎src/__tests__/tree.spec.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('SchemaTree', () => {
1313
it.each(
1414
fastGlob.sync('**/*.json', {
1515
cwd: path.join(__dirname, '__fixtures__'),
16-
ignore: ['stress-schema.json'],
16+
ignore: ['stress-schema.json', 'recursive-schema.json'],
1717
}),
1818
)('should generate valid tree for %s', async filename => {
1919
const schema = JSON.parse(await fs.promises.readFile(path.resolve(__dirname, '__fixtures__', filename), 'utf8'));
@@ -985,4 +985,17 @@ describe('SchemaTree', () => {
985985
});
986986
});
987987
});
988+
989+
describe('recursive walking', () => {
990+
it('should load with a max depth', async () => {
991+
const schema = JSON.parse(
992+
await fs.promises.readFile(path.resolve(__dirname, '__fixtures__', 'recursive-schema.json'), 'utf8'),
993+
);
994+
995+
const w = new SchemaTree(schema, {
996+
maxRefDepth: 1000,
997+
});
998+
w.populate();
999+
});
1000+
});
9881001
});

‎src/tree/tree.ts

+9
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,22 @@ import type { SchemaTreeOptions } from './types';
1111
export class SchemaTree {
1212
public walker: Walker;
1313
public root: RootNode;
14+
private readonly resolvedRefs = new Map();
1415

1516
constructor(public schema: SchemaFragment, protected readonly opts?: Partial<SchemaTreeOptions>) {
1617
this.root = new RootNode(schema);
18+
this.resolvedRefs = new Map();
1719
this.walker = new Walker(this.root, {
1820
mergeAllOf: this.opts?.mergeAllOf !== false,
1921
resolveRef: opts?.refResolver === null ? null : this.resolveRef,
22+
maxRefDepth: opts?.maxRefDepth,
2023
});
2124
}
2225

2326
public destroy() {
2427
this.root.children.length = 0;
2528
this.walker.destroy();
29+
this.resolvedRefs.clear();
2630
}
2731

2832
public populate() {
@@ -34,6 +38,10 @@ export class SchemaTree {
3438
}
3539

3640
protected resolveRef: WalkerRefResolver = (path, $ref) => {
41+
if (this.resolvedRefs.has($ref)) {
42+
return this.resolvedRefs.get($ref);
43+
}
44+
3745
const seenRefs: string[] = [];
3846
let cur$ref: unknown = $ref;
3947
let resolvedValue!: SchemaFragment;
@@ -48,6 +56,7 @@ export class SchemaTree {
4856
cur$ref = resolvedValue.$ref;
4957
}
5058

59+
this.resolvedRefs.set($ref, resolvedValue);
5160
return resolvedValue;
5261
};
5362

‎src/tree/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import type { SchemaFragment } from '../types';
22

33
export type SchemaTreeOptions = {
44
mergeAllOf: boolean;
5+
/** Resolves references to the schemas. If providing a custom implementation, it must return the same object reference for the same reference string. */
56
refResolver: SchemaTreeRefDereferenceFn | null;
7+
/** Controls the level of recursion of refs. Prevents overly complex trees and running out of stack depth. */
8+
maxRefDepth?: number | null;
69
};
710

811
export type SchemaTreeRefInfo = {

‎src/walker/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ export type WalkerRefResolver = (path: string[] | null, $ref: string) => SchemaF
66

77
export type WalkingOptions = {
88
mergeAllOf: boolean;
9+
/** Resolves references to the schemas. If providing a custom implementation, it must return the same object reference for the same reference string. */
910
resolveRef: WalkerRefResolver | null;
11+
/** Controls the level of recursion of refs. Prevents overly complex trees and running out of stack depth. */
12+
maxRefDepth?: number | null;
1013
};
1114

1215
export type WalkerSnapshot = {

‎src/walker/walker.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,22 @@ export class Walker extends EventEmitter<WalkerEmitter> {
3636
constructor(protected readonly root: RootNode, protected readonly walkingOptions: WalkingOptions) {
3737
super();
3838

39+
let maxRefDepth = walkingOptions.maxRefDepth ?? null;
40+
if (typeof maxRefDepth === 'number') {
41+
if (maxRefDepth < 1) {
42+
maxRefDepth = null;
43+
} else if (maxRefDepth > 1000) {
44+
// experimented with 1500 and the recursion limit is still lower than that
45+
maxRefDepth = 1000;
46+
}
47+
}
48+
walkingOptions.maxRefDepth = maxRefDepth;
49+
3950
this.path = [];
4051
this.depth = -1;
4152
this.fragment = root.fragment;
4253
this.schemaNode = root;
43-
this.processedFragments = new WeakMap<SchemaFragment, SchemaNode>();
54+
this.processedFragments = new WeakMap();
4455
this.mergedAllOfs = new WeakMap();
4556

4657
this.hooks = {};
@@ -51,7 +62,7 @@ export class Walker extends EventEmitter<WalkerEmitter> {
5162
this.depth = -1;
5263
this.fragment = this.root.fragment;
5364
this.schemaNode = this.root;
54-
this.processedFragments = new WeakMap<SchemaFragment, RegularNode | ReferenceNode>();
65+
this.processedFragments = new WeakMap();
5566
this.mergedAllOfs = new WeakMap();
5667
}
5768

@@ -265,7 +276,7 @@ export class Walker extends EventEmitter<WalkerEmitter> {
265276
}
266277

267278
protected processFragment(): [SchemaNode, ProcessedFragment] {
268-
const { walkingOptions, path, fragment: originalFragment } = this;
279+
const { walkingOptions, path, fragment: originalFragment, depth } = this;
269280
let { fragment } = this;
270281

271282
let retrieved = isNonNullable(fragment) ? this.retrieveFromFragment(fragment, originalFragment) : null;
@@ -275,7 +286,9 @@ export class Walker extends EventEmitter<WalkerEmitter> {
275286
}
276287

277288
if ('$ref' in fragment) {
278-
if (typeof fragment.$ref !== 'string') {
289+
if (typeof walkingOptions.maxRefDepth === 'number' && walkingOptions.maxRefDepth < depth) {
290+
return [new ReferenceNode(fragment, `max $ref depth limit reached`), fragment];
291+
} else if (typeof fragment.$ref !== 'string') {
279292
return [new ReferenceNode(fragment, '$ref is not a string'), fragment];
280293
} else if (walkingOptions.resolveRef !== null) {
281294
try {

0 commit comments

Comments
 (0)
Please sign in to comment.