Skip to content

Commit 793e0bc

Browse files
committed
feat: POC ref pathItems support
1 parent 11f601d commit 793e0bc

File tree

3 files changed

+95
-245
lines changed

3 files changed

+95
-245
lines changed

src/apitypes/rest/rest.operation.ts

Lines changed: 73 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,40 @@ 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 {
2323
BuildConfig,
2424
CrawlRule,
2525
DeprecateItem,
2626
NotificationMessage,
2727
OperationCrawlState,
28+
OperationId,
2829
SearchScopes,
2930
} from '../../types'
3031
import {
3132
buildSearchScope,
3233
capitalize,
3334
getKeyValue,
3435
getSplittedVersionKey,
35-
getValueByRefAndUpdate,
3636
isDeprecatedOperationItem,
37+
isObject,
3738
isOperationDeprecated,
3839
normalizePath,
3940
rawToApiKind,
41+
removeFirstSlash,
4042
setValueByPath,
43+
slugify,
4144
takeIf,
4245
takeIfDefined,
4346
} from '../../utils'
4447
import { API_KIND, INLINE_REFS_FLAG, ORIGINS_SYMBOL, VERSION_STATUS } from '../../consts'
45-
import { getCustomTags, resolveApiAudience } from './rest.utils'
48+
import { getCustomTags, getOperationBasePath, resolveApiAudience } from './rest.utils'
4649
import { DebugPerformanceContext, syncDebugPerformance } from '../../utils/logs'
4750
import {
4851
calculateDeprecatedItems,
4952
grepValue,
5053
JSON_SCHEMA_PROPERTY_DEPRECATED,
5154
matchPaths,
5255
OPEN_API_PROPERTY_COMPONENTS,
53-
OPEN_API_PROPERTY_PATH_ITEMS,
5456
OPEN_API_PROPERTY_PATHS,
5557
OPEN_API_PROPERTY_SCHEMAS,
5658
parseRef,
@@ -61,6 +63,7 @@ import {
6163
} from '@netcracker/qubership-apihub-api-unifier'
6264
import { calculateObjectHash } from '../../utils/hashes'
6365
import { calculateTolerantHash } from '../../components/deprecated'
66+
import { getValueByPath } from '../../utils/path'
6467

6568
export const buildRestOperation = (
6669
operationId: string,
@@ -145,7 +148,8 @@ export const buildRestOperation = (
145148
security,
146149
components?.securitySchemes,
147150
)
148-
calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, models, componentsHashMap)
151+
calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, [operationId], models, componentsHashMap)
152+
createSinglePathItemOperationSpec(specWithSingleOperation as OpenAPIV3.Document, refsOnlySingleOperationSpec as OpenAPIV3.Document, [operationId])
149153
const dataHash = calculateObjectHash(specWithSingleOperation)
150154
return [specWithSingleOperation, dataHash]
151155
}, debugCtx)
@@ -182,7 +186,7 @@ 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 = (sourceDocument: unknown, normalizedSpec: unknown, resultSpec: unknown, operations: OperationId[], models?: Record<string, string>, componentsHashMap?: Map<string, string>): void => {
186190
const handledObjects = new Set<unknown>()
187191
const inlineRefs = new Set<string>()
188192
syncCrawl(
@@ -214,67 +218,70 @@ export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unkno
214218
return
215219
}
216220
const componentName = matchResult.grepValues[grepKey].toString()
217-
let sourceComponents = getKeyValue(sourceDocument, ...matchResult.path)
218-
if (!sourceComponents || typeof sourceComponents !== 'object') {
221+
let component: any = getKeyValue(sourceDocument, ...matchResult.path)
222+
if (!component) {
219223
return
220224
}
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-
}
225+
if (isObject(component)) {
226+
component = { ...component }
227227
}
228228
if (models && !models[componentName] && isComponentsSchemaRef(matchResult.path)) {
229-
const existingHash = componentsHashMap?.get(componentName)
230-
if (existingHash) {
231-
models[componentName] = existingHash
229+
let componentHash = componentsHashMap?.get(componentName)
230+
if (componentHash) {
231+
models[componentName] = componentHash
232232
} else {
233-
const componentHashCalculated = calculateObjectHash(sourceComponents as object)
234-
componentsHashMap?.set(componentName, componentHashCalculated)
235-
models[componentName] = componentHashCalculated
233+
componentHash = calculateObjectHash(component)
234+
componentsHashMap?.set(componentName, componentHash)
235+
models[componentName] = componentHash
236236
}
237237
}
238-
setValueByPath(resultSpec, matchResult.path, sourceComponents)
238+
239+
setValueByPath(resultSpec, matchResult.path, component)
239240
})
240241
}
241242

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-
)
253-
}
243+
export function createSinglePathItemOperationSpec(sourceDocument: OpenAPIV3.Document, normalizedDocument: OpenAPIV3.Document, operations: OperationId[]): void {
244+
const { paths } = normalizedDocument
254245

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>) }
246+
for (const path of Object.keys(paths)) {
247+
const sourcePathItem = paths[path]
261248

262-
for (const key of Object.keys(filteredSource)) {
263-
if (httpMethods.has(key) && !allowedMethods.includes(key)) {
264-
delete filteredSource[key]
249+
const refs = (sourcePathItem as any)[INLINE_REFS_FLAG]
250+
if (!isNonNullObject(sourcePathItem) || !refs || refs.length === 0) {
251+
continue
252+
}
253+
const richReference = parseRef(refs[0])
254+
const valueByPath = getValueByPath(sourceDocument, richReference.jsonPath)
255+
for (const method of Object.keys(valueByPath)) {
256+
const httpMethod = method as OpenAPIV3.HttpMethods
257+
if (!isValidHttpMethod(httpMethod)) continue
258+
259+
const methodData = sourcePathItem[httpMethod]
260+
const basePath = getOperationBasePath(
261+
methodData?.servers ||
262+
sourcePathItem?.servers ||
263+
[],
264+
)
265+
266+
const operationPath = basePath + path
267+
const operationId = slugify(`${removeFirstSlash(operationPath)}-${method}`)
268+
269+
if (!operations.includes(operationId)) {
270+
delete valueByPath[method]
271+
}
265272
}
266273
}
274+
}
267275

268-
return filteredSource
276+
function isNonNullObject(value: unknown): value is Record<string, unknown> {
277+
return typeof value === 'object' && value !== null
269278
}
270279

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))
280+
export const isComponentsSchemaRef = (path: JsonPath): boolean => {
281+
return !!matchPaths(
282+
[path],
283+
[[OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_SCHEMAS, PREDICATE_UNCLOSED_END]],
284+
)
278285
}
279286

