Skip to content

Commit c0c001c

Browse files
authored
fix: scope bundle to prevent collisions (#50)
1 parent a275d2a commit c0c001c

File tree

3 files changed

+139
-46
lines changed

3 files changed

+139
-46
lines changed

src/__tests__/bundle.spec.ts

Lines changed: 109 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cloneDeep } from 'lodash';
22

3-
import { bundleTarget } from '../bundle';
3+
import { BUNDLE_ROOT, bundleTarget } from '../bundle';
44

55
describe('bundleTargetPath()', () => {
66
it('should work', () => {
@@ -41,19 +41,21 @@ describe('bundleTargetPath()', () => {
4141

4242
expect(result).toEqual({
4343
entity: {
44-
$ref: '#/definitions/user',
44+
$ref: `#/${BUNDLE_ROOT}/definitions/user`,
4545
},
46-
definitions: {
47-
user: {
48-
id: 'foo',
49-
address: {
50-
$ref: '#/definitions/address',
51-
},
52-
},
53-
address: {
54-
street: 'foo',
46+
[BUNDLE_ROOT]: {
47+
definitions: {
5548
user: {
56-
$ref: '#/definitions/user',
49+
id: 'foo',
50+
address: {
51+
$ref: `#/${BUNDLE_ROOT}/definitions/address`,
52+
},
53+
},
54+
address: {
55+
street: 'foo',
56+
user: {
57+
$ref: `#/${BUNDLE_ROOT}/definitions/user`,
58+
},
5759
},
5860
},
5961
},
@@ -98,22 +100,24 @@ describe('bundleTargetPath()', () => {
98100

99101
expect(result).toEqual({
100102
entity: {
101-
$ref: '#/definitions/user',
103+
$ref: `#/${BUNDLE_ROOT}/definitions/user`,
102104
},
103105
entity2: {
104106
$ref: './path/to/pet.json',
105107
},
106-
definitions: {
107-
user: {
108-
id: 'foo',
109-
address: {
110-
$ref: '#/definitions/address',
111-
},
112-
},
113-
address: {
114-
street: 'foo',
108+
[BUNDLE_ROOT]: {
109+
definitions: {
115110
user: {
116-
$ref: '#/definitions/user',
111+
id: 'foo',
112+
address: {
113+
$ref: `#/${BUNDLE_ROOT}/definitions/address`,
114+
},
115+
},
116+
address: {
117+
street: 'foo',
118+
user: {
119+
$ref: `#/${BUNDLE_ROOT}/definitions/user`,
120+
},
117121
},
118122
},
119123
},
@@ -160,22 +164,94 @@ describe('bundleTargetPath()', () => {
160164

161165
expect(result).toEqual({
162166
entity: {
163-
$ref: '#/responses/200/schema',
167+
$ref: `#/${BUNDLE_ROOT}/responses/200/schema`,
164168
},
165-
parameters: [
166-
undefined,
167-
{
168-
schema: {
169-
name: 'param',
169+
[BUNDLE_ROOT]: {
170+
parameters: [
171+
undefined,
172+
{
173+
schema: {
174+
name: 'param',
175+
},
176+
},
177+
],
178+
responses: {
179+
'200': {
180+
schema: {
181+
title: 'OK',
182+
parameter: {
183+
$ref: `#/${BUNDLE_ROOT}/parameters/1/schema`,
184+
},
185+
},
170186
},
171187
},
172-
],
188+
},
189+
});
190+
});
191+
192+
it('should support $ref to original document that collides with path on self', () => {
193+
const document = {
194+
schemas: {
195+
user: {
196+
friend: {
197+
$ref: '#/schemas/user',
198+
},
199+
},
200+
},
173201
responses: {
174202
'200': {
203+
other: 'foo',
175204
schema: {
176-
title: 'OK',
177-
parameter: {
178-
$ref: '#/parameters/1/schema',
205+
$ref: '#/schemas/user',
206+
},
207+
},
208+
},
209+
__target__: {
210+
user: {
211+
$ref: '#/schemas/user',
212+
},
213+
responses: {
214+
'200': {
215+
// as you can see, responses/200 is a path that also exists on __target__
216+
$ref: '#/responses/200',
217+
},
218+
},
219+
},
220+
};
221+
222+
const clone = cloneDeep(document);
223+
224+
const result = bundleTarget({
225+
document: clone,
226+
path: '#/__target__',
227+
});
228+
229+
// Do not mutate document
230+
expect(clone).toEqual(document);
231+
232+
expect(result).toEqual({
233+
user: {
234+
$ref: `#/${BUNDLE_ROOT}/schemas/user`,
235+
},
236+
responses: {
237+
'200': {
238+
$ref: `#/${BUNDLE_ROOT}/responses/200`,
239+
},
240+
},
241+
[BUNDLE_ROOT]: {
242+
schemas: {
243+
user: {
244+
friend: {
245+
// check recursive
246+
$ref: `#/${BUNDLE_ROOT}/schemas/user`,
247+
},
248+
},
249+
},
250+
responses: {
251+
'200': {
252+
other: 'foo',
253+
schema: {
254+
$ref: `#/${BUNDLE_ROOT}/schemas/user`,
179255
},
180256
},
181257
},

src/bundle.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,56 @@
11
import { cloneDeep, get, has, set } from 'lodash';
22

3+
import { hasRef } from './hasRef';
34
import { isLocalRef } from './isLocalRef';
5+
import { pathToPointer } from './pathToPointer';
46
import { pointerToPath } from './pointerToPath';
57
import { traverse } from './traverse';
68

9+
export const BUNDLE_ROOT = '__bundled__';
10+
711
export const bundleTarget = <T = unknown>({ document, path }: { document: T; path: string }, cur?: unknown) =>
812
_bundle(cloneDeep(document), path, cur);
913

10-
const _bundle = (document: unknown, path: string, cur?: unknown) => {
14+
const _bundle = (document: unknown, path: string, cur?: unknown, bundledRefInventory: any = {}) => {
1115
const objectToBundle = get(document, pointerToPath(path));
1216

13-
traverse(cur ? cur : objectToBundle, ({ property, propertyValue }) => {
14-
if (property === '$ref' && typeof propertyValue === 'string' && isLocalRef(propertyValue)) {
15-
const _path = pointerToPath(propertyValue);
17+
traverse(cur ? cur : objectToBundle, ({ parent }) => {
18+
if (hasRef(parent) && isLocalRef(parent.$ref)) {
19+
if (bundledRefInventory[parent.$ref]) {
20+
parent.$ref = bundledRefInventory[parent.$ref];
21+
22+
// no need to continue, this $ref has already been bundled
23+
return;
24+
}
25+
26+
const _path = pointerToPath(parent.$ref);
27+
const inventoryPath = [BUNDLE_ROOT, ..._path];
28+
const inventoryRef = pathToPointer(inventoryPath);
29+
1630
const bundled$Ref = get(document, _path);
17-
const exists = has(objectToBundle, _path);
18-
if (bundled$Ref && !exists) {
31+
if (bundled$Ref) {
1932
const pathProcessed = [];
2033

34+
bundledRefInventory[parent.$ref] = inventoryRef;
35+
parent.$ref = inventoryRef;
36+
2137
// make sure arrays and object decisions are preserved when copying over the portion of the tree
2238
for (const key of _path) {
2339
pathProcessed.push(key);
2440

25-
if (has(objectToBundle, pathProcessed)) continue;
41+
const inventoryPathProcessed = [BUNDLE_ROOT, ...pathProcessed];
42+
if (has(objectToBundle, inventoryPathProcessed)) continue;
2643

2744
const target = get(document, pathProcessed);
2845
if (Array.isArray(target)) {
29-
set(objectToBundle, pathProcessed, []);
46+
set(objectToBundle, inventoryPathProcessed, []);
3047
} else if (typeof target === 'object') {
31-
set(objectToBundle, pathProcessed, {});
48+
set(objectToBundle, inventoryPathProcessed, {});
3249
}
3350
}
3451

35-
set(objectToBundle, _path, bundled$Ref);
36-
_bundle(document, path, bundled$Ref);
52+
set(objectToBundle, inventoryPath, bundled$Ref);
53+
_bundle(document, path, bundled$Ref, bundledRefInventory);
3754
}
3855
}
3956
});

src/traverse.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import { JsonPath } from '@stoplight/types';
22

33
export const traverse = (
44
obj: unknown,
5-
func: (opts: { parentPath: JsonPath; property: string | number; propertyValue: unknown }) => void,
5+
func: (opts: { parent: unknown; parentPath: JsonPath; property: string | number; propertyValue: unknown }) => void,
66
path: JsonPath = [],
77
) => {
88
if (!obj || typeof obj !== 'object') return;
99

1010
for (const i in obj) {
1111
if (!obj.hasOwnProperty(i)) continue;
12-
func({ parentPath: path, property: i, propertyValue: obj[i] });
12+
func({ parent: obj, parentPath: path, property: i, propertyValue: obj[i] });
1313
if (obj[i] && typeof obj[i] === 'object') {
1414
traverse(obj[i], func, path.concat(i));
1515
}

0 commit comments

Comments
 (0)