diff --git a/src/__tests__/bundle.spec.ts b/src/__tests__/bundle.spec.ts index 4781b6f..705d867 100644 --- a/src/__tests__/bundle.spec.ts +++ b/src/__tests__/bundle.spec.ts @@ -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', }), ); }); @@ -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', }), ); }); @@ -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 = { diff --git a/src/bundle.ts b/src/bundle.ts index c823b29..1cab2c4 100644 --- a/src/bundle.ts +++ b/src/bundle.ts @@ -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);