280287
const isOperationPaths = (paths: JsonPath[]): boolean => {
@@ -296,51 +303,27 @@ const createSingleOperationSpec = (
296303
security?: OpenAPIV3.SecurityRequirementObject[],
297304
securitySchemes?: { [p: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.SecuritySchemeObject },
298305
): 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-
}
306+
const pathData = document.paths[path] as OpenAPIV3.PathItemObject
303307

304-
const baseSpec = {
308+
const ref = pathData.$ref
309+
const refFlag = (pathData as any)[INLINE_REFS_FLAG]
310+
return {
305311
openapi: openapi ?? '3.0.0',
306312
...takeIfDefined({ servers }),
307313
...takeIfDefined({ security }), // TODO: remove duplicates in security
314+
paths: {
315+
[path]: ref
316+
? pathData
317+
: {
318+
...extractCommonPathItemProperties(pathData),
319+
[method]: { ...pathData[method] },
320+
...(refFlag ? { [INLINE_REFS_FLAG]: refFlag } : {}),
321+
},
322+
},
308323
components: {
309324
...takeIfDefined({ securitySchemes }),
310325
},
311326
}
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-
}
344327
}
345328

346329
export const extractCommonPathItemProperties = (
@@ -352,3 +335,6 @@ export const extractCommonPathItemProperties = (
352335
...takeIfDefined({ parameters: pathData?.parameters }),
353336
})
354337

338+
function isValidHttpMethod(method: string): method is OpenAPIV3.HttpMethods {
339+
return (Object.values(OpenAPIV3.HttpMethods) as string[]).includes(method)
340+
}

src/strategies/document-group.strategy.ts

Lines changed: 22 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,32 @@ import { REST_API_TYPE } from '../apitypes'
2828
import {
2929
EXPORT_FORMAT_TO_FILE_FORMAT,
3030
fromBase64,
31-
getParentValueByRef,
32-
getValueByRefAndUpdate,
3331
removeFirstSlash,
3432
slugify,
33+
takeIfDefined,
3534
toVersionDocument,
3635
} from '../utils'
3736
import { OpenAPIV3 } from 'openapi-types'
3837
import { getOperationBasePath } from '../apitypes/rest/rest.utils'
3938
import { VersionRestDocument } from '../apitypes/rest/rest.types'
4039
import { FILE_FORMAT_JSON, INLINE_REFS_FLAG, NORMALIZE_OPTIONS } from '../consts'
4140
import { normalize } from '@netcracker/qubership-apihub-api-unifier'
42-
import { calculateSpecRefs, extractCommonPathItemProperties } from '../apitypes/rest/rest.operation'
41+
import {
42+
calculateSpecRefs,
43+
createSinglePathItemOperationSpec,
44+
extractCommonPathItemProperties,
45+
} from '../apitypes/rest/rest.operation'
4346

