Skip to content
This repository was archived by the owner on Jul 13, 2023. It is now read-only.

Commit ba7308d

Browse files
authored
feat: track $ref dependencies (#104)
* feat: track $ref dependencies * chore: pr feed back - revert changes to ICrawlerResult - don't use srn - add tests * feat: ref graph * chore: clean up * fix: graph should not be an option * fix: return a new graph instance when resolving a json object * chore: fix tests * chore: set node data in graph * chore: update refgraph typing * chore: add circular tests * chore: remove ref graph functions
1 parent 092b1b8 commit ba7308d

File tree

6 files changed

+219
-17
lines changed

6 files changed

+219
-17
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`resolver print tree circular refs 1`] = `
4+
Array [
5+
"#/ref3",
6+
"#/ref1",
7+
"#/ref2",
8+
]
9+
`;
10+
11+
exports[`resolver print tree indirect circular refs 1`] = `
12+
Array [
13+
"custom://obj3/",
14+
"custom://obj2/",
15+
"custom://obj1/",
16+
]
17+
`;
18+
19+
exports[`resolver print tree should handle local refs 1`] = `
20+
Array [
21+
"#/definitions/bear",
22+
]
23+
`;
24+
25+
exports[`resolver print tree should resolve http relative paths + back pointing uri refs 1`] = `undefined`;

