diff --git a/src/apitypes/rest/rest.operation.ts b/src/apitypes/rest/rest.operation.ts index 1b64c74..f2245ed 100644 --- a/src/apitypes/rest/rest.operation.ts +++ b/src/apitypes/rest/rest.operation.ts @@ -47,6 +47,7 @@ import { takeIf, takeIfDefined, } from '../../utils' +import { getUsedTags } from '../../utils/mergeOpenapiDocuments' import { API_KIND, INLINE_REFS_FLAG, ORIGINS_SYMBOL, VERSION_STATUS } from '../../consts' import { extractSecuritySchemesNames, getCustomTags, resolveApiAudience } from './rest.utils' import { DebugPerformanceContext, syncDebugPerformance } from '../../utils/logs' @@ -328,6 +329,9 @@ const isOperationPaths = (paths: JsonPath[]): boolean => { // todo output of this method disrupts document normalization. // origin symbols are not being transferred to the resulting spec. // DO NOT pass output of this method to apiDiff +// TODO: conceptually, this method does processing which is very similar +// is very similar to the reducedSourceSpecifications transformation. +// We should merge these two functions into one. export const createSingleOperationSpec = ( document: OpenAPIV3.Document, path: string, @@ -350,8 +354,12 @@ export const createSingleOperationSpec = ( : undefined const isRefPathData = !!pathData.$ref - return { + + // Construct the single operation document + const singleOperationDocument: TYPE.RestOperationData = { openapi: openapi ?? '3.0.0', + ...takeIfDefined({ info: document.info }), + ...takeIfDefined({ externalDocs: document.externalDocs }), ...takeIfDefined({ servers }), ...!operationSecurity ? takeIfDefined({ security }) : {},// Only add root security if operation security is not explicitly defined paths: { @@ -367,8 +375,20 @@ export const createSingleOperationSpec = ( components: effectiveSecuritySchemes ? { securitySchemes: effectiveSecuritySchemes } : undefined, }), } -} + // Filter tags to only include those used by this operation + if (document.tags) { + const filteredTags = getUsedTags([{ + ...singleOperationDocument, + tags: document.tags, + } as OpenAPIV3.Document]) + if (filteredTags) { + singleOperationDocument.tags = filteredTags + } + } + + return singleOperationDocument +} export const extractCommonPathItemProperties = ( pathData: OpenAPIV3.PathItemObject, ): Pick => ({ diff --git a/src/apitypes/rest/rest.types.ts b/src/apitypes/rest/rest.types.ts index 2bc4ed1..dafa743 100644 --- a/src/apitypes/rest/rest.types.ts +++ b/src/apitypes/rest/rest.types.ts @@ -47,10 +47,13 @@ export type VersionRestOperation = ApiOperation { - const matchResult = matchPaths(extractDeclarationPaths(diff), allowedDiff.pathTemplate) - return matchResult?.path && allowedDiff.allowedActions.includes(diff.action) - }, + const matchResult = matchPaths(extractDeclarationPaths(diff), allowedDiff.pathTemplate) + return matchResult?.path && allowedDiff.allowedActions.includes(diff.action) + }, ) } @@ -128,12 +128,12 @@ const validateResult = (rules: DiffRule[], diffs: Diff[], title1: string, title2 if (firstProhibitedDiff) { throw new Error(`Unable to merge ${trimPath(firstPathFromProhibitedDiff).join('.')}. These specifications have different content for it: ${title1}, ${title2}. - Please resolve the conflicts in source specification, republish them and try again. You can also download reduced source specifications and merge operations manually.`, + Please resolve the conflicts in source specification, republish them and try again. You can also download reduced source specifications and merge operations manually.`, ) } } -function getTags(specs: OpenAPIV3.Document[]): OpenAPIV3.TagObject[] | undefined { +export function getUsedTags(specs: OpenAPIV3.Document[]): OpenAPIV3.TagObject[] | undefined { const tagsWithUsages = specs .map(({ tags, paths }) => { const specTags: string[] = [] @@ -198,10 +198,10 @@ function prepareTemplate(openapi: string, template?: ExportTemplate): ExportTemp info, ...takeIfDefined({ servers }), ...takeIf({ - components: { - securitySchemes, - }, - }, !!securitySchemes, + components: { + securitySchemes, + }, + }, !!securitySchemes, ), ...takeIfDefined({ security }), ...takeIfDefined({ externalDocs }), diff --git a/test/projects/rest.operation/info-externaldocs-no-tags/base.yaml b/test/projects/rest.operation/info-externaldocs-no-tags/base.yaml new file mode 100644 index 0000000..7ac4466 --- /dev/null +++ b/test/projects/rest.operation/info-externaldocs-no-tags/base.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.3 +info: + title: Info and ExternalDocs without Tags Test + version: 1.0.0 + description: Test document for info and externalDocs when no tags are present +externalDocs: + description: API Documentation + url: https://docs.example.com +paths: + /test: + get: + summary: Test operation without tags + responses: + "200": + description: OK diff --git a/test/projects/rest.operation/info-externaldocs-tags-filtering/base.yaml b/test/projects/rest.operation/info-externaldocs-tags-filtering/base.yaml new file mode 100644 index 0000000..c86e812 --- /dev/null +++ b/test/projects/rest.operation/info-externaldocs-tags-filtering/base.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.3 +info: + title: Info, ExternalDocs, and Tags Filtering Test + version: 1.0.0 + description: Test document for filtering info, externalDocs, and tags + contact: + name: API Support + email: support@example.com +externalDocs: + description: API Documentation + url: https://docs.example.com +tags: + - name: pet + description: Pet operations + - name: store + description: Store operations + - name: user + description: User operations +paths: + /test: + get: + summary: Test operation + tags: + - pet + responses: + "200": + description: OK diff --git a/test/rest.operation.test.ts b/test/rest.operation.test.ts index 9d0b70b..7718952 100644 --- a/test/rest.operation.test.ts +++ b/test/rest.operation.test.ts @@ -122,5 +122,46 @@ describe('REST Operation Unit Tests', () => { expect(result.security).toEqual(document.security) }) }) + + describe('Info, ExternalDocs, and Tags Handling', () => { + test('should include info object from source document', async () => { + const document = await loadYamlFile('rest.operation/info-externaldocs-tags-filtering/base.yaml') + + const result = createTestSingleOperationSpec(document) + + expect(result.info).toBeDefined() + expect(result.info).toEqual(document.info) + }) + + test('should include externalDocs object from source document', async () => { + const document = await loadYamlFile('rest.operation/info-externaldocs-tags-filtering/base.yaml') + + const result = createTestSingleOperationSpec(document) + + expect(result.externalDocs).toBeDefined() + expect(result.externalDocs).toEqual(document.externalDocs) + }) + + test('should filter tags to only include those used by the operation', async () => { + const document = await loadYamlFile('rest.operation/info-externaldocs-tags-filtering/base.yaml') + + const result = createTestSingleOperationSpec(document) + + expect(result.tags).toEqual([ + { + name: 'pet', + description: 'Pet operations', + }, + ]) + }) + + test('should handle document with no tags', async () => { + const document = await loadYamlFile('rest.operation/info-externaldocs-no-tags/base.yaml') + + const result = createTestSingleOperationSpec(document) + + expect(result.tags).toBeUndefined() + }) + }) }) })