Skip to content

Commit 26cc1fd

Browse files
committed
feat: Fixed servers prefix for operations and added tests
1 parent aa6f46a commit 26cc1fd

File tree

7 files changed

+184
-33
lines changed

7 files changed

+184
-33
lines changed

src/apitypes/rest/rest.operation.ts

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
JSON_SCHEMA_PROPERTY_DEPRECATED,
5151
matchPaths,
5252
OPEN_API_PROPERTY_COMPONENTS,
53+
OPEN_API_PROPERTY_PATH_ITEMS,
5354
OPEN_API_PROPERTY_PATHS,
5455
OPEN_API_PROPERTY_SCHEMAS,
5556
parseRef,
@@ -212,39 +213,21 @@ export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unkno
212213
if (!matchResult) {
213214
return
214215
}
215-
//todo why? description?
216216
const componentName = matchResult.grepValues[grepKey].toString()
217-
let sourceComponents = getKeyValue(sourceDocument, ...matchResult.path)
218-
const resultComponents = getKeyValue(resultSpec, ...matchResult.path)
219-
const httpMethods = new Set<string>(Object.values(OpenAPIV3.HttpMethods) as string[])
220-
const allowedOps = (typeof resultComponents === 'object' && resultComponents !== null)
221-
? Object.keys(resultComponents as object).filter(key => httpMethods.has(key))
222-
: []
217+
let sourceComponents = getKeyValue(sourceDocument, ...matchResult.path) as unknown
223218
if (!sourceComponents || typeof sourceComponents !== 'object') {
224219
return
225220
}
226-
if (allowedOps.length > 0) {
227-
sourceComponents = { ...sourceComponents }
228-
Object.keys(sourceComponents as object).forEach((key: string) => {
229-
if (httpMethods.has(key) && !allowedOps.includes(key)) {
230-
// prune operations not present in the partial result component
231-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
232-
// @ts-ignore
233-
delete (sourceComponents as any)[key]
234-
}
235-
})
236-
}
221+
sourceComponents = prunePathItemMethods(sourceComponents, resultSpec, matchResult.path)
237222
// component is defined and object at this point
238223
if (models && !models[componentName] && isComponentsSchemaRef(matchResult.path)) {
239-
let componentHash = componentsHashMap?.get(componentName)
240-
if (componentHash) {
241-
models[componentName] = componentHash
224+
const existingHash = componentsHashMap?.get(componentName)
225+
if (existingHash) {
226+
models[componentName] = existingHash
242227
} else {
243-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
244-
// @ts-ignore
245-
componentHash = calculateObjectHash(sourceComponents)
246-
componentsHashMap?.set(componentName, componentHash)
247-
models[componentName] = componentHash
228+
const componentHashCalculated = calculateObjectHash(sourceComponents as object)
229+
componentsHashMap?.set(componentName, componentHashCalculated)
230+
models[componentName] = componentHashCalculated
248231
}
249232
}
250233
setValueByPath(resultSpec, matchResult.path, sourceComponents)
@@ -257,6 +240,57 @@ export const isComponentsSchemaRef = (path: JsonPath): boolean => {
257240
[[OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_SCHEMAS, PREDICATE_UNCLOSED_END]],
258241
)
259242
}
243+
export const isComponentsPathItemRef = (path: JsonPath): boolean => {
244+
return !!matchPaths(
245+
[path],
246+
[[OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PATH_ITEMS, PREDICATE_UNCLOSED_END]],
247+
)
248+
}
249+
250+
/**
251+
* Returns a shallow-copied object with HTTP method keys pruned to the allowed set,
252+
* but only when the jsonPath points to components.pathItems.
253+
*
254+
* If jsonPath does not match components.pathItems or allowedOps is empty, returns input unchanged.
255+
*/
256+
export const prunePathItemMethods = (
257+
source: unknown,
258+
resultSpec: unknown,
259+
jsonPath: JsonPath,
260+
): unknown => {
261+
const allowedOps = getAllowedHttpOps(resultSpec, jsonPath)
262+
if (!source || typeof source !== 'object') {
263+
return source
264+
}
265+
if (allowedOps.length === 0) {
266+
return source
267+
}
268+
if (!isComponentsPathItemRef(jsonPath)) {
269+
return source
270+
}
271+
const httpMethods = new Set<string>(Object.values(OpenAPIV3.HttpMethods) as string[])
272+
const copy: Record<string, unknown> = { ...(source as Record<string, unknown>) }
273+
Object.keys(copy).forEach((key: string) => {
274+
if (httpMethods.has(key) && !allowedOps.includes(key)) {
275+
delete copy[key]
276+
}
277+
})
278+
return copy
279+
}
280+
281+
/**
282+
* Extracts the set of HTTP methods allowed by the partial component found at jsonPath
283+
* within the resultSpec. If the path does not resolve to an object, returns an empty list.
284+
*/
285+
export const getAllowedHttpOps = (resultSpec: unknown, jsonPath: JsonPath): string[] => {
286+
const resultComponents = getKeyValue(resultSpec, ...jsonPath) as unknown
287+
if (typeof resultComponents !== 'object' || resultComponents === null) {
288+
return []
289+
}
290+
const httpMethods = new Set<string>(Object.values(OpenAPIV3.HttpMethods) as string[])
291+
return Object.keys(resultComponents as Record<string, unknown>).filter(key => httpMethods.has(key))
292+
}
293+
260294
const isOperationPaths = (paths: JsonPath[]): boolean => {
261295
return !!matchPaths(
262296
paths,
@@ -322,3 +356,4 @@ export const extractCommonPathItemProperties = (
322356
...takeIfDefined({ servers: pathData?.servers }),
323357
...takeIfDefined({ parameters: pathData?.parameters }),
324358
})
359+

src/strategies/document-group.strategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docu
142142
}
143143

