Skip to content

Commit c4db313

Browse files
committed
Merge remote-tracking branch 'origin/feature/pathItems' into feature/pathItems
2 parents 14ad3fa + 9dacc6a commit c4db313

File tree

4 files changed

+133
-248
lines changed

4 files changed

+133
-248
lines changed

src/apitypes/rest/rest.operation.ts

Lines changed: 117 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,41 @@ 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,
3234
capitalize,
3335
getKeyValue,
3436
getSplittedVersionKey,
35-
getValueByRefAndUpdate,
3637
isDeprecatedOperationItem,
38+
isObject,
3739
isOperationDeprecated,
3840
normalizePath,
3941
rawToApiKind,
42+
removeFirstSlash,
4043
setValueByPath,
44+
slugify,
4145
takeIf,
4246
takeIfDefined,
4347
} from '../../utils'
4448
import { API_KIND, INLINE_REFS_FLAG, ORIGINS_SYMBOL, VERSION_STATUS } from '../../consts'
45-
import { getCustomTags, resolveApiAudience } from './rest.utils'
49+
import { getCustomTags, getOperationBasePath, resolveApiAudience } from './rest.utils'
4650
import { DebugPerformanceContext, syncDebugPerformance } from '../../utils/logs'
4751
import {
4852
calculateDeprecatedItems,
4953
grepValue,
5054
JSON_SCHEMA_PROPERTY_DEPRECATED,
5155
matchPaths,
5256
OPEN_API_PROPERTY_COMPONENTS,
53-
OPEN_API_PROPERTY_PATH_ITEMS,
5457
OPEN_API_PROPERTY_PATHS,
5558
OPEN_API_PROPERTY_SCHEMAS,
5659
parseRef,
@@ -61,6 +64,7 @@ import {
6164
} from '@netcracker/qubership-apihub-api-unifier'
6265
import { calculateObjectHash } from '../../utils/hashes'
6366
import { calculateTolerantHash } from '../../components/deprecated'
67+
import { getValueByPath } from '../../utils/path'
6468

6569
export const buildRestOperation = (
6670
operationId: string,
@@ -145,7 +149,7 @@ export const buildRestOperation = (
145149
security,
146150
components?.securitySchemes,
147151
)
148-
calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, models, componentsHashMap)
152+
calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, [operationId], models, componentsHashMap)
149153
const dataHash = calculateObjectHash(specWithSingleOperation)
150154
return [specWithSingleOperation, dataHash]
151155
}, debugCtx)
@@ -182,7 +186,14 @@ export const buildRestOperation = (
182186
}
183187
}
184188

