Skip to content

Commit 30097c2

Browse files
committed
feat: Refactoring
1 parent 1caef30 commit 30097c2

File tree

3 files changed

+159
-91
lines changed

3 files changed

+159
-91
lines changed

src/apitypes/rest/rest.operation.ts

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import {
4848
grepValue,
4949
JSON_SCHEMA_PROPERTY_DEPRECATED,
5050
matchPaths,
51-
normalize,
5251
OPEN_API_PROPERTY_COMPONENTS,
5352
OPEN_API_PROPERTY_PATHS,
5453
OPEN_API_PROPERTY_SCHEMAS,
@@ -212,22 +211,39 @@ export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unkno
212211
if (!matchResult) {
213212
return
214213
}
214+
//todo why? description?
215215
const componentName = matchResult.grepValues[grepKey].toString()
216-
const component = getKeyValue(sourceDocument, ...matchResult.path)
217-
if (!component) {
216+
const sourceComponents = getKeyValue(sourceDocument, ...matchResult.path)
217+
const resultComponents = getKeyValue(resultSpec, ...matchResult.path)
218+
const httpMethods = new Set<string>(Object.values(OpenAPIV3.HttpMethods) as string[])
219+
const allowedOps = (typeof resultComponents === 'object' && resultComponents !== null)
220+
? Object.keys(resultComponents as object).filter(key => httpMethods.has(key))
221+
: []
222+
if (!sourceComponents || typeof sourceComponents !== 'object') {
218223
return
219224
}
225+
if (allowedOps.length > 0) {
226+
Object.keys(sourceComponents as object).forEach((key: string) => {
227+
if (httpMethods.has(key) && !allowedOps.includes(key)) {
228+
// prune operations not present in the partial result component
229+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
230+
// @ts-ignore
231+
delete (sourceComponents as any)[key]
232+
}
233+
})
234+
}
235+
// component is defined and object at this point
220236
if (models && !models[componentName] && isComponentsSchemaRef(matchResult.path)) {
221237
let componentHash = componentsHashMap?.get(componentName)
222238
if (componentHash) {
223239
models[componentName] = componentHash
224240
} else {
225-
componentHash = calculateObjectHash(component)
241+
componentHash = calculateObjectHash(sourceComponents)
226242
componentsHashMap?.set(componentName, componentHash)
227243
models[componentName] = componentHash
228244
}
229245
}
230-
setValueByPath(resultSpec, matchResult.path, component)
246+
setValueByPath(resultSpec, matchResult.path, sourceComponents)
231247
})
232248
}
233249

