Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/apitypes/rest/rest.operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.
Copy link
Collaborator

@JayLim2 JayLim2 Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(optional) comment duplication

// We should merge these two functions into one.
export const createSingleOperationSpec = (
document: OpenAPIV3.Document,
path: string,
Expand All @@ -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',
Copy link
Collaborator

@JayLim2 JayLim2 Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(optional) don't we have the constant with the value?

...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: {
Expand All @@ -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<OpenAPIV3.PathItemObject, 'summary' | 'description' | 'servers' | 'parameters'> => ({
Expand Down
3 changes: 3 additions & 0 deletions src/apitypes/rest/rest.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,13 @@ export type VersionRestOperation = ApiOperation<RestOperationData, RestOperation

export interface RestOperationData {
openapi: string
info?: OpenAPIV3.InfoObject
servers?: OpenAPIV3.ServerObject[]
paths: OpenAPIV3.PathsObject
components?: OpenAPIV3.ComponentsObject
security?: OpenAPIV3.SecurityRequirementObject[]
externalDocs?: OpenAPIV3.ExternalDocumentationObject
tags?: OpenAPIV3.TagObject[]
}

export interface RestRefCache {
Expand Down
20 changes: 10 additions & 10 deletions src/utils/mergeOpenapiDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const mergeOpenapiDocuments = (documents: OpenAPIV3.Document[], info: Ope
paths: merged.paths,
components: merged.components,
security: template?.security || merged.security,
...takeIfDefined({ tags: getTags(documents) }),
...takeIfDefined({ tags: getUsedTags(documents) }),
...takeIfDefined({ externalDocs: template?.externalDocs || getExternalDocs(merged, diffs) }),
}
}
Expand Down Expand Up @@ -112,9 +112,9 @@ function extractDeclarationPaths(diff: Diff): JsonPath[] {

function compliesWithRules(rules: DiffRule[], diff: Diff): boolean {
return rules.some(allowedDiff => {
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)
},
)
}

Expand All @@ -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[] = []
Expand Down Expand Up @@ -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 }),
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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: [email protected]
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
41 changes: 41 additions & 0 deletions test/rest.operation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
})
})