src/__tests__/resolver.spec.ts

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,7 @@ describe('resolver', () => {
819819
};
820820

821821
const runner = new ResolveRunner(source);
822-
const resolved = await runner.resolve('#/inner2/marcsStreet');
822+
const resolved = await runner.resolve({ jsonPointer: '#/inner2/marcsStreet' });
823823

824824
// only marcStreet and related paths replaced
825825
const newObj = {
@@ -847,7 +847,7 @@ describe('resolver', () => {
847847

848848
// now we use the same runner to resolve another portion of it
849849
// only the new portions are resolved (in addition to what has already been done)
850-
await resolved.runner.resolve('#/inner3');
850+
await resolved.runner.resolve({ jsonPointer: '#/inner3' });
851851

852852
expect(runner.source).toEqual({
853853
...newObj,
@@ -2070,6 +2070,130 @@ describe('resolver', () => {
20702070
});
20712071
});
20722072

2073+
describe('print tree', () => {
2074+
test('should handle local refs', async () => {
2075+
const data = {
2076+
title: 'Example',
2077+
type: 'object',
2078+
definitions: {
2079+
bear: {
2080+
type: 'object',
2081+
properties: {
2082+
type: {
2083+
type: 'string',
2084+
},
2085+
diet: {
2086+
type: 'string',
2087+
},
2088+
age: {
2089+
type: 'number',
2090+
},
2091+
},
2092+
required: ['type', 'diet', 'age'],
2093+
},
2094+
},
2095+
description: 'Bears are awesome',
2096+
properties: {
2097+
id: {
2098+
type: 'string',
2099+
},
2100+
bear: {
2101+
$ref: '#/definitions/bear',
2102+
},
2103+
},
2104+
};
2105+
2106+
const resolver = new Resolver();
2107+
const { graph } = await resolver.resolve(data);
2108+
2109+
expect(graph.dependenciesOf('root')).toMatchSnapshot();
2110+
});
2111+
2112+
// ./a#/foo -> ./b#bar -> ./a#/xxx -> ./c -> ./b#/zzz
2113+
test('should resolve http relative paths + back pointing uri refs', async () => {
2114+
const source = httpMocks['https://back-pointing.com/a'];
2115+
2116+
const resolver = new Resolver({
2117+
resolvers: {
2118+
https: new HttpReader(),
2119+
},
2120+
});
2121+
2122+
const baseUri = 'https://back-pointing.com/a';
2123+
const { graph } = await resolver.resolve(source, {
2124+
baseUri,
2125+
});
2126+
2127+
expect(graph.dependenciesOf[baseUri]).toMatchSnapshot();
2128+
});
2129+
2130+
test('circular refs', async () => {
2131+
const source = {
2132+
ref1: {
2133+
$ref: '#/ref3',
2134+
},
2135+
ref2: {
2136+
$ref: '#/ref1',
2137+
},
2138+
ref3: {
2139+
$ref: '#/ref2',
2140+
},
2141+
};
2142+
2143+
const resolver = new Resolver();
2144+
const { graph } = await resolver.resolve(source);
2145+
2146+
expect(graph.dependenciesOf('root')).toMatchSnapshot();
2147+
});
2148+
2149+
test('indirect circular refs', async () => {
2150+
const data = {
2151+
obj1: {
2152+
one: true,
2153+
foo: {
2154+
$ref: 'custom://obj2',
2155+
},
2156+
},
2157+
obj2: {
2158+
two: true,
2159+
foo: {
2160+
$ref: 'custom://obj3',
2161+
},
2162+
},
2163+
obj3: {
2164+
three: true,
2165+
foo: {
2166+
$ref: 'custom://obj1',
2167+
},
2168+
},
2169+
};
2170+
2171+
const source = {
2172+
inner: {
2173+
data: {
2174+
$ref: 'custom://obj1',
2175+
},
2176+
},
2177+
};
2178+
2179+
const reader: Types.IResolver = {
2180+
async resolve(ref: uri.URI): Promise<any> {
2181+
return data[ref.authority()];
2182+
},
2183+
};
2184+
2185+
const resolver = new Resolver({
2186+
resolvers: {
2187+
custom: reader,
2188+
},
2189+
});
2190+
2191+
const { graph } = await resolver.resolve(source);
2192+
2193+
expect(graph.dependenciesOf('root')).toMatchSnapshot();
2194+
});
2195+
});
2196+
20732197
describe('use cases', () => {
20742198
test('mixture of file and http', async () => {
20752199
const data = {

src/crawler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ export class ResolveCrawler implements Types.ICrawler {
145145
this.pointerGraph.addNode(targetPointer);
146146
}
147147

148+
const targetRef = `${this._runner.baseUri.toString()}${targetPointer}`;
149+
if (!this._runner.graph.hasNode(targetRef)) this._runner.graph.addNode(targetRef);
150+
if (this._runner.root !== targetRef) this._runner.graph.addDependency(this._runner.root, targetRef);
151+
148152
// register parent as a dependant of the target
149153
this.pointerGraph.addDependency(parentPointer, targetPointer);
150154

@@ -161,6 +165,10 @@ export class ResolveCrawler implements Types.ICrawler {
161165
}
162166
} else {
163167
// remote pointer
168+
const remoteRef = ref.toString();
169+
if (!this._runner.graph.hasNode(remoteRef)) this._runner.graph.addNode(remoteRef);
170+
if (this._runner.root !== remoteRef) this._runner.graph.addDependency(this._runner.root, remoteRef);
171+
164172
if (this._runner.dereferenceRemote && !this._runner.atMaxUriDepth()) {
165173
this.resolvers.push(this._runner.lookupAndResolveUri(opts));
166174
}

src/resolver.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { DepGraph } from 'dependency-graph';
2+
13
import { Cache } from './cache';
24
import { ResolveRunner } from './runner';
35
import * as Types from './types';
@@ -35,7 +37,8 @@ export class Resolver {
3537
}
3638

3739
public resolve(source: any, opts: Types.IResolveOpts = {}): Promise<Types.IResolveResult> {
38-
const runner = new ResolveRunner(source, {
40+
const graph = new DepGraph<any>({ circular: true });
41+
const runner = new ResolveRunner(source, graph, {
3942
uriCache: this.uriCache,
4043
resolvers: this.resolvers,
4144
getRef: this.getRef,
@@ -48,6 +51,6 @@ export class Resolver {
4851
ctx: Object.assign({}, this.ctx || {}, opts.ctx || {}),
4952
});
5053

51-
return runner.resolve(opts.jsonPointer);
54+
return runner.resolve(opts);
5255
}
5356
}

src/runner.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { pathToPointer, pointerToPath, startsWith, trimStart } from '@stoplight/json';
2-
import produce from 'immer';
2+
import { DepGraph } from 'dependency-graph';
3+
import produce, { original } from 'immer';
34
import { get, set } from 'lodash';
45
import { dirname, join } from 'path';
56
import * as URI from 'urijs';
@@ -24,6 +25,8 @@ export class ResolveRunner implements Types.IResolveRunner {
2425
public readonly id: number;
2526
public readonly baseUri: uri.URI;
2627
public readonly uriCache: Types.ICache;
28+
public readonly graph: DepGraph<any>;
29+
public readonly root: string;
2730

2831
public depth: number;
2932
public uriStack: string[];
@@ -44,7 +47,11 @@ export class ResolveRunner implements Types.IResolveRunner {
4447

4548
private _source: any;
4649

47-
constructor(source: any, opts: Types.IResolveRunnerOpts = {}) {
50+
constructor(
51+
source: any,
52+
graph: DepGraph<any> = new DepGraph<any>({ circular: true }),
53+
opts: Types.IResolveRunnerOpts = {},
54+
) {
4855
this.id = resolveRunnerCount += 1;
4956
this.depth = opts.depth || 0;
5057
this._source = source;
@@ -60,6 +67,13 @@ export class ResolveRunner implements Types.IResolveRunner {
6067
this.uriStack = opts.uriStack || [];
6168
this.uriCache = opts.uriCache || new Cache();
6269

70+
this.root = (opts.root && opts.root.toString()) || this.baseUri.toString() || 'root';
71+
72+
this.graph = graph;
73+
if (!this.graph.hasNode(this.root)) {
74+
this.graph.addNode(this.root);
75+
}
76+
6377
if (this.baseUri && this.depth === 0) {
6478
// if this first runner has a baseUri, seed the cache so we don't create another one for this uri later
6579
this.uriCache.set(this.computeUriCacheKey(this.baseUri), this);
@@ -94,16 +108,17 @@ export class ResolveRunner implements Types.IResolveRunner {
94108
return this._source;
95109
}
96110

97-
public async resolve(jsonPointer?: string, opts?: Types.IResolveOpts): Promise<Types.IResolveResult> {
111+
public async resolve(opts?: Types.IResolveOpts): Promise<Types.IResolveResult> {
98112
const resolved: Types.IResolveResult = {
99113
result: this.source,
114+
graph: this.graph,
100115
refMap: {},
101116
errors: [],
102117
runner: this,
103118
};
104119

105120
let targetPath: any;
106-
jsonPointer = jsonPointer && jsonPointer.trim();
121+
const jsonPointer = opts && opts.jsonPointer && opts.jsonPointer.trim();
107122
if (jsonPointer && jsonPointer !== '#' && jsonPointer !== '#/') {
108123
targetPath = pointerToPath(jsonPointer);
109124
resolved.result = get(resolved.result, targetPath);
@@ -165,6 +180,10 @@ export class ResolveRunner implements Types.IResolveRunner {
165180
return r.resolved.result;
166181
} else {
167182
set(draft, resolvedTargetPath, r.resolved.result);
183+
184+
if (this.graph.hasNode(String(r.uri))) {
185+
this.graph.setNodeData(String(r.uri), r.resolved.result);
186+
}
168187
}
169188
}
170189
});
@@ -209,6 +228,10 @@ export class ResolveRunner implements Types.IResolveRunner {
209228

210229
if (val !== void 0) {
211230
set(draft, dependantPath, val);
231+
232+
if (this.graph.hasNode(pathToPointer(pointerPath))) {
233+
this.graph.setNodeData(pathToPointer(pointerPath), original(val));
234+
}
212235
} else {
213236
resolved.errors.push({
214237
code: 'POINTER_MISSING',
@@ -366,9 +389,10 @@ export class ResolveRunner implements Types.IResolveRunner {
366389
}
367390
}
368391

369-
return new ResolveRunner(result, {
392+
return new ResolveRunner(result, this.graph, {
370393
depth: this.depth + 1,
371394
baseUri: ref.toString(),
395+
root: ref,
372396
uriStack: this.uriStack,
373397
uriCache: this.uriCache,
374398
resolvers: this.resolvers,
@@ -397,6 +421,7 @@ export class ResolveRunner implements Types.IResolveRunner {
397421
if (this.uriStack.includes(uriCacheKey)) {
398422
lookupResult.resolved = {
399423
result: val,
424+
graph: this.graph,
400425
refMap: {},
401426
errors: [],
402427
runner: this,
@@ -441,7 +466,10 @@ export class ResolveRunner implements Types.IResolveRunner {
441466
// only resolve the uri result if we were able to look it up and create the resolver
442467
// @ts-ignore
443468
if (uriResolver) {
444-
lookupResult.resolved = await uriResolver.resolve(Utils.uriToJSONPointer(ref), { parentPath });
469+
lookupResult.resolved = await uriResolver.resolve({
470+
jsonPointer: Utils.uriToJSONPointer(ref),
471+
parentPath,
472+
});
445473

446474
// if pointer resolution failed, revert to the original value (which will be a $ref most of the time)
447475
if (lookupResult.resolved.errors.length) {

0 commit comments

Comments
 (0)