Skip to content

Commit 5661ae0

Browse files
committed
feat: Fixed merge pathItems operations
1 parent 050f7ae commit 5661ae0

File tree

12 files changed

+266
-41
lines changed

12 files changed

+266
-41
lines changed

src/apitypes/rest/rest.operation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { JsonPath, syncCrawl } from '@netcracker/qubership-apihub-json-crawl'
18-
import { OpenAPIV3 } from 'openapi-types'
18+
import { OpenAPIV3 } from 'openapi-types'
1919
import { REST_API_TYPE, REST_KIND_KEY } from './rest.consts'
2020
import { operationRules } from './rest.rules'
2121
import type * as TYPE from './rest.types'
@@ -59,6 +59,7 @@ import {
5959
} from '@netcracker/qubership-apihub-api-unifier'
6060
import { calculateObjectHash } from '../../utils/hashes'
6161
import { calculateTolerantHash } from '../../components/deprecated'
62+
import { getValueByPath } from '../../utils/path'
6263

6364
export const buildRestOperation = (
6465
operationId: string,
@@ -307,7 +308,6 @@ const createSingleOperationSpec = (
307308
}
308309
}
309310

310-
const getValueByPath = (value: any, path: JsonPath): any => path.reduce((data, key) => data[key], value)
311311
export const extractCommonPathItemProperties = (
312312
pathData: OpenAPIV3.PathItemObject,
313313
): Pick<OpenAPIV3.PathItemObject, 'summary' | 'description' | 'servers' | 'parameters'> => ({

src/strategies/document-group.strategy.ts

Lines changed: 102 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
EXPORT_FORMAT_TO_FILE_FORMAT,
3030
fromBase64,
3131
removeFirstSlash,
32+
setValueByPath,
3233
slugify,
3334
takeIfDefined,
3435
toVersionDocument,
@@ -37,25 +38,19 @@ import { OpenAPIV3 } from 'openapi-types'
3738
import { getOperationBasePath } from '../apitypes/rest/rest.utils'
3839
import { VersionRestDocument } from '../apitypes/rest/rest.types'
3940
import { FILE_FORMAT_JSON, INLINE_REFS_FLAG, NORMALIZE_OPTIONS } from '../consts'
40-
import { normalize } from '@netcracker/qubership-apihub-api-unifier'
41+
import { normalize, parseRef } from '@netcracker/qubership-apihub-api-unifier'
4142
import { calculateSpecRefs, extractCommonPathItemProperties } from '../apitypes/rest/rest.operation'
43+
import { getValueByPath } from '../utils/path'
4244

4345
function getTransformedDocument(document: ResolvedGroupDocument, format: FileFormat, packages: ResolvedReferenceMap): VersionRestDocument {
4446
const versionDocument = toVersionDocument(document, format)
4547

46-
const source = extractDocumentData(versionDocument)
48+
const sourceDocument = extractDocumentData(versionDocument)
4749
versionDocument.data = transformDocumentData(versionDocument)
48-
const normalizedDocument = normalize(
49-
versionDocument.data,
50-
{
51-
...NORMALIZE_OPTIONS,
52-
inlineRefsFlag: INLINE_REFS_FLAG,
53-
source,
54-
},
55-
) as OpenAPIV3.Document
50+
const normalizedDocument = normalizeOpenApi(versionDocument.data, sourceDocument)
5651
versionDocument.publish = true
5752

58-
calculateSpecRefs(source, normalizedDocument, versionDocument.data)
53+
calculateSpecRefs(sourceDocument, normalizedDocument, versionDocument.data)
5954

6055
// dashboard case
6156
if (document.packageRef) {
@@ -116,48 +111,119 @@ function extractDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docume
116111
}
117112

118113
function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Document {
114+
const sourceDocument = extractDocumentData(versionDocument)
115+
const normalizedDocument = normalizeOpenApi(sourceDocument)
119116

120-
const documentData = extractDocumentData(versionDocument)
121-
const { paths, components, ...rest } = documentData
122-
const result: OpenAPIV3.Document = {
123-
...rest,
117+
const { paths: sourcePaths, components: sourceComponents, ...restOfSource } = sourceDocument
118+
const { paths: normalizedPaths } = normalizedDocument
119+
120+
const resultDocument: OpenAPIV3.Document = {
121+
...restOfSource,
124122
paths: {},
125123
}
126124

127-
for (const path of Object.keys(paths)) {
128-
const pathData = paths[path]
129-
if (typeof pathData !== 'object' || !pathData) {
125+
for (const path of Object.keys(normalizedPaths)) {
126+
const sourcePathItem = sourcePaths[path]
127+
const normalizedPathItem = normalizedPaths[path]
128+
129+
if (!isNonNullObject(sourcePathItem) || !isNonNullObject(normalizedPathItem)) {
130130
continue
131131
}
132-
const commonPathItemProperties = extractCommonPathItemProperties(pathData)
133132

134-
for (const method of Object.keys(pathData)) {
135-
const inferredMethod = method as OpenAPIV3.HttpMethods
133+
const commonPathProps = extractCommonPathItemProperties(sourcePathItem)
134+
const pathItemRef = '$ref' in sourcePathItem ? sourcePathItem.$ref : undefined
136135

137-
// check if field is a valid openapi http method defined in OpenAPIV3.HttpMethods
138-
if (!Object.values(OpenAPIV3.HttpMethods).includes(inferredMethod)) {
136+
for (const method of Object.keys(normalizedPathItem)) {
137+
const inferredMethod = method as OpenAPIV3.HttpMethods
138+
if (!isValidHttpMethod(inferredMethod)) {
139139
continue
140140
}
141141

142-
const methodData = pathData[inferredMethod]
143-
const basePath = getOperationBasePath(methodData?.servers || pathData?.servers || documentData?.servers || [])
142+
const methodData = normalizedPathItem[inferredMethod]
143+
const basePath = getOperationBasePath(methodData?.servers || sourcePathItem?.servers || sourcePathItem?.servers || [])
144144
const operationPath = basePath + path
145145

146146
const operationId = slugify(`${removeFirstSlash(operationPath)}-${method}`)
147147

148-
if (versionDocument.operationIds.includes(operationId)) {
149-
const pathData = documentData.paths[path]!
150-
result.paths[path] = {
151-
...result.paths[path],
152-
...commonPathItemProperties,
153-
[inferredMethod]: { ...pathData[inferredMethod] },
154-
}
155-
result.components = {
156-
...takeIfDefined({ securitySchemes: components?.securitySchemes }),
157-
}
148+
if (!versionDocument.operationIds.includes(operationId)) {
149+
continue
150+
}
151+
152+
const { updatedPathItem, extraComponents } = buildPathAndComponents(
153+
sourceDocument,
154+
path,
155+
inferredMethod,
156+
commonPathProps,
157+
pathItemRef,
158+
)
159+
160+
resultDocument.paths[path] = {
161+
...(resultDocument.paths[path] || {}),
162+
...updatedPathItem,
163+
}
164+
resultDocument.components = {
165+
...takeIfDefined({ securitySchemes: sourceComponents?.securitySchemes }),
166+
...extraComponents,
158167
}
159168
}
160169
}
161170

162-
return result
171+
return resultDocument
172+
}
173+
174+
function normalizeOpenApi(document: OpenAPIV3.Document, source?: OpenAPIV3.Document): OpenAPIV3.Document {
175+
return normalize(
176+
document,
177+
{
178+
...NORMALIZE_OPTIONS,
179+
inlineRefsFlag: INLINE_REFS_FLAG,
180+
...(source ? { source } : {}),
181+
},
182+
) as OpenAPIV3.Document
183+
}
184+
185+
function isValidHttpMethod(method: string): method is OpenAPIV3.HttpMethods {
186+
return (Object.values(OpenAPIV3.HttpMethods) as string[]).includes(method)
187+
}
188+
189+
function isNonNullObject(value: unknown): value is Record<string, unknown> {
190+
return typeof value === 'object' && value !== null
191+
}
192+
193+
function buildPathAndComponents(
194+
sourceDocument: OpenAPIV3.Document,
195+
path: string,
196+
method: OpenAPIV3.HttpMethods,
197+
commonPathProps: Partial<OpenAPIV3.PathItemObject>,
198+
pathItemRef?: string,
199+
): { updatedPathItem: OpenAPIV3.PathItemObject; extraComponents?: OpenAPIV3.ComponentsObject } {
200+
if (!pathItemRef) {
201+
const originalPathItem = sourceDocument.paths[path]!
202+
return {
203+
updatedPathItem: {
204+
...commonPathProps,
205+
[method]: { ...originalPathItem[method] },
206+
} as OpenAPIV3.PathItemObject,
207+
}
208+
}
209+
210+
const { jsonPath } = parseRef(pathItemRef)
211+
const targetPathItem = getValueByPath(sourceDocument, jsonPath) as OpenAPIV3.PathItemObject
212+
const resolvedPathItem = {
213+
...extractCommonPathItemProperties(targetPathItem),
214+
[method]: { ...targetPathItem[method] },
215+
}
216+
const componentsContainer: any = {}
217+
setValueByPath(componentsContainer, jsonPath, resolvedPathItem)
218+
219+
const originalPathItem = sourceDocument.paths[path]!
220+
const mergedPathItem: OpenAPIV3.PathItemObject = {
221+
...(originalPathItem),
222+
...commonPathProps,
223+
}
224+
225+
return {
226+
updatedPathItem: mergedPathItem,
227+
extraComponents: componentsContainer.components,
228+
}
163229
}

src/utils/path.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,6 @@ export function areDeclarationPathsEqual(firstItemDeclarationPaths: JsonPath[],
6464

6565
return true
6666
}
67+
68+
export const getValueByPath = (value: any, path: JsonPath): any => path.reduce((data, key) => data[key], value)
69+

test/document-group.test.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,32 @@ describe('Document Group test', () => {
4444
})
4545

4646
test('should have properly merged documents', async () => {
47-
const pkg = LocalRegistry.openPackage('merge-operations', groupToOperationIdsMap)
47+
await runMergeOperationsCase('case1')
48+
})
49+
50+
test('should have properly merged documents with pathItems', async () => {
51+
await runMergeOperationsCase('case2')
52+
})
53+
54+
async function runMergeOperationsCase(caseName: string): Promise<void> {
55+
const pkg = LocalRegistry.openPackage(`merge-operations/${caseName}`, groupToOperationIdsMap)
4856
const editor = await Editor.openProject(pkg.packageId, pkg)
4957

5058
await pkg.publish(pkg.packageId, { packageId: pkg.packageId })
59+
5160
const result = await editor.run({
5261
packageId: pkg.packageId,
5362
buildType: BUILD_TYPE.MERGED_SPECIFICATION,
5463
groupName: GROUP_NAME,
5564
apiType: REST_API_TYPE,
5665
})
57-
const expectedResult = load((await loadFileAsString(pkg.projectsDir, pkg.packageId, EXPECTED_RESULT_FILE))!)
66+
67+
const expectedResult = load(
68+
(await loadFileAsString(pkg.projectsDir, pkg.packageId, EXPECTED_RESULT_FILE))!,
69+
)
70+
5871
expect(result.merged?.data).toEqual(expectedResult)
59-
})
72+
}
6073

6174
test('should rename documents with matching names', async () => {
6275
const dashboard = LocalRegistry.openPackage('documents-collision', groupToOperationIdsMap2)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
openapi: "3.1.0"
2+
info:
3+
title: test
4+
version: 0.1.0
5+
paths:
6+
/path1:
7+
$ref: '#/components/pathItems/pathItem1'
8+
components:
9+
pathItems:
10+
pathItem1:
11+
summary: path item summary
12+
description: path item description
13+
servers:
14+
- url: https://example1.com
15+
description: This is a server description in path item
16+
parameters:
17+
- $ref: "#/components/parameters/usedParameter1"
18+
get:
19+
summary: operation summary
20+
description: operation description
21+
servers:
22+
- url: https://example2.com
23+
description: This is a server description in operation
24+
parameters:
25+
- $ref: '#/components/parameters/usedParameter2'
26+
responses:
27+
'200':
28+
description: response description
29+
parameters:
30+
usedParameter1:
31+
name: usedParameter1
32+
in: query
33+
usedParameter2:
34+
name: usedParameter2
35+
in: query
36+
unusedParameter:
37+
name: unusedParameter
38+
in: query
39+
securitySchemes:
40+
BearerAuth:
41+
type: http
42+
description: Bearer token authentication. Default security scheme for API usage.
43+
scheme: bearer
44+
bearerFormat: JWT
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
openapi: "3.1.0"
2+
info:
3+
title: test
4+
version: 0.1.0
5+
paths:
6+
/path2:
7+
$ref: '#/components/pathItems/pathItem2'
8+
components:
9+
pathItems:
10+
pathItem2:
11+
parameters:
12+
- $ref: "#/components/parameters/usedParameter1"
13+
post:
14+
parameters:
15+
- $ref: '#/components/parameters/usedParameter2'
16+
responses:
17+
'200':
18+
description: response description
19+
parameters:
20+
usedParameter1:
21+
name: usedParameter1
22+
in: query
23+
usedParameter2:
24+
name: usedParameter2
25+
in: query
26+
unusedParameter:
27+
name: unusedParameter
28+
in: query

0 commit comments

Comments
 (0)