Skip to content

Commit e26c4ba

Browse files
authored
feat: support for reusable pathItems definitions in components for OAS 3.1 (#35)
1 parent 07e546c commit e26c4ba

File tree

70 files changed

+1594
-120
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+1594
-120
lines changed

src/apitypes/rest/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import { OpenAPIV3 } from 'openapi-types'
1919
import { buildRestDocument, createRestExportDocument, dumpRestDocument } from './rest.document'
2020
import { REST_API_TYPE, REST_DOCUMENT_TYPE } from './rest.consts'
2121
import { compareRestOperationsData } from './rest.changes'
22-
import { buildRestOperations, createNormalizedOperationId } from './rest.operations'
22+
import { buildRestOperations } from './rest.operations'
2323
import { parseRestFile } from './rest.parser'
24-
2524
import { ApiBuilder } from '../../types'
25+
import { calculateNormalizedOperationId } from '../../utils'
2626

2727
export * from './rest.consts'
2828

@@ -34,6 +34,6 @@ export const restApiBuilder: ApiBuilder<OpenAPIV3.Document> = {
3434
buildOperations: buildRestOperations,
3535
dumpDocument: dumpRestDocument,
3636
compareOperationsData: compareRestOperationsData,
37-
createNormalizedOperationId: createNormalizedOperationId,
37+
createNormalizedOperationId: calculateNormalizedOperationId,
3838
createExportDocument: createRestExportDocument,
3939
}

src/apitypes/rest/rest.operation.ts

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,35 @@ 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'
22-
import type {
22+
import { RestOperationData } from './rest.types'
23+
import {
2324
BuildConfig,
2425
CrawlRule,
2526
DeprecateItem,
2627
NotificationMessage,
2728
OperationCrawlState,
29+
OperationId,
2830
SearchScopes,
2931
} from '../../types'
3032
import {
3133
buildSearchScope,
34+
calculateOperationId,
3235
capitalize,
36+
extractSymbolProperty,
3337
getKeyValue,
3438
getSplittedVersionKey,
39+
getSymbolValueIfDefined,
3540
isDeprecatedOperationItem,
3641
isOperationDeprecated,
42+
isValidHttpMethod,
3743
normalizePath,
3844
rawToApiKind,
3945
setValueByPath,
4046
takeIf,
4147
takeIfDefined,
4248
} from '../../utils'
4349
import { API_KIND, INLINE_REFS_FLAG, ORIGINS_SYMBOL, VERSION_STATUS } from '../../consts'
44-
import { getCustomTags, resolveApiAudience } from './rest.utils'
50+
import { getCustomTags, getOperationBasePath, resolveApiAudience } from './rest.utils'
4551
import { DebugPerformanceContext, syncDebugPerformance } from '../../utils/logs'
4652
import {
4753
calculateDeprecatedItems,
@@ -59,6 +65,7 @@ import {
5965
} from '@netcracker/qubership-apihub-api-unifier'
6066
import { calculateObjectHash } from '../../utils/hashes'
6167
import { calculateTolerantHash } from '../../components/deprecated'
68+
import { getValueByPath } from '../../utils/path'
6269

6370
export const buildRestOperation = (
6471
operationId: string,
@@ -143,7 +150,7 @@ export const buildRestOperation = (
143150
security,
144151
components?.securitySchemes,
145152
)
146-
calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, models, componentsHashMap)
153+
calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, [operationId], models, componentsHashMap)
147154
const dataHash = calculateObjectHash(specWithSingleOperation)
148155
return [specWithSingleOperation, dataHash]
149156
}, debugCtx)
@@ -180,7 +187,14 @@ export const buildRestOperation = (
180187
}
181188
}
182189

