Skip to content

Commit

Permalink
fix(bundleTarget): improve # $ref handling, and preserve circular ind…
Browse files Browse the repository at this point in the history
…irect $refs (#115)
  • Loading branch information
marbemac authored Aug 5, 2022
1 parent 3d2ab04 commit 38a1a87
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 80 deletions.
143 changes: 122 additions & 21 deletions src/__tests__/bundle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,28 +599,27 @@ describe('bundleTargetPath()', () => {

expect(safeStringify(result)).toEqual(
safeStringify({
title: 'Hello',
type: 'object',
properties: {
Hello: {
$ref: `#`,
},
World: {
$ref: `#/${BUNDLE_ROOT}/World`,
},
},
[BUNDLE_ROOT]: {
Hello: '[Circular]',
World: {
title: 'World',
type: 'object',
properties: {
name: {
type: 'string',
},
},
title: 'World',
type: 'object',
},
},
properties: {
Hello: {
$ref: `#/${BUNDLE_ROOT}/Hello`,
},
World: {
$ref: `#/${BUNDLE_ROOT}/World`,
},
},
title: 'Hello',
type: 'object',
}),
);
});
Expand Down Expand Up @@ -659,21 +658,20 @@ describe('bundleTargetPath()', () => {

expect(safeStringify(result)).toEqual(
safeStringify({
__bundled__: {
GeographicalCoordinate: {
type: 'object',
},
Location: '[Circular]',
},
type: 'object',
properties: {
PhysicalGeographicalCoordinate: {
$ref: '#/__bundled__/GeographicalCoordinate',
},
RelatedLocation: {
$ref: '#/__bundled__/Location',
$ref: '#',
},
},
__bundled__: {
GeographicalCoordinate: {
type: 'object',
},
},
type: 'object',
}),
);
});
Expand Down Expand Up @@ -920,6 +918,109 @@ describe('bundleTargetPath()', () => {
expect(Array.isArray(result.components.schemas)).toBe(false);
});

it('should rewrite $refs that point at the bundleRoot, to #', () => {
const document = {
__target__: {
type: 'object',
properties: {
circular: {
$ref: '#/__target__',
},
},
},
};

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

expect(result).toEqual({
type: 'object',
properties: {
circular: {
$ref: '#',
},
},
});
});

it('should handle when the target is a direct $ref to itself', () => {
const document = {
__target__: {
$ref: '#/__target__',
description: 'Test',
},
};

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

expect(result).toEqual({
$ref: '#',
description: 'Test',
});
});

it('should handle and preserve circular indirect relationships', () => {
const document = {
definitions: {
User: {
$ref: '#/definitions/Editor',
description: 'Some User with a basic set of permissions',
},
Admin: {
$ref: '#/definitions/User',
description: 'Some User with an elevated set of permissions',
},
Editor: {
$ref: '#/definitions/Admin',
description: 'Some User with write permissions',
},
},
__target__: {
type: 'object',
properties: {
user: {
$ref: '#/definitions/Editor',
},
},
required: ['user'],
},
};

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

expect(result).toEqual({
type: 'object',
properties: {
user: {
$ref: `#/${BUNDLE_ROOT}/Editor`,
},
},
required: ['user'],
[BUNDLE_ROOT]: {
User: {
$ref: `#/${BUNDLE_ROOT}/Editor`,
description: 'Some User with a basic set of permissions',
},
Admin: {
$ref: `#/${BUNDLE_ROOT}/User`,
description: 'Some User with an elevated set of permissions',
},
Editor: {
$ref: `#/${BUNDLE_ROOT}/Admin`,
description: 'Some User with write permissions',
},
},
});
});

describe('when custom keyProvider is provided', () => {
it('should work', () => {
const document = {
Expand Down
128 changes: 69 additions & 59 deletions src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,89 +67,99 @@ const bundle = (document: unknown, bundleRoot: JsonPath, errorsRoot: JsonPath, k
const $refTarget = pointerToPath(path);
const objectToBundle = get(document, $refTarget);

traverse(cur ? cur : objectToBundle, ({ parent }) => {
if (hasRef(parent) && isLocalRef(parent.$ref)) {
const $ref = parent.$ref;
if (errorsObj[$ref]) return;
traverse(cur ? cur : objectToBundle, {
onEnter: ({ value: parent }) => {
if (hasRef(parent) && isLocalRef(parent.$ref)) {
const $ref = parent.$ref;
if (errorsObj[$ref]) return;

if ($ref === path) {
bundledRefInventory[$ref] = '#';
}

if (bundledRefInventory[$ref]) {
parent.$ref = bundledRefInventory[$ref];
if (bundledRefInventory[$ref]) {
parent.$ref = bundledRefInventory[$ref];

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

let _path;
let inventoryPath;
let inventoryKey;
let inventoryRef;
let _path;
let inventoryPath;
let inventoryKey;
let inventoryRef;

try {
_path = pointerToPath($ref);
try {
_path = pointerToPath($ref);

let _inventoryKey;
if (keyProvider) {
_inventoryKey = keyProvider({ document, path: _path });
}
let _inventoryKey;
if (keyProvider) {
_inventoryKey = keyProvider({ document, path: _path });
}

if (!_inventoryKey) {
_inventoryKey = defaultKeyProvider({ document, path: _path });
}
if (!_inventoryKey) {
_inventoryKey = defaultKeyProvider({ document, path: _path });
}

inventoryKey = _inventoryKey;
inventoryKey = _inventoryKey;

let i = 1;
while (takenKeys.has(inventoryKey)) {
i++;
inventoryKey = `${_inventoryKey}_${i}`;
let i = 1;
while (takenKeys.has(inventoryKey)) {
i++;
inventoryKey = `${_inventoryKey}_${i}`;

if (i > 20) {
throw new Error(`Keys ${_inventoryKey}_2 through ${_inventoryKey}_${20} already taken.`);
if (i > 20) {
throw new Error(`Keys ${_inventoryKey}_2 through ${_inventoryKey}_${20} already taken.`);
}
}
}

takenKeys.add(inventoryKey);
takenKeys.add(inventoryKey);

inventoryPath = [...bundleRoot, inventoryKey];
inventoryPath = [...bundleRoot, inventoryKey];

inventoryRef = pathToPointer(inventoryPath);
} catch (error) {
errorsObj[$ref] = error instanceof Error ? error.message : String(error);
}
inventoryRef = pathToPointer(inventoryPath);
} catch (error) {
errorsObj[$ref] = error instanceof Error ? error.message : String(error);
}

// Ignore invalid $refs and carry on
if (!_path || !inventoryPath || !inventoryRef) return;
// Ignore invalid $refs and carry on
if (!_path || !inventoryPath || !inventoryRef) return;

let bundled$Ref: unknown;
if (typeof document === 'object' && document !== null) {
try {
bundled$Ref = resolveInlineRef(Object(document), $ref);
} catch {
let bundled$Ref: unknown;
if (typeof document === 'object' && document !== null) {
// check the simple way first, to preserve these relationships when possible
bundled$Ref = get(document, _path);

if (!bundled$Ref) {
try {
// if we could not find it with a simple lookup, check for deep refs etc via resolveInlineRef
bundled$Ref = resolveInlineRef(Object(document), $ref);
} catch {}
}
}
}

if (bundled$Ref !== void 0) {
bundledRefInventory[$ref] = inventoryRef;
parent.$ref = inventoryRef;
if (bundled$Ref !== void 0) {
bundledRefInventory[$ref] = inventoryRef;
parent.$ref = inventoryRef;

if (!has(bundledObj, inventoryPath)) {
if (Array.isArray(bundled$Ref)) {
set(bundledObj, inventoryPath, new Array(bundled$Ref.length).fill(null));
} else if (typeof bundled$Ref === 'object') {
setWith(bundledObj, inventoryPath, {}, Object);
}
if (!has(bundledObj, inventoryPath)) {
if (Array.isArray(bundled$Ref)) {
set(bundledObj, inventoryPath, new Array(bundled$Ref.length).fill(null));
} else if (typeof bundled$Ref === 'object') {
setWith(bundledObj, inventoryPath, {}, Object);
}

set(bundledObj, inventoryPath, bundled$Ref);
set(bundledObj, inventoryPath, bundled$Ref);

if (!stack[$ref]) {
stack[$ref] = true;
_bundle(path, stack, bundled$Ref, bundledRefInventory, bundledObj, errorsObj);
stack[$ref] = false;
if (!stack[$ref]) {
stack[$ref] = true;
_bundle(path, stack, bundled$Ref, bundledRefInventory, bundledObj, errorsObj);
stack[$ref] = false;
}
}
}
}
}
},
});

const finalObjectToBundle = get(bundledObj, bundleRoot);
Expand Down

0 comments on commit 38a1a87

Please sign in to comment.