144144
const methodData = normalizedPathItem[inferredMethod]
145-
const basePath = getOperationBasePath(methodData?.servers || sourcePathItem?.servers || [])
145+
const basePath = getOperationBasePath(methodData?.servers || sourcePathItem?.servers || sourceDocument?.servers || [])
146146
const operationPath = basePath + path
147147
const operationId = slugify(`${removeFirstSlash(operationPath)}-${method}`)
148148

test/document-group.test.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ const groupWithOneOperationIdsMap = {
4545
'path1-post',
4646
],
4747
}
48+
49+
const groupToOneServerPrefixPathOperationIdsMap = {
50+
[GROUP_NAME]: [
51+
'api-v1-path1-get',
52+
'api-v1-path2-post',
53+
],
54+
}
55+
4856
const EXPECTED_RESULT_FILE = 'result.yaml'
4957

5058
describe('Document Group test', () => {
@@ -90,6 +98,21 @@ describe('Document Group test', () => {
9098
}
9199
})
92100

101+
test('should define operations with servers prefix', async () => {
102+
const pkg = LocalRegistry.openPackage('document-group/define-operations-with-servers-prefix', groupToOneServerPrefixPathOperationIdsMap)
103+
const editor = await Editor.openProject(pkg.packageId, pkg)
104+
await pkg.publish(pkg.packageId, { packageId: pkg.packageId })
105+
106+
const result = await editor.run({
107+
packageId: pkg.packageId,
108+
groupName: GROUP_NAME,
109+
buildType: BUILD_TYPE.REDUCED_SOURCE_SPECIFICATIONS,
110+
})
111+
for (const document of Array.from(result.documents.values())) {
112+
expect(Object.keys(document.data.paths).length).toEqual(document.operationIds.length)
113+
}
114+
})
115+
93116
test('should rename documents with matching names', async () => {
94117
const dashboard = LocalRegistry.openPackage('documents-collision', groupToOperationIdsMap2)
95118
const package1 = LocalRegistry.openPackage('documents-collision/package1')
@@ -111,10 +134,22 @@ describe('Document Group test', () => {
111134

112135
expect(Array.from(result.documents.values())).toEqual(
113136
expect.toIncludeSameMembers([
114-
expect.objectContaining({ fileId: 'documents-collision/package1_1.yaml', filename: 'documents-collision/package1_1.json' }),
115-
expect.objectContaining({ fileId: 'documents-collision/package1_2.yaml', filename: 'documents-collision/package1_2.json' }),
116-
expect.objectContaining({ fileId: 'documents-collision/package2_1.yaml', filename: 'documents-collision/package2_1.json' }),
117-
expect.objectContaining({ fileId: 'documents-collision/package3_1.yaml', filename: 'documents-collision/package3_1.json' }),
137+
expect.objectContaining({
138+
fileId: 'documents-collision/package1_1.yaml',
139+
filename: 'documents-collision/package1_1.json',
140+
}),
141+
expect.objectContaining({
142+
fileId: 'documents-collision/package1_2.yaml',
143+
filename: 'documents-collision/package1_2.json',
144+
}),
145+
expect.objectContaining({
146+
fileId: 'documents-collision/package2_1.yaml',
147+
filename: 'documents-collision/package2_1.json',
148+
}),
149+
expect.objectContaining({
150+
fileId: 'documents-collision/package3_1.yaml',
151+
filename: 'documents-collision/package3_1.json',
152+
}),
118153
]),
119154
)
120155

@@ -187,8 +222,23 @@ describe('Document Group test', () => {
187222
}
188223
})
189224

225+
test('should define pathitems operations with servers prefix', async () => {
226+
const pkg = LocalRegistry.openPackage('document-group/define-pathitems-operations-with-servers-prefix', groupToOneServerPrefixPathOperationIdsMap)
227+
const editor = await Editor.openProject(pkg.packageId, pkg)
228+
await pkg.publish(pkg.packageId, { packageId: pkg.packageId })
229+
230+
const result = await editor.run({
231+
packageId: pkg.packageId,
232+
groupName: GROUP_NAME,
233+
buildType: BUILD_TYPE.REDUCED_SOURCE_SPECIFICATIONS,
234+
})
235+
for (const document of Array.from(result.documents.values())) {
236+
expect(Object.keys(document.data.paths).length).toEqual(document.operationIds.length)
237+
}
238+
})
239+
190240
describe('Chain refs', () => {
191-
const COMPONENTS_ITEM_1_PATH =['components', 'pathItems', 'componentsPathItem1']
241+
const COMPONENTS_ITEM_1_PATH = ['components', 'pathItems', 'componentsPathItem1']
192242
test('should have documents with keep pathItems in components', async () => {
193243
const pkg = LocalRegistry.openPackage('document-group/define-pathitems-via-reference-object-chain', groupToOnePathOperationIdsMap)
194244
const editor = await Editor.openProject(pkg.packageId, pkg)
@@ -222,7 +272,7 @@ describe('Document Group test', () => {
222272
})
223273
})
224274

225-
describe('reference-object', ()=> {
275+
describe('reference-object', () => {
226276
test('second-level-object-are-the-same-when-overriding-for-response', async () => {
227277
const pkg = LocalRegistry.openPackage('document-group/second-level-object-are-the-same-when-overriding-for-response', groupToOnePathOperationIdsMap)
228278
const editor = await Editor.openProject(pkg.packageId, pkg)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
openapi: 3.1.0
2+
info:
3+
title: ATUI Petstore 3.0
4+
version: 1.0.0
5+
servers:
6+
- url: https://petstore3.swagger.io/api/v1
7+
paths:
8+
/path1:
9+
get:
10+
responses:
11+
'200':
12+
description: response description main
13+
/path2:
14+
post:
15+
responses:
16+
'200':
17+
description: response description main
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"packageId": "55",
3+
"apiType": "rest",
4+
"version": "v1",
5+
"files": [
6+
{
7+
"fileId": "1.yaml",
8+
"publish": true,
9+
"labels": [],
10+
"commitId": "6c778b1f44200bd19944a6a8eac10a4e5a21a3cd"
11+
}
12+
]
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
openapi: 3.1.0
2+
info:
3+
title: ATUI Petstore 3.0
4+
version: 1.0.0
5+
servers:
6+
- url: https://petstore3.swagger.io/api/v1
7+
paths:
8+
/path1:
9+
$ref: '#/components/pathItems/pathItem1'
10+
/path2:
11+
$ref: '#/components/pathItems/pathItem2'
12+
components:
13+
pathItems:
14+
pathItem1:
15+
get:
16+
responses:
17+
'200':
18+
description: response description main
19+
pathItem2:
20+
post:
21+
responses:
22+
'200':
23+
description: response description main
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"packageId": "55",
3+
"apiType": "rest",
4+
"version": "v1",
5+
"files": [
6+
{
7+
"fileId": "1.yaml",
8+
"publish": true,
9+
"labels": [],
10+
"commitId": "6c778b1f44200bd19944a6a8eac10a4e5a21a3cd"
11+
}
12+
]
13+
}

0 commit comments

Comments
 (0)