diff --git a/src/__tests__/bundle.spec.ts b/src/__tests__/bundle.spec.ts index 3dc4334..4781b6f 100644 --- a/src/__tests__/bundle.spec.ts +++ b/src/__tests__/bundle.spec.ts @@ -1,4 +1,4 @@ -import { cloneDeep } from 'lodash'; +import { cloneDeep, get as _get } from 'lodash'; import { BUNDLE_ROOT as BUNDLE_ROOT_POINTER, bundleTarget } from '../bundle'; import { safeStringify } from '../safeStringify'; @@ -920,6 +920,155 @@ describe('bundleTargetPath()', () => { expect(Array.isArray(result.components.schemas)).toBe(false); }); + describe('when custom keyProvider is provided', () => { + it('should work', () => { + const document = { + definitions: { + user: { + id: 'foo', + address: { + $ref: '#/definitions/address', + }, + 'x-stoplight': { id: 'USER_ID' }, + }, + address: { + street: 'foo', + user: { + $ref: '#/definitions/user', + }, + city: { + $ref: '#/definitions/city', + }, + 'x-stoplight': { id: 'ADDRESS_ID' }, + }, + city: { + name: 'foo', + }, + card: { + zip: '20815', + 'x-stoplight': { id: 'CARD_ID' }, + }, + }, + __target__: { + entity: { + $ref: '#/definitions/user', + }, + }, + }; + + const clone = cloneDeep(document); + + const result = bundleTarget({ + document: clone, + path: '#/__target__', + + /** + * This example fetches the x-stoplight.id value from the object the ref path points at, and + * returns that to use as the new $ref key in the bundled object (if id is present, otherwise falls back to default logic) + */ + keyProvider: ({ document, path }) => { + const target = _get(document, path); + const id = target?.['x-stoplight']?.['id']; + return id; + }, + }); + + // Do not mutate document + expect(clone).toEqual(document); + + expect(result).toEqual({ + entity: { + $ref: `#/${BUNDLE_ROOT}/USER_ID`, + }, + [BUNDLE_ROOT]: { + USER_ID: { + id: 'foo', + address: { + $ref: `#/${BUNDLE_ROOT}/ADDRESS_ID`, + }, + 'x-stoplight': { id: 'USER_ID' }, + }, + ADDRESS_ID: { + street: 'foo', + user: { + $ref: `#/${BUNDLE_ROOT}/USER_ID`, + }, + city: { + $ref: `#/${BUNDLE_ROOT}/city`, + }, + 'x-stoplight': { id: 'ADDRESS_ID' }, + }, + city: { + name: 'foo', + }, + }, + }); + }); + + it('should increment key if duplicate', () => { + const document = { + definitions: { + business_address: { + id: 'foo', + address: { + $ref: '#/definitions/address', + }, + 'x-stoplight': { id: 'ADDRESS_ID' }, + }, + address: { + street: 'foo', + 'x-stoplight': { id: 'ADDRESS_ID' }, + }, + }, + __target__: { + entity1: { + $ref: '#/definitions/business_address', + }, + entity2: { + $ref: '#/definitions/address', + }, + }, + }; + + const clone = cloneDeep(document); + + const result = bundleTarget({ + document: clone, + path: '#/__target__', + keyProvider: ({ document, path }) => { + const target = _get(document, path); + const id = target?.['x-stoplight']?.['id']; + return id; + }, + }); + + // Do not mutate document + expect(clone).toEqual(document); + + expect(result).toEqual({ + entity1: { + $ref: `#/${BUNDLE_ROOT}/ADDRESS_ID`, + }, + entity2: { + $ref: `#/${BUNDLE_ROOT}/ADDRESS_ID_2`, + }, + [BUNDLE_ROOT]: { + ADDRESS_ID: { + id: 'foo', + address: { + $ref: `#/${BUNDLE_ROOT}/ADDRESS_ID_2`, + }, + 'x-stoplight': { id: 'ADDRESS_ID' }, + }, + ADDRESS_ID_2: { + street: 'foo', + 'x-stoplight': { id: 'ADDRESS_ID' }, + }, + }, + }); + }); + }); + describe('when custom bundleRoot is provided', () => { it('should work', () => { const bundleRoot = '#/__custom-root__'; diff --git a/src/bundle.ts b/src/bundle.ts index fbe900b..c823b29 100644 --- a/src/bundle.ts +++ b/src/bundle.ts @@ -11,6 +11,8 @@ import { traverse } from './traverse'; export const BUNDLE_ROOT = '#/__bundled__'; export const ERRORS_ROOT = '#/__errors__'; +type KeyProviderFn = (props: { document: unknown; path: JsonPath }) => string | void | undefined | null; + export const bundleTarget = ( { document, @@ -18,12 +20,14 @@ export const bundleTarget = ( bundleRoot = BUNDLE_ROOT, errorsRoot = ERRORS_ROOT, cloneDocument = true, + keyProvider, }: { document: T; path: string; bundleRoot?: string; errorsRoot?: string; cloneDocument?: boolean; + keyProvider?: KeyProviderFn; }, cur?: unknown, ) => { @@ -32,10 +36,24 @@ export const bundleTarget = ( } const workingDocument = cloneDocument ? cloneDeep(document) : document; - return bundle(workingDocument, pointerToPath(bundleRoot), pointerToPath(errorsRoot))(path, { [path]: true }, cur); + return bundle( + workingDocument, + pointerToPath(bundleRoot), + pointerToPath(errorsRoot), + keyProvider, + )(path, { [path]: true }, cur); }; -const bundle = (document: unknown, bundleRoot: JsonPath, errorsRoot: JsonPath) => { +const defaultKeyProvider = ({ document, path }: { document: unknown; path: JsonPath }) => { + if (Array.isArray(get(document, path.slice(0, -1)))) { + const inventoryKeyRoot = path[path.length - 2]; + return `${inventoryKeyRoot}_${path[path.length - 1]}`; + } else { + return String(path[path.length - 1]); + } +}; + +const bundle = (document: unknown, bundleRoot: JsonPath, errorsRoot: JsonPath, keyProvider?: KeyProviderFn) => { const takenKeys = new Set(); const _bundle = ( @@ -70,12 +88,12 @@ const bundle = (document: unknown, bundleRoot: JsonPath, errorsRoot: JsonPath) = _path = pointerToPath($ref); let _inventoryKey; + if (keyProvider) { + _inventoryKey = keyProvider({ document, path: _path }); + } - if (Array.isArray(get(document, _path.slice(0, -1)))) { - const inventoryKeyRoot = _path[_path.length - 2]; - _inventoryKey = `${inventoryKeyRoot}_${_path[_path.length - 1]}`; - } else { - _inventoryKey = _path[_path.length - 1]; + if (!_inventoryKey) { + _inventoryKey = defaultKeyProvider({ document, path: _path }); } inventoryKey = _inventoryKey;