4447
function getTransformedDocument(document: ResolvedGroupDocument, format: FileFormat, packages: ResolvedReferenceMap): VersionRestDocument {
4548
const versionDocument = toVersionDocument(document, format)
4649

4750
const sourceDocument = extractDocumentData(versionDocument)
4851
versionDocument.data = transformDocumentData(versionDocument)
4952
const normalizedDocument = normalizeOpenApi(versionDocument.data, sourceDocument)
53+
createSinglePathItemOperationSpec(sourceDocument, normalizedDocument, versionDocument.operationIds)
5054
versionDocument.publish = true
5155

52-
calculateSpecRefs(sourceDocument, normalizedDocument, versionDocument.data)
56+
calculateSpecRefs(sourceDocument, normalizedDocument, versionDocument.data, versionDocument.operationIds)
5357

5458
// dashboard case
5559
if (document.packageRef) {
@@ -128,7 +132,7 @@ function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docu
128132
continue
129133
}
130134

131-
const commonProps = extractCommonPathItemProperties(sourcePathItem)
135+
const commonPathItemProperties = extractCommonPathItemProperties(sourcePathItem)
132136

133137
for (const method of Object.keys(normalizedPathItem)) {
134138
const httpMethod = method as OpenAPIV3.HttpMethods
@@ -149,29 +153,21 @@ function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docu
149153
continue
150154
}
151155

152-
const pathData = sourceDocument.paths[path]!
153-
if (sourcePathItem?.$ref) {
154-
handleRefPathItem(
155-
resultDocument,
156-
sourceDocument,
157-
path,
158-
pathData,
159-
httpMethod,
160-
)
161-
} else {
162-
handleInlinePathItem(
163-
resultDocument,
164-
path,
165-
pathData,
166-
httpMethod,
167-
commonProps,
168-
)
169-
}
156+
if (versionDocument.operationIds.includes(operationId)) {
157+
const pathData = sourceDocument.paths[path]!
158+
const ref = pathData.$ref ?? ''
159+
if (ref) {
160+
resultDocument.paths[path] = pathData
161+
} else {
162+
resultDocument.paths[path] = {
163+
...resultDocument.paths[path],
164+
...commonPathItemProperties,
165+
[httpMethod]: { ...pathData[httpMethod] },
166+
}
170167

171-
if (sourceComponents?.securitySchemes) {
168+
}
172169
resultDocument.components = {
173-
...resultDocument.components,
174-
securitySchemes: sourceComponents.securitySchemes,
170+
...takeIfDefined({ securitySchemes: sourceComponents?.securitySchemes }),
175171
}
176172
}
177173
}
@@ -180,48 +176,6 @@ function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docu
180176
return resultDocument
181177
}
182178

183-
function handleRefPathItem(
184-
resultDocument: OpenAPIV3.Document,
185-
sourceDocument: OpenAPIV3.Document,
186-
path: string,
187-
pathData: OpenAPIV3.PathItemObject | OpenAPIV3.ReferenceObject,
188-
method: OpenAPIV3.HttpMethods,
189-
): void {
190-
const ref = pathData.$ref ?? ''
191-
const operationsFormResult = getParentValueByRef(ref, resultDocument)
192-
193-
const cleanedDocument = getValueByRefAndUpdate(ref, sourceDocument, (pathItemObject: OpenAPIV3.PathItemObject) => ({
194-
...operationsFormResult,
195-
...extractCommonPathItemProperties(pathItemObject),
196-
[method]: { ...pathItemObject[method] },
197-
}))
198-
199-
resultDocument.paths[path] = pathData
200-
201-
if (cleanedDocument.components) {
202-
resultDocument.components = {
203-
...resultDocument.components,
204-
...cleanedDocument.components,
205-
}
206-
}
207-
}
208-
209-
function handleInlinePathItem(
210-
resultDoc: OpenAPIV3.Document,
211-
path: string,
212-
pathData: OpenAPIV3.PathItemObject,
213-
method: OpenAPIV3.HttpMethods,
214-
commonProps: Partial<OpenAPIV3.PathItemObject>,
215-
): void {
216-
const existingPath = resultDoc.paths[path]
217-
218-
resultDoc.paths[path] = {
219-
...existingPath,
220-
...commonProps,
221-
[method]: { ...pathData[method] },
222-
}
223-
}
224-
225179
function normalizeOpenApi(document: OpenAPIV3.Document, source?: OpenAPIV3.Document): OpenAPIV3.Document {
226180
return normalize(
227181
document,

0 commit comments

Comments
 (0)