183-
export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unknown, resultSpec: unknown, models?: Record<string, string>, componentsHashMap?: Map<string, string>): void => {
190+
export const calculateSpecRefs = (
191+
sourceDocument: TYPE.RestOperationData,
192+
normalizedSpec: TYPE.RestOperationData,
193+
resultSpec: TYPE.RestOperationData,
194+
operations: OperationId[],
195+
models?: Record<string, string>,
196+
componentsHashMap?: Map<string, string>,
197+
): void => {
184198
const handledObjects = new Set<unknown>()
185199
const inlineRefs = new Set<string>()
186200
syncCrawl(
@@ -212,10 +226,11 @@ export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unkno
212226
return
213227
}
214228
const componentName = matchResult.grepValues[grepKey].toString()
215-
const component = getKeyValue(sourceDocument, ...matchResult.path)
229+
const component = getKeyValue(sourceDocument, ...matchResult.path) as Record<string, unknown>
216230
if (!component) {
217231
return
218232
}
233+
219234
if (models && !models[componentName] && isComponentsSchemaRef(matchResult.path)) {
220235
let componentHash = componentsHashMap?.get(componentName)
221236
if (componentHash) {
@@ -226,8 +241,68 @@ export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unkno
226241
models[componentName] = componentHash
227242
}
228243
}
244+
229245
setValueByPath(resultSpec, matchResult.path, component)
230246
})
247+
248+
if (operations?.length) {
249+
reduceComponentPathItemsToOperations(resultSpec, normalizedSpec, operations)
250+
}
251+
}
252+
253+
function reduceComponentPathItemsToOperations(
254+
resultSpec: RestOperationData,
255+
normalizedDocument: RestOperationData,
256+
operations: OperationId[],
257+
): void {
258+
const { paths } = normalizedDocument
259+
260+
for (const path of Object.keys(paths)) {
261+
const sourcePathItem = paths[path] as OpenAPIV3.PathItemObject
262+
const pathItemComponentJsonPath = getPathItemComponentJsonPath(sourcePathItem)
263+
if (!pathItemComponentJsonPath) {
264+
continue
265+
}
266+
267+
const pathItemComponent = getValueByPath(resultSpec, pathItemComponentJsonPath) as OpenAPIV3.PathItemObject
268+
269+
const operationIds: OpenAPIV3.HttpMethods[] = (Object.keys(pathItemComponent) as OpenAPIV3.HttpMethods[])
270+
.filter((httpMethod) => isValidHttpMethod(httpMethod))
271+
.filter(httpMethod => {
272+
const methodData = sourcePathItem[httpMethod as OpenAPIV3.HttpMethods]
273+
if (!methodData) return false
274+
const basePath = getOperationBasePath(
275+
methodData?.servers ||
276+
sourcePathItem?.servers ||
277+
[],
278+
)
279+
const operationId = calculateOperationId(basePath, httpMethod, path)
280+
return operations.includes(operationId)
281+
})
282+
283+
if (operationIds?.length) {
284+
const pathItemObject = {
285+
...extractCommonPathItemProperties(pathItemComponent),
286+
...operationIds.reduce<OpenAPIV3.PathItemObject>((pathItemObject: OpenAPIV3.PathItemObject, operationId: OpenAPIV3.HttpMethods) => {
287+
const operationData = pathItemComponent[operationId]
288+
if (operationData) {
289+
pathItemObject[operationId] = { ...operationData }
290+
}
291+
return pathItemObject
292+
}, {}),
293+
}
294+
setValueByPath(resultSpec, pathItemComponentJsonPath, pathItemObject)
295+
}
296+
}
297+
}
298+
299+
const getPathItemComponentJsonPath = (sourcePathItem: OpenAPIV3.PathItemObject): JsonPath | undefined => {
300+
const refs = getSymbolValueIfDefined(sourcePathItem, INLINE_REFS_FLAG) as string[] | undefined
301+
if (!refs || refs.length === 0) {
302+
return undefined
303+
}
304+
305+
return parseRef(refs[0])?.jsonPath
231306
}
232307

