Skip to content

Commit

Permalink
fix: scope bundle to prevent collisions (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
marbemac authored Jun 17, 2020
1 parent a275d2a commit c0c001c
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 46 deletions.
142 changes: 109 additions & 33 deletions src/__tests__/bundle.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cloneDeep } from 'lodash';

import { bundleTarget } from '../bundle';
import { BUNDLE_ROOT, bundleTarget } from '../bundle';

describe('bundleTargetPath()', () => {
it('should work', () => {
Expand Down Expand Up @@ -41,19 +41,21 @@ describe('bundleTargetPath()', () => {

expect(result).toEqual({
entity: {
$ref: '#/definitions/user',
$ref: `#/${BUNDLE_ROOT}/definitions/user`,
},
definitions: {
user: {
id: 'foo',
address: {
$ref: '#/definitions/address',
},
},
address: {
street: 'foo',
[BUNDLE_ROOT]: {
definitions: {
user: {
$ref: '#/definitions/user',
id: 'foo',
address: {
$ref: `#/${BUNDLE_ROOT}/definitions/address`,
},
},
address: {
street: 'foo',
user: {
$ref: `#/${BUNDLE_ROOT}/definitions/user`,
},
},
},
},
Expand Down Expand Up @@ -98,22 +100,24 @@ describe('bundleTargetPath()', () => {

expect(result).toEqual({
entity: {
$ref: '#/definitions/user',
$ref: `#/${BUNDLE_ROOT}/definitions/user`,
},
entity2: {
$ref: './path/to/pet.json',
},
definitions: {
user: {
id: 'foo',
address: {
$ref: '#/definitions/address',
},
},
address: {
street: 'foo',
[BUNDLE_ROOT]: {
definitions: {
user: {
$ref: '#/definitions/user',
id: 'foo',
address: {
$ref: `#/${BUNDLE_ROOT}/definitions/address`,
},
},
address: {
street: 'foo',
user: {
$ref: `#/${BUNDLE_ROOT}/definitions/user`,
},
},
},
},
Expand Down Expand Up @@ -160,22 +164,94 @@ describe('bundleTargetPath()', () => {

expect(result).toEqual({
entity: {
$ref: '#/responses/200/schema',
$ref: `#/${BUNDLE_ROOT}/responses/200/schema`,
},
parameters: [
undefined,
{
schema: {
name: 'param',
[BUNDLE_ROOT]: {
parameters: [
undefined,
{
schema: {
name: 'param',
},
},
],
responses: {
'200': {
schema: {
title: 'OK',
parameter: {
$ref: `#/${BUNDLE_ROOT}/parameters/1/schema`,
},
},
},
},
],
},
});
});

it('should support $ref to original document that collides with path on self', () => {
const document = {
schemas: {
user: {
friend: {
$ref: '#/schemas/user',
},
},
},
responses: {
'200': {
other: 'foo',
schema: {
title: 'OK',
parameter: {
$ref: '#/parameters/1/schema',
$ref: '#/schemas/user',
},
},
},
__target__: {
user: {
$ref: '#/schemas/user',
},
responses: {
'200': {
// as you can see, responses/200 is a path that also exists on __target__
$ref: '#/responses/200',
},
},
},
};

const clone = cloneDeep(document);

const result = bundleTarget({
document: clone,
path: '#/__target__',
});

// Do not mutate document
expect(clone).toEqual(document);

expect(result).toEqual({
user: {
$ref: `#/${BUNDLE_ROOT}/schemas/user`,
},
responses: {
'200': {
$ref: `#/${BUNDLE_ROOT}/responses/200`,
},
},
[BUNDLE_ROOT]: {
schemas: {
user: {
friend: {
// check recursive
$ref: `#/${BUNDLE_ROOT}/schemas/user`,
},
},
},
responses: {
'200': {
other: 'foo',
schema: {
$ref: `#/${BUNDLE_ROOT}/schemas/user`,
},
},
},
Expand Down
39 changes: 28 additions & 11 deletions src/bundle.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,56 @@
import { cloneDeep, get, has, set } from 'lodash';

import { hasRef } from './hasRef';
import { isLocalRef } from './isLocalRef';
import { pathToPointer } from './pathToPointer';
import { pointerToPath } from './pointerToPath';
import { traverse } from './traverse';

export const BUNDLE_ROOT = '__bundled__';

export const bundleTarget = <T = unknown>({ document, path }: { document: T; path: string }, cur?: unknown) =>
_bundle(cloneDeep(document), path, cur);

const _bundle = (document: unknown, path: string, cur?: unknown) => {
const _bundle = (document: unknown, path: string, cur?: unknown, bundledRefInventory: any = {}) => {
const objectToBundle = get(document, pointerToPath(path));

traverse(cur ? cur : objectToBundle, ({ property, propertyValue }) => {
if (property === '$ref' && typeof propertyValue === 'string' && isLocalRef(propertyValue)) {
const _path = pointerToPath(propertyValue);
traverse(cur ? cur : objectToBundle, ({ parent }) => {
if (hasRef(parent) && isLocalRef(parent.$ref)) {
if (bundledRefInventory[parent.$ref]) {
parent.$ref = bundledRefInventory[parent.$ref];

// no need to continue, this $ref has already been bundled
return;
}

const _path = pointerToPath(parent.$ref);
const inventoryPath = [BUNDLE_ROOT, ..._path];
const inventoryRef = pathToPointer(inventoryPath);

const bundled$Ref = get(document, _path);
const exists = has(objectToBundle, _path);
if (bundled$Ref && !exists) {
if (bundled$Ref) {
const pathProcessed = [];

bundledRefInventory[parent.$ref] = inventoryRef;
parent.$ref = inventoryRef;

// make sure arrays and object decisions are preserved when copying over the portion of the tree
for (const key of _path) {
pathProcessed.push(key);

if (has(objectToBundle, pathProcessed)) continue;
const inventoryPathProcessed = [BUNDLE_ROOT, ...pathProcessed];
if (has(objectToBundle, inventoryPathProcessed)) continue;

const target = get(document, pathProcessed);
if (Array.isArray(target)) {
set(objectToBundle, pathProcessed, []);
set(objectToBundle, inventoryPathProcessed, []);
} else if (typeof target === 'object') {
set(objectToBundle, pathProcessed, {});
set(objectToBundle, inventoryPathProcessed, {});
}
}

set(objectToBundle, _path, bundled$Ref);
_bundle(document, path, bundled$Ref);
set(objectToBundle, inventoryPath, bundled$Ref);
_bundle(document, path, bundled$Ref, bundledRefInventory);
}
}
});
Expand Down
4 changes: 2 additions & 2 deletions src/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { JsonPath } from '@stoplight/types';

export const traverse = (
obj: unknown,
func: (opts: { parentPath: JsonPath; property: string | number; propertyValue: unknown }) => void,
func: (opts: { parent: unknown; parentPath: JsonPath; property: string | number; propertyValue: unknown }) => void,
path: JsonPath = [],
) => {
if (!obj || typeof obj !== 'object') return;

for (const i in obj) {
if (!obj.hasOwnProperty(i)) continue;
func({ parentPath: path, property: i, propertyValue: obj[i] });
func({ parent: obj, parentPath: path, property: i, propertyValue: obj[i] });
if (obj[i] && typeof obj[i] === 'object') {
traverse(obj[i], func, path.concat(i));
}
Expand Down

0 comments on commit c0c001c

Please sign in to comment.