From 84b7fe65c5b45005e5d8c7c142bf6e12662779fd Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Thu, 23 Jan 2025 14:13:38 -0800 Subject: [PATCH] fix(oas): account for deep `$ref` pointers when reducing an API def (#926) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | 🚥 Resolves https://github.com/readmeio/oas/issues/925 | | :------------------- | ## 🧰 Changes This updates `oas/reducer` to account for deep `$ref` pointers like `#/components/examples/event-min/value`. Normally when we're running through components to remove we only look at `#/components/examples/event-min`, however when a schema like this is deeply referenced we won't pick up that it's used and end up removing `event-min` from the components block, resulting in a corrupted schema that will no longer validate. --- packages/oas/src/reducer/index.ts | 19 ++++++++++++++++++- packages/oas/test/reducer/index.test.ts | 17 +---------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/oas/src/reducer/index.ts b/packages/oas/src/reducer/index.ts index 0d80c430..093b9932 100644 --- a/packages/oas/src/reducer/index.ts +++ b/packages/oas/src/reducer/index.ts @@ -39,6 +39,12 @@ function accumulateUsedRefs(schema: Record, $refs: Set, } getUsedRefs($refSchema).forEach(({ value: currRef }) => { + // Because it's possible to have a parameter named `$ref`, which our lookup would pick up as a + // false positive, we want to exclude that from `$ref` matching as it's not really a reference. + if (typeof currRef !== 'string') { + return; + } + // If we've already processed this $ref don't send us into an infinite loop. if ($refs.has(currRef)) { return; @@ -178,7 +184,18 @@ export default function reducer(definition: OASDocument, opts: ReducerOptions = if ('components' in reduced) { Object.keys(reduced.components).forEach((componentType: keyof ComponentsObject) => { Object.keys(reduced.components[componentType]).forEach(component => { - if (!$refs.has(`#/components/${componentType}/${component}`)) { + // If our `$ref` either is a full, or deep match, then we should preserve it. + const refIsUsed = + $refs.has(`#/components/${componentType}/${component}`) || + Array.from($refs).some(ref => { + // Because you can have a `$ref` like `#/components/examples/event-min/value`, which + // would be accumulated via our `$refs` query, we want to make sure we account for them. + // If we don't look for these then we'll end up removing them from the overall reduced + // definition, resulting in data loss and schema corruption. + return ref.startsWith(`#/components/${componentType}/${component}/`); + }); + + if (!refIsUsed) { delete reduced.components[componentType][component]; } }); diff --git a/packages/oas/test/reducer/index.test.ts b/packages/oas/test/reducer/index.test.ts index 8d2bb638..22a171ea 100644 --- a/packages/oas/test/reducer/index.test.ts +++ b/packages/oas/test/reducer/index.test.ts @@ -1,7 +1,5 @@ import type { OASDocument } from '../../src/types.js'; -import { inspect } from 'node:util'; - import swagger from '@readme/oas-examples/2.0/json/petstore.json'; import parametersCommon from '@readme/oas-examples/3.0/json/parameters-common.json'; import petstore from '@readme/oas-examples/3.0/json/petstore.json'; @@ -17,16 +15,6 @@ import reduceQuirks from '../__datasets__/reduce-quirks.json'; import securityRootLevel from '../__datasets__/security-root-level.json'; import tagQuirks from '../__datasets__/tag-quirks.json'; -declare global { - interface Console { - logx: any; - } -} - -console.logx = (obj: any) => { - console.log(inspect(obj, false, null, true)); -}; - describe('reducer', () => { it('should not do anything if no reducers are supplied', () => { expect(reducer(petstore as any)).toStrictEqual(petstore as any); @@ -207,10 +195,7 @@ describe('reducer', () => { expect(Object.keys(reduced.paths['/anything'])).toStrictEqual(['get', 'post']); }); - /** - * @see {@link https://github.com/readmeio/oas/issues/925} - */ - it.skip('should preserved deeply nested `example` refs', () => { + it('should preserved deeply nested `example` refs', () => { const reduced = reducer(reduceQuirks as any, { paths: { '/events': ['get'],