185-
export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unknown, resultSpec: unknown, models?: Record<string, string>, componentsHashMap?: Map<string, string>): void => {
189+
export const calculateSpecRefs = (
190+
sourceDocument: TYPE.RestOperationData,
191+
normalizedSpec: TYPE.RestOperationData,
192+
resultSpec: TYPE.RestOperationData,
193+
operations: OperationId[],
194+
models?: Record<string, string>,
195+
componentsHashMap?: Map<string, string>,
196+
): void => {
186197
const handledObjects = new Set<unknown>()
187198
const inlineRefs = new Set<string>()
188199
syncCrawl(
@@ -214,67 +225,94 @@ export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unkno
214225
return
215226
}
216227
const componentName = matchResult.grepValues[grepKey].toString()
217-
let sourceComponents = getKeyValue(sourceDocument, ...matchResult.path)
218-
if (!sourceComponents || typeof sourceComponents !== 'object') {
228+
let component = getKeyValue(sourceDocument, ...matchResult.path) as Record<string, unknown>
229+
if (!component) {
219230
return
220231
}
221-
222-
if (typeof sourceComponents === 'object') {
223-
const allowedOps = getAllowedHttpOps(resultSpec, matchResult.path)
224-
if (allowedOps.length > 0 && isComponentsPathItemRef(matchResult.path)) {
225-
sourceComponents = filterPathItemOperations(sourceComponents, allowedOps)
226-
}
232+
if (isObject(component)) {
233+
component = { ...component }
227234
}
228235
if (models && !models[componentName] && isComponentsSchemaRef(matchResult.path)) {
229-
const existingHash = componentsHashMap?.get(componentName)
230-
if (existingHash) {
231-
models[componentName] = existingHash
236+
let componentHash = componentsHashMap?.get(componentName)
237+
if (componentHash) {
238+
models[componentName] = componentHash
232239
} else {
233-
const componentHashCalculated = calculateObjectHash(sourceComponents as object)
234-
componentsHashMap?.set(componentName, componentHashCalculated)
235-
models[componentName] = componentHashCalculated
240+
componentHash = calculateObjectHash(component)
241+
componentsHashMap?.set(componentName, componentHash)
242+
models[componentName] = componentHash
236243
}
237244
}
238-
setValueByPath(resultSpec, matchResult.path, sourceComponents)
245+
246+
setValueByPath(resultSpec, matchResult.path, component)
239247
})
240-
}
241248

242-
export const isComponentsSchemaRef = (path: JsonPath): boolean => {
243-
return !!matchPaths(
244-
[path],
245-
[[OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_SCHEMAS, PREDICATE_UNCLOSED_END]],
246-
)
247-
}
248-
export const isComponentsPathItemRef = (path: JsonPath): boolean => {
249-
return !!matchPaths(
250-
[path],
251-
[[OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PATH_ITEMS, PREDICATE_UNCLOSED_END]],
252-
)
249+
if (operations?.length) {
250+
resolveComponentsPathItemOperationSpec(resultSpec, normalizedSpec, operations)
251+
}
253252
}
254253

255-
export const filterPathItemOperations = (
256-
source: unknown,
257-
allowedMethods: string[],
258-
): unknown => {
259-
const httpMethods = new Set<string>(Object.values(OpenAPIV3.HttpMethods) as string[])
260-
const filteredSource: Record<string, unknown> = { ...(source as Record<string, unknown>) }
254+
export function resolveComponentsPathItemOperationSpec(
255+
sourceDocument: RestOperationData,
256+
normalizedDocument: RestOperationData,
257+
operations: OperationId[],
258+
): void {
259+
const { paths } = normalizedDocument
261260

262-
for (const key of Object.keys(filteredSource)) {
263-
if (httpMethods.has(key) && !allowedMethods.includes(key)) {
264-
delete filteredSource[key]
261+
for (const path of Object.keys(paths)) {
262+
const sourcePathItem = paths[path] as OpenAPIV3.PathItemObject
263+
if (!isNonNullObject(sourcePathItem)) {
264+
continue
265+
}
266+
const refs: string[] = hasInlineRefsFlag(sourcePathItem) ? sourcePathItem[INLINE_REFS_FLAG] : []
267+
if (refs.length === 0) {
268+
continue
269+
}
270+
const { jsonPath } = parseRef(refs[0])
271+
if (!jsonPath) {
272+
continue
273+
}
274+
275+
const valueByPath = getValueByPath(sourceDocument, jsonPath) as OpenAPIV3.PathItemObject
276+
277+
const operationIds: OpenAPIV3.HttpMethods[] = (Object.keys(valueByPath) as OpenAPIV3.HttpMethods[])
278+
.filter((httpMethod) => isValidHttpMethod(httpMethod))
279+
.filter(httpMethod => {
280+
const methodData = sourcePathItem[httpMethod as OpenAPIV3.HttpMethods]
281+
if (!methodData) return false
282+
const basePath = getOperationBasePath(
283+
methodData?.servers ||
284+
sourcePathItem?.servers ||
285+
[],
286+
)
287+
const operationId = getOperationId(basePath, httpMethod, path)
288+
return operations.includes(operationId)
289+
})
290+
291+
if (operationIds?.length) {
292+
const pathItemObject = {
293+
...extractCommonPathItemProperties(valueByPath),
294+
...operationIds.reduce<OpenAPIV3.PathItemObject>((pathItemObject: OpenAPIV3.PathItemObject, operationId: OpenAPIV3.HttpMethods) => {
295+
const operationData = valueByPath[operationId]
296+
if (operationData) {
297+
pathItemObject[operationId] = { ...operationData }
298+
}
299+
return pathItemObject
300+
}, {}),
301+
}
302+
setValueByPath(sourceDocument, jsonPath, pathItemObject)
265303
}
266304
}
305+
}
267306

268-
return filteredSource
307+
function isNonNullObject(value: unknown): value is Record<string, unknown> {
308+
return typeof value === 'object' && value !== null
269309
}
270310

271-
export const getAllowedHttpOps = (resultSpec: unknown, jsonPath: JsonPath): string[] => {
272-
const resultComponents = getKeyValue(resultSpec, ...jsonPath) as unknown
273-
if (typeof resultComponents !== 'object' || resultComponents === null) {
274-
return []
275-
}
276-
const httpMethods = new Set<string>(Object.values(OpenAPIV3.HttpMethods) as string[])
277-
return Object.keys(resultComponents as Record<string, unknown>).filter(key => httpMethods.has(key))
311+
export const isComponentsSchemaRef = (path: JsonPath): boolean => {
312+
return !!matchPaths(
313+
[path],
314+
[[OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_SCHEMAS, PREDICATE_UNCLOSED_END]],
315+
)
278316
}
279317

280318
const isOperationPaths = (paths: JsonPath[]): boolean => {
@@ -284,6 +322,10 @@ const isOperationPaths = (paths: JsonPath[]): boolean => {
284322
)
285323
}
286324

325+
function hasInlineRefsFlag(obj: unknown): obj is { [INLINE_REFS_FLAG]: string[] } {
326+
return typeof obj === 'object' && obj !== null && INLINE_REFS_FLAG in obj
327+
}
328+
287329
// todo output of this method disrupts document normalization.
288330
// origin symbols are not being transferred to the resulting spec.
289331
// DO NOT pass output of this method to apiDiff
@@ -296,51 +338,27 @@ const createSingleOperationSpec = (
296338
security?: OpenAPIV3.SecurityRequirementObject[],
297339
securitySchemes?: { [p: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.SecuritySchemeObject },
298340
): TYPE.RestOperationData => {
299-
const pathData = document.paths[path] as OpenAPIV3.PathItemObject | undefined
300-
if (!pathData) {
301-
throw new Error(`Path "${path}" not found in the document`)
302-
}
341+
const pathData = document.paths[path] as OpenAPIV3.PathItemObject
303342

304-
const baseSpec = {
343+
const isContainsRef = !!pathData.$ref
344+
const refFlag = hasInlineRefsFlag(pathData) ? pathData[INLINE_REFS_FLAG] : false
345+
return {
305346
openapi: openapi ?? '3.0.0',
306347
...takeIfDefined({ servers }),
307348
...takeIfDefined({ security }), // TODO: remove duplicates in security
349+
paths: {
350+
[path]: isContainsRef
351+
? pathData
352+
: {
353+
...extractCommonPathItemProperties(pathData),
354+
[method]: { ...pathData[method] },
355+
...(refFlag ? { [INLINE_REFS_FLAG]: refFlag } : {}),
356+
},
357+
},
308358
components: {
309359
...takeIfDefined({ securitySchemes }),
310360
},
311361
}
312-
313-
const ref = pathData.$ref
314-
if (ref) {
315-
const cleanedDocument = getValueByRefAndUpdate(
316-
ref,
317-
document,
318-
(pathItemObject: OpenAPIV3.PathItemObject) => ({
319-
...extractCommonPathItemProperties(pathItemObject),
320-
[method]: { ...pathItemObject[method] },
321-
}))
322-
323-
return {
324-
...baseSpec,
325-
paths: {
326-
[path]: pathData,
327-
},
328-
components: {
329-
...baseSpec.components,
330-
...cleanedDocument.components ?? {},
331-
},
332-
}
333-
}
334-
335-
return {
336-
...baseSpec,
337-
paths: {
338-
[path]: {
339-
...extractCommonPathItemProperties(pathData),
340-
[method]: { ...pathData[method] },
341-
},
342-
},
343-
}
344362
}
345363

346364
export const extractCommonPathItemProperties = (
@@ -352,3 +370,15 @@ export const extractCommonPathItemProperties = (
352370
...takeIfDefined({ parameters: pathData?.parameters }),
353371
})
354372

373+
function isValidHttpMethod(method: string): method is OpenAPIV3.HttpMethods {
374+
return (Object.values(OpenAPIV3.HttpMethods) as string[]).includes(method)
375+
}
376+
377+
export function getOperationId(
378+
basePath: string,
379+
key: string,
380+
path: string,
381+
): string {
382+
const operationPath = basePath + path
383+
return slugify(`${removeFirstSlash(operationPath)}-${key}`)
384+
}

src/apitypes/rest/rest.operations.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@
1616

1717
import { OpenAPIV3 } from 'openapi-types'
1818

19-
import { buildRestOperation } from './rest.operation'
19+
import { buildRestOperation, getOperationId } from './rest.operation'
2020
import { OperationIdNormalizer, OperationsBuilder } from '../../types'
2121
import {
2222
createBundlingErrorHandler,
2323
IGNORE_PATH_PARAM_UNIFIED_PLACEHOLDER,
2424
removeComponents,
25-
removeFirstSlash,
2625
slugify,
2726
} from '../../utils'
2827
import { getOperationBasePath } from './rest.utils'
@@ -77,9 +76,8 @@ export const buildRestOperations: OperationsBuilder<OpenAPIV3.Document> = async
7776
await asyncFunction(() => {
7877
const methodData = pathData[key as OpenAPIV3.HttpMethods]
7978
const basePath = getOperationBasePath(methodData?.servers || pathData?.servers || servers || [])
80-
const operationPath = basePath + path
8179

82-
const operationId = slugify(`${removeFirstSlash(operationPath)}-${key}`)
80+
const operationId = getOperationId(basePath, key, path)
8381

8482
if (ctx.operationResolver(operationId)) {
8583
ctx.notifications.push({

0 commit comments

Comments
 (0)