Skip to content

Commit 5d6e244

Browse files
committed
fix: external $refs expanding
1 parent dee59bc commit 5d6e244

File tree

4 files changed

+151
-13
lines changed

4 files changed

+151
-13
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ exports[`populateTree util should match array-of-refs.json 1`] = `
5555
Object {
5656
"children": Array [
5757
Object {
58+
"children": Array [],
5859
"id": "random-id",
5960
"name": "",
6061
"parent": [Circular],
@@ -276,6 +277,7 @@ Object {
276277
Object {
277278
"children": Array [
278279
Object {
280+
"children": Array [],
279281
"id": "random-id",
280282
"name": "",
281283
"parent": [Circular],
@@ -334,6 +336,7 @@ Object {
334336
"parent": [Circular],
335337
},
336338
Object {
339+
"children": Array [],
337340
"id": "random-id",
338341
"name": "",
339342
"parent": [Circular],

src/tree/__tests__/tree.spec.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,126 @@ describe('SchemaTree', () => {
162162
);
163163
});
164164
});
165+
166+
describe('empty $ref', () => {
167+
let schema: JSONSchema4;
168+
169+
beforeEach(() => {
170+
schema = {
171+
type: 'object',
172+
properties: {
173+
id: {
174+
$ref: '',
175+
},
176+
},
177+
};
178+
});
179+
180+
test('given no custom resolver, should generate an error', () => {
181+
const tree = new SchemaTree(schema, new SchemaTreeState(), {
182+
expandedDepth: 0,
183+
mergeAllOf: false,
184+
resolveRef: void 0,
185+
});
186+
187+
tree.populate();
188+
189+
tree.unwrap(tree.itemAt(1) as TreeListParentNode);
190+
expect(getNodeMetadata(tree.itemAt(2) as TreeListParentNode)).toHaveProperty(
191+
'error',
192+
'The pointer is empty',
193+
);
194+
});
195+
196+
test('given a custom resolver, should attempt to resolve the reference', () => {
197+
const tree = new SchemaTree(schema, new SchemaTreeState(), {
198+
expandedDepth: 0,
199+
mergeAllOf: false,
200+
resolveRef() {
201+
throw new ReferenceError('Seems like you do not want this to be empty.');
202+
},
203+
});
204+
205+
tree.populate();
206+
207+
tree.unwrap(tree.itemAt(1) as TreeListParentNode);
208+
expect(getNodeMetadata(tree.itemAt(2) as TreeListParentNode)).toHaveProperty(
209+
'error',
210+
'Seems like you do not want this to be empty.'
211+
);
212+
});
213+
});
214+
215+
describe('external $refs', () => {
216+
let schema: JSONSchema4;
217+
218+
beforeEach(() => {
219+
schema = {
220+
type: 'object',
221+
properties: {
222+
user: {
223+
type: 'array',
224+
items: {
225+
$ref: '../test#',
226+
},
227+
},
228+
id: {
229+
$ref: '../foo#id',
230+
},
231+
},
232+
};
233+
});
234+
235+
test('given no custom resolver, should generate an error', () => {
236+
const tree = new SchemaTree(schema, new SchemaTreeState(), {
237+
expandedDepth: 0,
238+
mergeAllOf: false,
239+
resolveRef: void 0,
240+
});
241+
242+
tree.populate();
243+
244+
tree.unwrap(tree.itemAt(1) as TreeListParentNode);
245+
expect(getNodeMetadata(tree.itemAt(2) as TreeListParentNode)).toHaveProperty(
246+
'error',
247+
'Cannot dereference external references',
248+
);
249+
250+
tree.unwrap(tree.itemAt(3) as TreeListParentNode);
251+
expect(getNodeMetadata(tree.itemAt(4) as TreeListParentNode)).toHaveProperty(
252+
'error',
253+
'Cannot dereference external references',
254+
);
255+
});
256+
257+
test('given a custom resolver, should attempt to resolve the reference', () => {
258+
const tree = new SchemaTree(schema, new SchemaTreeState(), {
259+
expandedDepth: 0,
260+
mergeAllOf: false,
261+
resolveRef({ source, pointer }) {
262+
if (source === '../test') {
263+
throw new ReferenceError(`Could not read "${source}"`);
264+
}
265+
266+
throw new ReferenceError(`Pointer "${pointer}" is missing`);
267+
},
268+
});
269+
270+
tree.populate();
271+
272+
tree.unwrap(tree.itemAt(1) as TreeListParentNode);
273+
expect(getNodeMetadata(tree.itemAt(2) as TreeListParentNode)).toHaveProperty(
274+
'error',
275+
'Could not read "../test"',
276+
);
277+
278+
tree.unwrap(tree.itemAt(3) as TreeListParentNode);
279+
expect(getNodeMetadata(tree.itemAt(4) as TreeListParentNode)).toHaveProperty(
280+
'error',
281+
'Pointer "#id" is missing',
282+
);
283+
});
284+
});
165285
});
166286

167287
describe('paths generation', () => {

src/tree/tree.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isLocalRef, pathToPointer, pointerToPath } from '@stoplight/json';
1+
import { extractPointerFromRef, extractSourceFromRef, pointerToPath } from '@stoplight/json';
22
import { Tree, TreeListParentNode, TreeState } from '@stoplight/tree-list';
33
import { JsonPath, Optional } from '@stoplight/types';
44
import { JSONSchema4 } from 'json-schema';
@@ -10,8 +10,13 @@ import { getSchemaNodeMetadata, metadataStore } from './metadata';
1010
import { canStepIn } from './utils/canStepIn';
1111
import { populateTree } from './utils/populateTree';
1212

13+
export type SchemaTreeRefInfo = {
14+
source: string | null;
15+
pointer: string | null;
16+
};
17+
1318
export type SchemaTreeRefDereferenceFn = (
14-
refPath: JsonPath,
19+
ref: SchemaTreeRefInfo,
1520
propertyPath: JsonPath,
1621
schema: JSONSchema4,
1722
) => Optional<JSONSchema4>;
@@ -44,10 +49,7 @@ export class SchemaTree extends Tree {
4449
populateTree(this.schema, this.root, 0, [], {
4550
mergeAllOf: this.mergeAllOf,
4651
onNode: (fragment, node, parentTreeNode, level): boolean => {
47-
if (
48-
(isRefNode(node) && node.$ref !== null && isLocalRef(node.$ref)) ||
49-
(hasRefItems(node) && node.items.$ref !== null && isLocalRef(node.items.$ref))
50-
) {
52+
if ((isRefNode(node) && node.$ref !== null) || (hasRefItems(node) && node.items.$ref !== null)) {
5153
expanded[node.id] = false;
5254
}
5355

@@ -133,13 +135,27 @@ export class SchemaTree extends Tree {
133135
}
134136

135137
protected populateRefFragment(node: TreeListParentNode, path: JsonPath, ref: string | null) {
136-
if (!ref) {
138+
if (ref === null) {
137139
throw new Error('Unknown $ref value');
138140
}
139-
const refPath = pointerToPath(ref);
140-
const schemaFragment = this.resolveRef ? this.resolveRef(refPath, path, this.schema) : _get(this.schema, refPath);
141+
142+
const source = extractSourceFromRef(ref);
143+
const pointer = extractPointerFromRef(ref);
144+
145+
let schemaFragment: Optional<JSONSchema4>;
146+
147+
if (this.resolveRef !== void 0) {
148+
schemaFragment = this.resolveRef({ source, pointer }, path, this.schema);
149+
} else if (source !== null) {
150+
throw new ReferenceError('Cannot dereference external references');
151+
} else if (pointer === null) {
152+
throw new ReferenceError('The pointer is empty');
153+
} else {
154+
schemaFragment = _get(this.schema, pointerToPath(pointer));
155+
}
156+
141157
if (!_isObject(schemaFragment)) {
142-
throw new ReferenceError(`Could not dereference "${pathToPointer(refPath)}"`);
158+
throw new ReferenceError(`Could not dereference "${ref}"`);
143159
}
144160

145161
this.populateTreeFragment(node, schemaFragment, path, false);

src/tree/utils/populateTree.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { isLocalRef } from '@stoplight/json';
21
import { TreeListNode, TreeListParentNode } from '@stoplight/tree-list';
32
import { JsonPath, Optional } from '@stoplight/types';
43
import { JSONSchema4 } from 'json-schema';
@@ -44,7 +43,7 @@ export const populateTree: Walker = (schema, parent, level, path, options): unde
4443
path,
4544
});
4645

47-
if (isRefNode(node) && node.$ref !== null && isLocalRef(node.$ref) && node.$ref !== '#') {
46+
if (isRefNode(node) && node.$ref !== null) {
4847
(treeNode as TreeListParentNode).children = [];
4948
} else if (!isCombinerNode(node)) {
5049
switch (getPrimaryType(node)) {
@@ -90,7 +89,7 @@ function processArray(
9089
path: JsonPath,
9190
options: WalkingOptions | null,
9291
): SchemaTreeListNode {
93-
if (hasRefItems(schema) && schema.items.$ref && isLocalRef(schema.items.$ref)) {
92+
if (hasRefItems(schema) && schema.items.$ref) {
9493
(node as TreeListParentNode).children = [];
9594
} else if (Array.isArray(schema.items)) {
9695
const children: SchemaTreeListNode[] = [];

0 commit comments

Comments
 (0)