Skip to content

Commit c52431d

Browse files
authored
feat: implement resolveInlineRef util (#68)
* feat: implement resolveInlineRef util * fix: circular detection was screwed * chore: tweak
1 parent 7c17067 commit c52431d

3 files changed

Lines changed: 177 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { resolveInlineRef } from '../resolveInlineRef';
2+
3+
describe('resolveInlineRef', () => {
4+
test('should follow refs', () => {
5+
const doc = {
6+
a: {
7+
$ref: '#/b/foo',
8+
},
9+
b: {
10+
$ref: '#/c',
11+
},
12+
c: {
13+
$ref: '#/d',
14+
},
15+
d: {
16+
foo: 'woo!',
17+
},
18+
};
19+
20+
expect(resolveInlineRef(doc, '#/a')).toEqual('woo!');
21+
});
22+
23+
test('should follow refs #2', () => {
24+
const doc = {
25+
a: {
26+
$ref: '#/b/foo',
27+
},
28+
b: {
29+
foo: {
30+
$ref: '#/c',
31+
},
32+
bar: {
33+
$ref: '#/e',
34+
},
35+
},
36+
c: {
37+
$ref: '#/b/bar',
38+
},
39+
e: 'woo!',
40+
};
41+
42+
expect(resolveInlineRef(doc, '#/a')).toEqual('woo!');
43+
});
44+
45+
test('should handle direct circular refs', () => {
46+
const doc = {
47+
a: {
48+
$ref: '#/b',
49+
},
50+
b: {
51+
$ref: '#/a',
52+
},
53+
};
54+
55+
expect(resolveInlineRef(doc, '#/a')).toEqual({
56+
$ref: '#/a',
57+
});
58+
});
59+
60+
test('should handle direct circular refs #2', () => {
61+
const doc = {
62+
a: {
63+
$ref: '#/b/foo',
64+
},
65+
b: {
66+
foo: {
67+
$ref: '#/c',
68+
},
69+
bar: {
70+
$ref: '#/e',
71+
},
72+
},
73+
c: {
74+
$ref: '#/b/bar',
75+
},
76+
e: {
77+
$ref: '#/a',
78+
},
79+
};
80+
81+
expect(resolveInlineRef(doc, '#/a')).toEqual({
82+
$ref: '#/a',
83+
});
84+
});
85+
86+
test('given external reference, should throw', () => {
87+
const doc = {
88+
a: {
89+
$ref: './foo#/b/foo',
90+
},
91+
};
92+
93+
expect(resolveInlineRef.bind(null, doc, '#/a')).toThrowError('Cannot resolve external references');
94+
});
95+
96+
test('given missing segment, should throw', () => {
97+
const doc = {
98+
a: {
99+
$ref: '#/b/foo',
100+
},
101+
b: {
102+
bar: false,
103+
},
104+
};
105+
106+
expect(resolveInlineRef.bind(null, doc, '#/a')).toThrowError("Could not resolve '#/b/foo'");
107+
});
108+
109+
test('given path pointing at invalid data, should throw', () => {
110+
const doc = {
111+
a: {
112+
$ref: '#/b/foo/bar',
113+
},
114+
b: {
115+
foo: null,
116+
},
117+
};
118+
119+
expect(resolveInlineRef.bind(null, doc, '#/a')).toThrowError("Could not resolve '#/b/foo/bar'");
120+
});
121+
122+
test('given invalid $ref type, should throw', () => {
123+
const doc = {
124+
a: {
125+
$ref: 2,
126+
},
127+
};
128+
129+
expect(resolveInlineRef.bind(null, doc, '#/a')).toThrowError('$ref should be a string');
130+
});
131+
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './parseWithPointers';
1515
export * from './pathToPointer';
1616
export * from './pointerToPath';
1717
export * from './renameObjectKey';
18+
export * from './resolveInlineRef';
1819
export * from './safeParse';
1920
export * from './safeStringify';
2021
export * from './startsWith';

src/resolveInlineRef.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Dictionary } from '@stoplight/types';
2+
import { extractSourceFromRef } from './extractSourceFromRef';
3+
import { pointerToPath } from './pointerToPath';
4+
5+
function isObject(maybeObj: unknown): maybeObj is { [key in PropertyKey]: unknown } {
6+
return typeof maybeObj === 'object' && maybeObj !== null;
7+
}
8+
9+
function _resolveInlineRef(document: Dictionary<unknown>, ref: string, seen: unknown[]): unknown {
10+
const source = extractSourceFromRef(ref);
11+
if (source !== null) {
12+
throw new ReferenceError('Cannot resolve external references');
13+
}
14+
15+
const path = pointerToPath(ref);
16+
let value: unknown = document;
17+
for (const segment of path) {
18+
if (!isObject(value) || !(segment in value)) {
19+
throw new ReferenceError(`Could not resolve '${ref}'`);
20+
}
21+
22+
value = value[segment];
23+
24+
if (isObject(value) && '$ref' in value) {
25+
if (seen.includes(value)) {
26+
// circular, let's stop
27+
return seen[seen.length - 1];
28+
}
29+
30+
seen.push(value);
31+
32+
if (typeof value.$ref !== 'string') {
33+
throw new TypeError('$ref should be a string');
34+
}
35+
36+
value = _resolveInlineRef(document, value.$ref, seen);
37+
}
38+
}
39+
40+
return value;
41+
}
42+
43+
export function resolveInlineRef(document: Dictionary<unknown>, ref: string): unknown {
44+
return _resolveInlineRef(document, ref, []);
45+
}

0 commit comments

Comments
 (0)