@@ -258,59 +274,34 @@ const createSingleOperationSpec = (
258274
): TYPE.RestOperationData => {
259275
const pathData = document.paths[path] as OpenAPIV3.PathItemObject
260276

261-
const resolveRefPathItem = (ref: string): OpenAPIV3.PathItemObject | null => {
262-
if (!ref) {
263-
return null
264-
}
265-
const target = normalize(pathData, { source: document }) as OpenAPIV3.PathItemObject
266-
if (!target || typeof target !== 'object') {
267-
return null
268-
}
277+
if (INLINE_REFS_FLAG in pathData || (pathData && '$ref' in pathData && pathData.$ref)) {
278+
//todo check
269279
return {
270-
...extractCommonPathItemProperties(target),
271-
[method]: { ...target[method] },
272-
}
273-
}
274-
275-
const buildComponentsFromRef = (ref: string): OpenAPIV3.ComponentsObject => {
276-
const resolved = resolveRefPathItem(ref)
277-
if (!resolved) {return {}}
278-
const { jsonPath } = parseRef(ref)
279-
const container: any = {}
280-
setValueByPath(container, jsonPath, resolved)
281-
return container.components || {}
282-
}
283-
284-
const specBase = {
285-
openapi: openapi ?? '3.0.0',
286-
...takeIfDefined({ servers }),
287-
...takeIfDefined({ security }), // TODO: remove duplicates in security
288-
components: {
289-
...takeIfDefined({ securitySchemes }),
290-
},
291-
}
292-
293-
if (pathData && '$ref' in pathData && pathData.$ref) {
294-
return {
295-
...specBase,
280+
openapi: openapi ?? '3.0.0',
281+
...takeIfDefined({ servers }),
282+
...takeIfDefined({ security }), // TODO: remove duplicates in security
296283
paths: {
297284
[path]: pathData,
298285
},
299286
components: {
300-
...specBase.components,
301-
...buildComponentsFromRef(pathData.$ref ?? ''),
287+
...takeIfDefined({ securitySchemes }),
302288
},
303289
}
304290
}
305291

306292
return {
307-
...specBase,
293+
openapi: openapi ?? '3.0.0',
294+
...takeIfDefined({ servers }),
295+
...takeIfDefined({ security }), // TODO: remove duplicates in security
308296
paths: {
309297
[path]: {
310298
...extractCommonPathItemProperties(pathData),
311299
[method]: { ...pathData[method] },
312300
},
313301
},
302+
components: {
303+
...takeIfDefined({ securitySchemes }),
304+
},
314305
}
315306
}
316307

src/strategies/document-group.strategy.ts

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { REST_API_TYPE } from '../apitypes'
2828
import {
2929
EXPORT_FORMAT_TO_FILE_FORMAT,
3030
fromBase64,
31+
getParentValueByRef,
32+
resolveRefAndMap,
3133
removeFirstSlash,
3234
slugify,
3335
takeIfDefined,
@@ -37,9 +39,8 @@ import { OpenAPIV3 } from 'openapi-types'
3739
import { getOperationBasePath } from '../apitypes/rest/rest.utils'
3840
import { VersionRestDocument } from '../apitypes/rest/rest.types'
3941
import { FILE_FORMAT_JSON, INLINE_REFS_FLAG, NORMALIZE_OPTIONS } from '../consts'
40-
import { normalize, parseRef } from '@netcracker/qubership-apihub-api-unifier'
42+
import { normalize } from '@netcracker/qubership-apihub-api-unifier'
4143
import { calculateSpecRefs, extractCommonPathItemProperties } from '../apitypes/rest/rest.operation'
42-
import { getValueByPath } from '../utils/path'
4344

4445
function getTransformedDocument(document: ResolvedGroupDocument, format: FileFormat, packages: ResolvedReferenceMap): VersionRestDocument {
4546
const versionDocument = toVersionDocument(document, format)
@@ -97,7 +98,7 @@ export class DocumentGroupStrategy implements BuilderStrategy {
9798
}
9899
}
99100

100-
function parseBase64String(value: string): object {
101+
function parseBase64String(value: string): unknown {
101102
return JSON.parse(fromBase64(value))
102103
}
103104

@@ -112,7 +113,6 @@ function extractDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docume
112113
function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Document {
113114
const sourceDocument = extractDocumentData(versionDocument)
114115
const normalizedDocument = normalizeOpenApi(sourceDocument)
115-
116116
const { paths: sourcePaths, components: sourceComponents, ...restOfSource } = sourceDocument
117117
const { paths: normalizedPaths } = normalizedDocument
118118

@@ -121,7 +121,8 @@ function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docu
121121
paths: {},
122122
}
123123

124-
for (const path of Object.keys(normalizedPaths)) {
124+
const normalizedPathKeys = Object.keys(normalizedPaths)
125+
for (const path of normalizedPathKeys) {
125126
const sourcePathItem = sourcePaths[path]
126127
const normalizedPathItem = normalizedPaths[path]
127128

@@ -130,39 +131,53 @@ function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docu
130131
}
131132

132133
const commonPathProps = extractCommonPathItemProperties(sourcePathItem)
133-
const pathItemRef = sourcePathItem?.$ref
134134

135-
for (const method of Object.keys(normalizedPathItem)) {
135+
const methodKeys = Object.keys(normalizedPathItem)
136+
for (const method of methodKeys) {
136137
const inferredMethod = method as OpenAPIV3.HttpMethods
138+
139+
// check if field is a valid openapi http method defined in OpenAPIV3.HttpMethods
137140
if (!isValidHttpMethod(inferredMethod)) {
138141
continue
139142
}
140143

141144
const methodData = normalizedPathItem[inferredMethod]
142-
const basePath = getOperationBasePath(methodData?.servers || sourcePathItem?.servers || sourcePathItem?.servers || [])
145+
const basePath = getOperationBasePath(methodData?.servers || sourcePathItem?.servers || [])
143146
const operationPath = basePath + path
144-
145147
const operationId = slugify(`${removeFirstSlash(operationPath)}-${method}`)
146148

147149
if (!versionDocument.operationIds.includes(operationId)) {
148150
continue
149151
}
150152

151-
const updatedPathItem = buildPath(
152-
sourceDocument,
153-
path,
154-
inferredMethod,
155-
commonPathProps,
156-
pathItemRef,
157-
)
158-
159-
resultDocument.paths[path] = {
160-
...(resultDocument.paths[path] || {}),
161-
...updatedPathItem,
162-
}
163-
resultDocument.components = {
164-
...takeIfDefined({ securitySchemes: sourceComponents?.securitySchemes }),
153+
const pathItemRef = sourcePathItem?.$ref
154+
const pathData = sourceDocument.paths[path]!
155+
if (pathItemRef) {
156+
const targetFromResultDocument = getParentValueByRef(resultDocument, pathData.$ref ?? '')
157+
const target = resolveRefAndMap(sourceDocument, pathData.$ref ?? '', (value) => ({
158+
...targetFromResultDocument,
159+
...extractCommonPathItemProperties(value),
160+
[method]: { ...value[method] },
161+
}))
162+
163+
resultDocument.paths[path] = pathData
164+
165+
resultDocument.components = {
166+
...takeIfDefined({ securitySchemes: sourceComponents?.securitySchemes }),
167+
...target.components,
168+
}
169+
} else {
170+
const existingPath = resultDocument.paths[path]
171+
resultDocument.paths[path] = {
172+
...existingPath,
173+
...commonPathProps,
174+
[inferredMethod]: { ...pathData[inferredMethod] },
175+
}
176+
resultDocument.components = {
177+
...takeIfDefined({ securitySchemes: sourceComponents?.securitySchemes }),
178+
}
165179
}
180+
166181
}
167182
}
168183

@@ -187,29 +202,3 @@ function isValidHttpMethod(method: string): method is OpenAPIV3.HttpMethods {
187202
function isNonNullObject(value: unknown): value is Record<string, unknown> {
188203
return typeof value === 'object' && value !== null
189204
}
190-
191-
function buildPath(
192-
sourceDocument: OpenAPIV3.Document,
193-
path: string,
194-
method: OpenAPIV3.HttpMethods,
195-
commonPathProps: Partial<OpenAPIV3.PathItemObject>,
196-
pathItemRef?: string,
197-
): OpenAPIV3.PathItemObject {
198-
if (!pathItemRef) {
199-
const originalPathItem = sourceDocument.paths[path]!
200-
return {
201-
...commonPathProps,
202-
[method]: { ...originalPathItem[method] },
203-
} as OpenAPIV3.PathItemObject
204-
}
205-
206-
const { jsonPath } = parseRef(pathItemRef)
207-
const targetPathItem = getValueByPath(sourceDocument, jsonPath) as OpenAPIV3.PathItemObject
208-
if (!targetPathItem) return {}
209-
210-
const originalPathItem = sourceDocument.paths[path]!
211-
return {
212-
...(originalPathItem),
213-
...commonPathProps,
214-
}
215-
}

src/utils/builder.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
import { API_KIND } from '../consts'
3030
import { Diff, DiffType } from '@netcracker/qubership-apihub-api-diff'
3131
import { JsonPath } from '@netcracker/qubership-apihub-json-crawl'
32+
import { parseRef } from '@netcracker/qubership-apihub-api-unifier'
3233

3334
export type ObjPath = (string | number)[]
3435

@@ -169,6 +170,93 @@ export const getValueByOpenAPIPath = (obj: any, path: string): any => {
169170
return value
170171
}
171172

173+
const isRecordObject = (candidate: unknown): candidate is Record<string, any> => {
174+
return typeof candidate === 'object' && candidate !== null
175+
}
176+
177+
const getValueByJsonPath = (root: any, path: JsonPath): any => {
178+
let current: any = root
179+
for (const part of path) {
180+
if (typeof part === 'string') {
181+
if (part === '$') {
182+
continue
183+
}
184+
if (part === '*') {
185+
const keys = Object.keys(current ?? {})
186+
current = keys.length > 0 ? current[keys[0]] : undefined
187+
continue
188+
}
189+
}
190+
current = current?.[part as any]
191+
}
192+
return current
193+
}
194+
195+
export const getParentValueByRef = (obj: any, ref: string): any => {
196+
const visited = new Set<string>()
197+
let currentRef: string | undefined = ref
198+
199+
while (currentRef) {
200+
if (visited.has(currentRef)) {
201+
// circular reference guard
202+
return undefined
203+
}
204+
visited.add(currentRef)
205+
206+
const { jsonPath } = parseRef(currentRef)
207+
const value = getValueByJsonPath(obj, jsonPath)
208+
209+
if (isRecordObject(value) && typeof value.$ref === 'string') {
210+
currentRef = value.$ref
211+
continue
212+
}
213+
return value
214+
}
215+
216+
return undefined
217+
}
218+
219+
export const resolveRefAndMap = (
220+
obj: any,
221+
ref: string,
222+
valueMapper: (target: any) => any,
223+
component: any = {},
224+
): any => {
225+
const visited = new Set<string>()
226+
let currentRef: string | undefined = ref
227+
let lastPath: JsonPath = []
228+
229+
while (currentRef) {
230+
if (visited.has(currentRef)) {
231+
// circular reference guard
232+
break
233+
}
234+
visited.add(currentRef)
235+
236+
const { jsonPath } = parseRef(currentRef)
237+
lastPath = jsonPath
238+
const value = getValueByJsonPath(obj, jsonPath)
239+
240+
if (isRecordObject(value) && typeof value.$ref === 'string') {
241+
// preserve intermediate referenced node
242+
setValueByPath(component, jsonPath, value)
243+
currentRef = value.$ref
244+
continue
245+
}
246+
247+
// terminal value reached; apply mapper
248+
setValueByPath(component, jsonPath, valueMapper(value))
249+
return component
250+
}
251+
252+
// Fallback when loop breaks due to cycle or missing ref
253+
if (lastPath.length) {
254+
const terminal = getValueByJsonPath(obj, lastPath)
255+
setValueByPath(component, lastPath, valueMapper(terminal))
256+
}
257+
return component
258+
}
259+
172260
export const rawToApiKind = (apiKindLike: string): ApiKind => {
173261
const candidate = apiKindLike.toLowerCase() as ApiKind
174262
return [API_KIND.BWC, API_KIND.NO_BWC, API_KIND.EXPERIMENTAL].includes(candidate) ? candidate : API_KIND.BWC

0 commit comments

Comments
 (0)