diff --git a/packages/downgrader/src/shared.ts b/packages/downgrader/src/shared.ts index 091a5d4..2896993 100644 --- a/packages/downgrader/src/shared.ts +++ b/packages/downgrader/src/shared.ts @@ -803,11 +803,12 @@ export function hasDanglingOperationRef(link: unknown, ctx: Context): boolean { } function isOperationPointer(tokens: readonly string[]): boolean { + // Read from the end, an operation key can be parsed two ways: + // `additionalOperations` keys may be lowercase, even `query` or `get`, and a + // webhook or callback expression may itself be named `additionalOperations`. const key = tokens.at(-1) as string - if ((HTTP_METHODS as readonly string[]).includes(key) || key === 'query') { - return isPathItemPointer(tokens.slice(0, -1)) - } - return tokens.at(-2) === 'additionalOperations' && isPathItemPointer(tokens.slice(0, -2)) + return (tokens.at(-2) === 'additionalOperations' && isPathItemPointer(tokens.slice(0, -2))) + || (((HTTP_METHODS as readonly string[]).includes(key) || key === 'query') && isPathItemPointer(tokens.slice(0, -1))) } function isPathItemPointer(tokens: readonly string[]): boolean { diff --git a/packages/downgrader/tests/v3.2-to-v3.1/spec/removed-parts.test.ts b/packages/downgrader/tests/v3.2-to-v3.1/spec/removed-parts.test.ts index e70eb1b..d08da1f 100644 --- a/packages/downgrader/tests/v3.2-to-v3.1/spec/removed-parts.test.ts +++ b/packages/downgrader/tests/v3.2-to-v3.1/spec/removed-parts.test.ts @@ -298,6 +298,23 @@ describe('path Item references', () => { }) }) + // The 3.2 schema only forbids uppercase method names as `additionalOperations` + // keys, so a lowercase `query` or fixed method like `get` is a valid key + // there, and pointers through it still land in a removed part. A webhook may + // itself be named `additionalOperations`, with its own removed `query`. + it.each([ + '#/paths/~1a/additionalOperations/query/callbacks/c/{$url}', + '#/paths/~1a/additionalOperations/get/callbacks/c/{$url}', + '#/webhooks/additionalOperations/query/callbacks/c/{$url}', + ])('inlines a path item $ref to %s', ($ref) => { + const callbacks = { c: { '{$url}': { description: 'inlined' } } } + expect(dig(convertSpec({ + components: { pathItems: { P: { $ref } } }, + paths: { '/a': { additionalOperations: { get: { callbacks }, query: { callbacks } } } }, + webhooks: { additionalOperations: { query: { callbacks } } }, + }), 'components', 'pathItems', 'P')).toEqual({ description: 'inlined' }) + }) + // Only pointers that actually land on a Path Item are merged as one. An // extension inside a Callback Object is not a Path Item, so a `$ref` to it // is left as written.