233308
export const isComponentsSchemaRef = (path: JsonPath): boolean => {
@@ -236,6 +311,7 @@ export const isComponentsSchemaRef = (path: JsonPath): boolean => {
236311
[[OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_SCHEMAS, PREDICATE_UNCLOSED_END]],
237312
)
238313
}
314+
239315
const isOperationPaths = (paths: JsonPath[]): boolean => {
240316
return !!matchPaths(
241317
paths,
@@ -257,15 +333,19 @@ const createSingleOperationSpec = (
257333
): TYPE.RestOperationData => {
258334
const pathData = document.paths[path] as OpenAPIV3.PathItemObject
259335

336+
const isRefPathData = !!pathData.$ref
260337
return {
261338
openapi: openapi ?? '3.0.0',
262339
...takeIfDefined({ servers }),
263340
...takeIfDefined({ security }), // TODO: remove duplicates in security
264341
paths: {
265-
[path]: {
266-
...extractCommonPathItemProperties(pathData),
267-
[method]: { ...pathData[method] },
268-
},
342+
[path]: isRefPathData
343+
? pathData
344+
: {
345+
...extractCommonPathItemProperties(pathData),
346+
[method]: { ...pathData[method] },
347+
...extractSymbolProperty(pathData, INLINE_REFS_FLAG),
348+
},
269349
},
270350
components: {
271351
...takeIfDefined({ securitySchemes }),

src/apitypes/rest/rest.operations.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,8 @@
1717
import { OpenAPIV3 } from 'openapi-types'
1818

1919
import { buildRestOperation } from './rest.operation'
20-
import { OperationIdNormalizer, OperationsBuilder } from '../../types'
21-
import {
22-
createBundlingErrorHandler,
23-
IGNORE_PATH_PARAM_UNIFIED_PLACEHOLDER,
24-
removeComponents,
25-
removeFirstSlash,
26-
slugify,
27-
} from '../../utils'
20+
import { OperationsBuilder } from '../../types'
21+
import { calculateOperationId, createBundlingErrorHandler, removeComponents } from '../../utils'
2822
import { getOperationBasePath } from './rest.utils'
2923
import type * as TYPE from './rest.types'
3024
import { HASH_FLAG, INLINE_REFS_FLAG, MESSAGE_SEVERITY, NORMALIZE_OPTIONS, ORIGINS_SYMBOL } from '../../consts'
@@ -61,7 +55,7 @@ export const buildRestOperations: OperationsBuilder<OpenAPIV3.Document> = async
6155
debugCtx,
6256
)
6357

64-
const { paths, servers } = document.data
58+
const { paths, servers } = effectiveDocument
6559

6660
const operations: TYPE.VersionRestOperation[] = []
6761
if (!paths) { return [] }
@@ -77,9 +71,8 @@ export const buildRestOperations: OperationsBuilder<OpenAPIV3.Document> = async
7771
await asyncFunction(() => {
7872
const methodData = pathData[key as OpenAPIV3.HttpMethods]
7973
const basePath = getOperationBasePath(methodData?.servers || pathData?.servers || servers || [])
80-
const operationPath = basePath + path
8174

82-
const operationId = slugify(`${removeFirstSlash(operationPath)}-${key}`)
75+
const operationId = calculateOperationId(basePath, key, path)
8376

8477
if (ctx.operationResolver(operationId)) {
8578
ctx.notifications.push({
@@ -112,8 +105,3 @@ export const buildRestOperations: OperationsBuilder<OpenAPIV3.Document> = async
112105
}
113106
return operations
114107
}
115-
116-
export const createNormalizedOperationId: OperationIdNormalizer = (operation) => {
117-
const { metadata: { path, method } } = operation
118-
return slugify(`${path}-${method}`, [], IGNORE_PATH_PARAM_UNIFIED_PLACEHOLDER)
119-
}

src/consts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
VALIDATION_RULES_SEVERITY_LEVEL_WARNING,
2828
ValidationRulesSeverity,
2929
} from './types'
30+
import { OpenAPIV3 } from 'openapi-types'
3031

3132
export const DEFAULT_BATCH_SIZE = 32
3233

@@ -169,3 +170,4 @@ export const EMPTY_CHANGE_SUMMARY_DTO = {
169170
}
170171

171172
export const CUSTOM_PARAMETER_API_AUDIENCE = 'x-api-audience'
173+
export const HTTP_METHODS_SET = new Set(Object.values(OpenAPIV3.HttpMethods) as string[])

0 commit comments

Comments
 (0)