From 793e0bcee5d95e650fbf798fbf1b5f1c6b25dee5 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Fri, 12 Sep 2025 17:51:40 +0400 Subject: [PATCH 1/6] feat: POC ref pathItems support --- src/apitypes/rest/rest.operation.ts | 160 ++++++++++------------ src/strategies/document-group.strategy.ts | 90 +++--------- src/utils/builder.ts | 90 ------------ 3 files changed, 95 insertions(+), 245 deletions(-) diff --git a/src/apitypes/rest/rest.operation.ts b/src/apitypes/rest/rest.operation.ts index 3f9b0b4..0998ede 100644 --- a/src/apitypes/rest/rest.operation.ts +++ b/src/apitypes/rest/rest.operation.ts @@ -19,12 +19,13 @@ import { OpenAPIV3 } from 'openapi-types' import { REST_API_TYPE, REST_KIND_KEY } from './rest.consts' import { operationRules } from './rest.rules' import type * as TYPE from './rest.types' -import type { +import { BuildConfig, CrawlRule, DeprecateItem, NotificationMessage, OperationCrawlState, + OperationId, SearchScopes, } from '../../types' import { @@ -32,17 +33,19 @@ import { capitalize, getKeyValue, getSplittedVersionKey, - getValueByRefAndUpdate, isDeprecatedOperationItem, + isObject, isOperationDeprecated, normalizePath, rawToApiKind, + removeFirstSlash, setValueByPath, + slugify, takeIf, takeIfDefined, } from '../../utils' import { API_KIND, INLINE_REFS_FLAG, ORIGINS_SYMBOL, VERSION_STATUS } from '../../consts' -import { getCustomTags, resolveApiAudience } from './rest.utils' +import { getCustomTags, getOperationBasePath, resolveApiAudience } from './rest.utils' import { DebugPerformanceContext, syncDebugPerformance } from '../../utils/logs' import { calculateDeprecatedItems, @@ -50,7 +53,6 @@ import { JSON_SCHEMA_PROPERTY_DEPRECATED, matchPaths, OPEN_API_PROPERTY_COMPONENTS, - OPEN_API_PROPERTY_PATH_ITEMS, OPEN_API_PROPERTY_PATHS, OPEN_API_PROPERTY_SCHEMAS, parseRef, @@ -61,6 +63,7 @@ import { } from '@netcracker/qubership-apihub-api-unifier' import { calculateObjectHash } from '../../utils/hashes' import { calculateTolerantHash } from '../../components/deprecated' +import { getValueByPath } from '../../utils/path' export const buildRestOperation = ( operationId: string, @@ -145,7 +148,8 @@ export const buildRestOperation = ( security, components?.securitySchemes, ) - calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, models, componentsHashMap) + calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, [operationId], models, componentsHashMap) + createSinglePathItemOperationSpec(specWithSingleOperation as OpenAPIV3.Document, refsOnlySingleOperationSpec as OpenAPIV3.Document, [operationId]) const dataHash = calculateObjectHash(specWithSingleOperation) return [specWithSingleOperation, dataHash] }, debugCtx) @@ -182,7 +186,7 @@ export const buildRestOperation = ( } } -export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unknown, resultSpec: unknown, models?: Record, componentsHashMap?: Map): void => { +export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unknown, resultSpec: unknown, operations: OperationId[], models?: Record, componentsHashMap?: Map): void => { const handledObjects = new Set() const inlineRefs = new Set() syncCrawl( @@ -214,67 +218,70 @@ export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unkno return } const componentName = matchResult.grepValues[grepKey].toString() - let sourceComponents = getKeyValue(sourceDocument, ...matchResult.path) - if (!sourceComponents || typeof sourceComponents !== 'object') { + let component: any = getKeyValue(sourceDocument, ...matchResult.path) + if (!component) { return } - - if (typeof sourceComponents === 'object') { - const allowedOps = getAllowedHttpOps(resultSpec, matchResult.path) - if (allowedOps.length > 0 && isComponentsPathItemRef(matchResult.path)) { - sourceComponents = filterPathItemOperations(sourceComponents, allowedOps) - } + if (isObject(component)) { + component = { ...component } } if (models && !models[componentName] && isComponentsSchemaRef(matchResult.path)) { - const existingHash = componentsHashMap?.get(componentName) - if (existingHash) { - models[componentName] = existingHash + let componentHash = componentsHashMap?.get(componentName) + if (componentHash) { + models[componentName] = componentHash } else { - const componentHashCalculated = calculateObjectHash(sourceComponents as object) - componentsHashMap?.set(componentName, componentHashCalculated) - models[componentName] = componentHashCalculated + componentHash = calculateObjectHash(component) + componentsHashMap?.set(componentName, componentHash) + models[componentName] = componentHash } } - setValueByPath(resultSpec, matchResult.path, sourceComponents) + + setValueByPath(resultSpec, matchResult.path, component) }) } -export const isComponentsSchemaRef = (path: JsonPath): boolean => { - return !!matchPaths( - [path], - [[OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_SCHEMAS, PREDICATE_UNCLOSED_END]], - ) -} -export const isComponentsPathItemRef = (path: JsonPath): boolean => { - return !!matchPaths( - [path], - [[OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PATH_ITEMS, PREDICATE_UNCLOSED_END]], - ) -} +export function createSinglePathItemOperationSpec(sourceDocument: OpenAPIV3.Document, normalizedDocument: OpenAPIV3.Document, operations: OperationId[]): void { + const { paths } = normalizedDocument -export const filterPathItemOperations = ( - source: unknown, - allowedMethods: string[], -): unknown => { - const httpMethods = new Set(Object.values(OpenAPIV3.HttpMethods) as string[]) - const filteredSource: Record = { ...(source as Record) } + for (const path of Object.keys(paths)) { + const sourcePathItem = paths[path] - for (const key of Object.keys(filteredSource)) { - if (httpMethods.has(key) && !allowedMethods.includes(key)) { - delete filteredSource[key] + const refs = (sourcePathItem as any)[INLINE_REFS_FLAG] + if (!isNonNullObject(sourcePathItem) || !refs || refs.length === 0) { + continue + } + const richReference = parseRef(refs[0]) + const valueByPath = getValueByPath(sourceDocument, richReference.jsonPath) + for (const method of Object.keys(valueByPath)) { + const httpMethod = method as OpenAPIV3.HttpMethods + if (!isValidHttpMethod(httpMethod)) continue + + const methodData = sourcePathItem[httpMethod] + const basePath = getOperationBasePath( + methodData?.servers || + sourcePathItem?.servers || + [], + ) + + const operationPath = basePath + path + const operationId = slugify(`${removeFirstSlash(operationPath)}-${method}`) + + if (!operations.includes(operationId)) { + delete valueByPath[method] + } } } +} - return filteredSource +function isNonNullObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null } -export const getAllowedHttpOps = (resultSpec: unknown, jsonPath: JsonPath): string[] => { - const resultComponents = getKeyValue(resultSpec, ...jsonPath) as unknown - if (typeof resultComponents !== 'object' || resultComponents === null) { - return [] - } - const httpMethods = new Set(Object.values(OpenAPIV3.HttpMethods) as string[]) - return Object.keys(resultComponents as Record).filter(key => httpMethods.has(key)) +export const isComponentsSchemaRef = (path: JsonPath): boolean => { + return !!matchPaths( + [path], + [[OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_SCHEMAS, PREDICATE_UNCLOSED_END]], + ) } const isOperationPaths = (paths: JsonPath[]): boolean => { @@ -296,51 +303,27 @@ const createSingleOperationSpec = ( security?: OpenAPIV3.SecurityRequirementObject[], securitySchemes?: { [p: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.SecuritySchemeObject }, ): TYPE.RestOperationData => { - const pathData = document.paths[path] as OpenAPIV3.PathItemObject | undefined - if (!pathData) { - throw new Error(`Path "${path}" not found in the document`) - } + const pathData = document.paths[path] as OpenAPIV3.PathItemObject - const baseSpec = { + const ref = pathData.$ref + const refFlag = (pathData as any)[INLINE_REFS_FLAG] + return { openapi: openapi ?? '3.0.0', ...takeIfDefined({ servers }), ...takeIfDefined({ security }), // TODO: remove duplicates in security + paths: { + [path]: ref + ? pathData + : { + ...extractCommonPathItemProperties(pathData), + [method]: { ...pathData[method] }, + ...(refFlag ? { [INLINE_REFS_FLAG]: refFlag } : {}), + }, + }, components: { ...takeIfDefined({ securitySchemes }), }, } - - const ref = pathData.$ref - if (ref) { - const cleanedDocument = getValueByRefAndUpdate( - ref, - document, - (pathItemObject: OpenAPIV3.PathItemObject) => ({ - ...extractCommonPathItemProperties(pathItemObject), - [method]: { ...pathItemObject[method] }, - })) - - return { - ...baseSpec, - paths: { - [path]: pathData, - }, - components: { - ...baseSpec.components, - ...cleanedDocument.components ?? {}, - }, - } - } - - return { - ...baseSpec, - paths: { - [path]: { - ...extractCommonPathItemProperties(pathData), - [method]: { ...pathData[method] }, - }, - }, - } } export const extractCommonPathItemProperties = ( @@ -352,3 +335,6 @@ export const extractCommonPathItemProperties = ( ...takeIfDefined({ parameters: pathData?.parameters }), }) +function isValidHttpMethod(method: string): method is OpenAPIV3.HttpMethods { + return (Object.values(OpenAPIV3.HttpMethods) as string[]).includes(method) +} diff --git a/src/strategies/document-group.strategy.ts b/src/strategies/document-group.strategy.ts index 3381c4d..da25da2 100644 --- a/src/strategies/document-group.strategy.ts +++ b/src/strategies/document-group.strategy.ts @@ -28,10 +28,9 @@ import { REST_API_TYPE } from '../apitypes' import { EXPORT_FORMAT_TO_FILE_FORMAT, fromBase64, - getParentValueByRef, - getValueByRefAndUpdate, removeFirstSlash, slugify, + takeIfDefined, toVersionDocument, } from '../utils' import { OpenAPIV3 } from 'openapi-types' @@ -39,7 +38,11 @@ import { getOperationBasePath } from '../apitypes/rest/rest.utils' import { VersionRestDocument } from '../apitypes/rest/rest.types' import { FILE_FORMAT_JSON, INLINE_REFS_FLAG, NORMALIZE_OPTIONS } from '../consts' import { normalize } from '@netcracker/qubership-apihub-api-unifier' -import { calculateSpecRefs, extractCommonPathItemProperties } from '../apitypes/rest/rest.operation' +import { + calculateSpecRefs, + createSinglePathItemOperationSpec, + extractCommonPathItemProperties, +} from '../apitypes/rest/rest.operation' function getTransformedDocument(document: ResolvedGroupDocument, format: FileFormat, packages: ResolvedReferenceMap): VersionRestDocument { const versionDocument = toVersionDocument(document, format) @@ -47,9 +50,10 @@ function getTransformedDocument(document: ResolvedGroupDocument, format: FileFor const sourceDocument = extractDocumentData(versionDocument) versionDocument.data = transformDocumentData(versionDocument) const normalizedDocument = normalizeOpenApi(versionDocument.data, sourceDocument) + createSinglePathItemOperationSpec(sourceDocument, normalizedDocument, versionDocument.operationIds) versionDocument.publish = true - calculateSpecRefs(sourceDocument, normalizedDocument, versionDocument.data) + calculateSpecRefs(sourceDocument, normalizedDocument, versionDocument.data, versionDocument.operationIds) // dashboard case if (document.packageRef) { @@ -128,7 +132,7 @@ function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docu continue } - const commonProps = extractCommonPathItemProperties(sourcePathItem) + const commonPathItemProperties = extractCommonPathItemProperties(sourcePathItem) for (const method of Object.keys(normalizedPathItem)) { const httpMethod = method as OpenAPIV3.HttpMethods @@ -149,29 +153,21 @@ function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docu continue } - const pathData = sourceDocument.paths[path]! - if (sourcePathItem?.$ref) { - handleRefPathItem( - resultDocument, - sourceDocument, - path, - pathData, - httpMethod, - ) - } else { - handleInlinePathItem( - resultDocument, - path, - pathData, - httpMethod, - commonProps, - ) - } + if (versionDocument.operationIds.includes(operationId)) { + const pathData = sourceDocument.paths[path]! + const ref = pathData.$ref ?? '' + if (ref) { + resultDocument.paths[path] = pathData + } else { + resultDocument.paths[path] = { + ...resultDocument.paths[path], + ...commonPathItemProperties, + [httpMethod]: { ...pathData[httpMethod] }, + } - if (sourceComponents?.securitySchemes) { + } resultDocument.components = { - ...resultDocument.components, - securitySchemes: sourceComponents.securitySchemes, + ...takeIfDefined({ securitySchemes: sourceComponents?.securitySchemes }), } } } @@ -180,48 +176,6 @@ function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docu return resultDocument } -function handleRefPathItem( - resultDocument: OpenAPIV3.Document, - sourceDocument: OpenAPIV3.Document, - path: string, - pathData: OpenAPIV3.PathItemObject | OpenAPIV3.ReferenceObject, - method: OpenAPIV3.HttpMethods, -): void { - const ref = pathData.$ref ?? '' - const operationsFormResult = getParentValueByRef(ref, resultDocument) - - const cleanedDocument = getValueByRefAndUpdate(ref, sourceDocument, (pathItemObject: OpenAPIV3.PathItemObject) => ({ - ...operationsFormResult, - ...extractCommonPathItemProperties(pathItemObject), - [method]: { ...pathItemObject[method] }, - })) - - resultDocument.paths[path] = pathData - - if (cleanedDocument.components) { - resultDocument.components = { - ...resultDocument.components, - ...cleanedDocument.components, - } - } -} - -function handleInlinePathItem( - resultDoc: OpenAPIV3.Document, - path: string, - pathData: OpenAPIV3.PathItemObject, - method: OpenAPIV3.HttpMethods, - commonProps: Partial, -): void { - const existingPath = resultDoc.paths[path] - - resultDoc.paths[path] = { - ...existingPath, - ...commonProps, - [method]: { ...pathData[method] }, - } -} - function normalizeOpenApi(document: OpenAPIV3.Document, source?: OpenAPIV3.Document): OpenAPIV3.Document { return normalize( document, diff --git a/src/utils/builder.ts b/src/utils/builder.ts index 943bc6e..87b6a78 100644 --- a/src/utils/builder.ts +++ b/src/utils/builder.ts @@ -171,96 +171,6 @@ export const getValueByOpenAPIPath = (obj: any, path: string): any => { return value } -const isRecordObject = (candidate: unknown): candidate is Record => { - return typeof candidate === 'object' && candidate !== null -} - -const getValueByJsonPath = (root: any, path: JsonPath): any => { - let current: any = root - for (const part of path) { - if (typeof part === 'string') { - if (part === '$') { - continue - } - if (part === '*') { - const keys = Object.keys(current ?? {}) - current = keys.length > 0 ? current[keys[0]] : undefined - continue - } - } - current = current?.[part as any] - } - return current -} - -export const getParentValueByRef = (ref: string, document: OpenAPIV3.Document): any => { - const cyclingGuard = new Set() - let currentRef: string | undefined = ref - - while (currentRef) { - if (cyclingGuard.has(currentRef)) { - return undefined - } - cyclingGuard.add(currentRef) - - const { jsonPath } = parseRef(currentRef) - const value = getValueByJsonPath(document, jsonPath) - - if (isRecordObject(value) && typeof value.$ref === 'string') { - currentRef = value.$ref - continue - } - return value - } - - return undefined -} - -export const getValueByRefAndUpdate = ( - ref: string, - document: OpenAPIV3.Document, - valueMapper: (target: Record) => Record, - result: Record = {}, -): Record => { - const cyclingGuard = new Set() - let currentRef: string | undefined = ref - let lastPath: JsonPath = [] - - while (currentRef) { - if (cyclingGuard.has(currentRef)) { - break - } - cyclingGuard.add(currentRef) - - const { jsonPath } = parseRef(currentRef) - lastPath = jsonPath - - const rawValue = getValueByJsonPath(document, jsonPath) - if (rawValue === null) { - break - } - - const value = { ...rawValue } - - if (isRecordObject(value) && typeof value.$ref === 'string') { - setValueByPath(result, jsonPath, value) - currentRef = value.$ref - continue - } - - setValueByPath(result, jsonPath, valueMapper(value)) - return result - } - - if (lastPath.length) { - const terminal = getValueByJsonPath(document, lastPath) - if (terminal !== undefined) { - setValueByPath(result, lastPath, valueMapper(terminal)) - } - } - return result -} - export const rawToApiKind = (apiKindLike: string): ApiKind => { const candidate = apiKindLike.toLowerCase() as ApiKind return [API_KIND.BWC, API_KIND.NO_BWC, API_KIND.EXPERIMENTAL].includes(candidate) ? candidate : API_KIND.BWC From c00bce3f0b0046e0e2df71114296639ff57f3dfe Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 15 Sep 2025 10:23:37 +0400 Subject: [PATCH 2/6] feat: refactoring --- src/apitypes/rest/rest.operation.ts | 23 +++++++++++++++++---- src/strategies/document-group.strategy.ts | 25 ++++++++--------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/apitypes/rest/rest.operation.ts b/src/apitypes/rest/rest.operation.ts index 0998ede..0627417 100644 --- a/src/apitypes/rest/rest.operation.ts +++ b/src/apitypes/rest/rest.operation.ts @@ -19,6 +19,7 @@ import { OpenAPIV3 } from 'openapi-types' import { REST_API_TYPE, REST_KIND_KEY } from './rest.consts' import { operationRules } from './rest.rules' import type * as TYPE from './rest.types' +import { RestOperationData } from './rest.types' import { BuildConfig, CrawlRule, @@ -149,7 +150,6 @@ export const buildRestOperation = ( components?.securitySchemes, ) calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, [operationId], models, componentsHashMap) - createSinglePathItemOperationSpec(specWithSingleOperation as OpenAPIV3.Document, refsOnlySingleOperationSpec as OpenAPIV3.Document, [operationId]) const dataHash = calculateObjectHash(specWithSingleOperation) return [specWithSingleOperation, dataHash] }, debugCtx) @@ -186,7 +186,14 @@ export const buildRestOperation = ( } } -export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unknown, resultSpec: unknown, operations: OperationId[], models?: Record, componentsHashMap?: Map): void => { +export const calculateSpecRefs = ( + sourceDocument: TYPE.RestOperationData, + normalizedSpec: TYPE.RestOperationData, + resultSpec: TYPE.RestOperationData, + operations: OperationId[], + models?: Record, + componentsHashMap?: Map, +): void => { const handledObjects = new Set() const inlineRefs = new Set() syncCrawl( @@ -218,7 +225,7 @@ export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unkno return } const componentName = matchResult.grepValues[grepKey].toString() - let component: any = getKeyValue(sourceDocument, ...matchResult.path) + let component = getKeyValue(sourceDocument, ...matchResult.path) as Record if (!component) { return } @@ -238,9 +245,17 @@ export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unkno setValueByPath(resultSpec, matchResult.path, component) }) + + if (operations?.length) { + createSinglePathItemOperationSpec(resultSpec, normalizedSpec, operations) + } } -export function createSinglePathItemOperationSpec(sourceDocument: OpenAPIV3.Document, normalizedDocument: OpenAPIV3.Document, operations: OperationId[]): void { +export function createSinglePathItemOperationSpec( + sourceDocument: RestOperationData, + normalizedDocument: RestOperationData, + operations: OperationId[], +): void { const { paths } = normalizedDocument for (const path of Object.keys(paths)) { diff --git a/src/strategies/document-group.strategy.ts b/src/strategies/document-group.strategy.ts index da25da2..925f246 100644 --- a/src/strategies/document-group.strategy.ts +++ b/src/strategies/document-group.strategy.ts @@ -38,11 +38,7 @@ import { getOperationBasePath } from '../apitypes/rest/rest.utils' import { VersionRestDocument } from '../apitypes/rest/rest.types' import { FILE_FORMAT_JSON, INLINE_REFS_FLAG, NORMALIZE_OPTIONS } from '../consts' import { normalize } from '@netcracker/qubership-apihub-api-unifier' -import { - calculateSpecRefs, - createSinglePathItemOperationSpec, - extractCommonPathItemProperties, -} from '../apitypes/rest/rest.operation' +import { calculateSpecRefs, extractCommonPathItemProperties } from '../apitypes/rest/rest.operation' function getTransformedDocument(document: ResolvedGroupDocument, format: FileFormat, packages: ResolvedReferenceMap): VersionRestDocument { const versionDocument = toVersionDocument(document, format) @@ -50,7 +46,6 @@ function getTransformedDocument(document: ResolvedGroupDocument, format: FileFor const sourceDocument = extractDocumentData(versionDocument) versionDocument.data = transformDocumentData(versionDocument) const normalizedDocument = normalizeOpenApi(versionDocument.data, sourceDocument) - createSinglePathItemOperationSpec(sourceDocument, normalizedDocument, versionDocument.operationIds) versionDocument.publish = true calculateSpecRefs(sourceDocument, normalizedDocument, versionDocument.data, versionDocument.operationIds) @@ -155,17 +150,15 @@ function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docu if (versionDocument.operationIds.includes(operationId)) { const pathData = sourceDocument.paths[path]! - const ref = pathData.$ref ?? '' - if (ref) { - resultDocument.paths[path] = pathData - } else { - resultDocument.paths[path] = { - ...resultDocument.paths[path], - ...commonPathItemProperties, - [httpMethod]: { ...pathData[httpMethod] }, - } + const isContainsRef = !!pathData.$ref + resultDocument.paths[path] = isContainsRef + ? pathData + : { + ...resultDocument.paths[path], + ...commonPathItemProperties, + [httpMethod]: { ...pathData[httpMethod] }, + } - } resultDocument.components = { ...takeIfDefined({ securitySchemes: sourceComponents?.securitySchemes }), } From 6203ae9204145ecfca7d53d9ae7a2333bfb3f6bc Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 15 Sep 2025 10:40:25 +0400 Subject: [PATCH 3/6] feat: typing --- src/apitypes/rest/rest.operation.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/apitypes/rest/rest.operation.ts b/src/apitypes/rest/rest.operation.ts index 0627417..c05973c 100644 --- a/src/apitypes/rest/rest.operation.ts +++ b/src/apitypes/rest/rest.operation.ts @@ -259,13 +259,18 @@ export function createSinglePathItemOperationSpec( const { paths } = normalizedDocument for (const path of Object.keys(paths)) { - const sourcePathItem = paths[path] - - const refs = (sourcePathItem as any)[INLINE_REFS_FLAG] - if (!isNonNullObject(sourcePathItem) || !refs || refs.length === 0) { + const sourcePathItem = paths[path] as OpenAPIV3.PathItemObject + if (!isNonNullObject(sourcePathItem)) { + continue + } + const refs = hasInlineRefsFlag(sourcePathItem) ? sourcePathItem[INLINE_REFS_FLAG] : [] + if (refs.length === 0) { continue } const richReference = parseRef(refs[0]) + if (!richReference) { + continue + } const valueByPath = getValueByPath(sourceDocument, richReference.jsonPath) for (const method of Object.keys(valueByPath)) { const httpMethod = method as OpenAPIV3.HttpMethods @@ -306,6 +311,10 @@ const isOperationPaths = (paths: JsonPath[]): boolean => { ) } +function hasInlineRefsFlag(obj: unknown): obj is { [INLINE_REFS_FLAG]: string[] } { + return typeof obj === 'object' && obj !== null && INLINE_REFS_FLAG in obj +} + // todo output of this method disrupts document normalization. // origin symbols are not being transferred to the resulting spec. // DO NOT pass output of this method to apiDiff @@ -320,14 +329,14 @@ const createSingleOperationSpec = ( ): TYPE.RestOperationData => { const pathData = document.paths[path] as OpenAPIV3.PathItemObject - const ref = pathData.$ref - const refFlag = (pathData as any)[INLINE_REFS_FLAG] + const isContainsRef = !!pathData.$ref + const refFlag = hasInlineRefsFlag(pathData) ? pathData[INLINE_REFS_FLAG] : false return { openapi: openapi ?? '3.0.0', ...takeIfDefined({ servers }), ...takeIfDefined({ security }), // TODO: remove duplicates in security paths: { - [path]: ref + [path]: isContainsRef ? pathData : { ...extractCommonPathItemProperties(pathData), From 665fec89eb4a373f74694a6cd21f23c846b17725 Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 15 Sep 2025 11:26:51 +0400 Subject: [PATCH 4/6] feat: Added cleaning PathItemObject in components --- src/apitypes/rest/rest.operation.ts | 48 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/apitypes/rest/rest.operation.ts b/src/apitypes/rest/rest.operation.ts index c05973c..3859e10 100644 --- a/src/apitypes/rest/rest.operation.ts +++ b/src/apitypes/rest/rest.operation.ts @@ -263,7 +263,7 @@ export function createSinglePathItemOperationSpec( if (!isNonNullObject(sourcePathItem)) { continue } - const refs = hasInlineRefsFlag(sourcePathItem) ? sourcePathItem[INLINE_REFS_FLAG] : [] + const refs: string[] = hasInlineRefsFlag(sourcePathItem) ? sourcePathItem[INLINE_REFS_FLAG] : [] if (refs.length === 0) { continue } @@ -271,24 +271,36 @@ export function createSinglePathItemOperationSpec( if (!richReference) { continue } - const valueByPath = getValueByPath(sourceDocument, richReference.jsonPath) - for (const method of Object.keys(valueByPath)) { - const httpMethod = method as OpenAPIV3.HttpMethods - if (!isValidHttpMethod(httpMethod)) continue - - const methodData = sourcePathItem[httpMethod] - const basePath = getOperationBasePath( - methodData?.servers || - sourcePathItem?.servers || - [], - ) - - const operationPath = basePath + path - const operationId = slugify(`${removeFirstSlash(operationPath)}-${method}`) - - if (!operations.includes(operationId)) { - delete valueByPath[method] + const valueByPath = getValueByPath(sourceDocument, richReference.jsonPath) as OpenAPIV3.PathItemObject + + const operationIds: OpenAPIV3.HttpMethods[] = (Object.keys(valueByPath) as OpenAPIV3.HttpMethods[]) + .filter((httpMethod) => isValidHttpMethod(httpMethod)) + .filter(httpMethod => { + const methodData = sourcePathItem[httpMethod as OpenAPIV3.HttpMethods] + if (!methodData) return false + const basePath = getOperationBasePath( + methodData?.servers || + sourcePathItem?.servers || + [], + ) + + const operationPath = basePath + path + const operationId = slugify(`${removeFirstSlash(operationPath)}-${httpMethod}`) + return operations.includes(operationId) + }) + + if (operationIds?.length) { + const pathItem = { + ...extractCommonPathItemProperties(valueByPath), + ...operationIds.reduce((pathItemObject: OpenAPIV3.PathItemObject, operationId: OpenAPIV3.HttpMethods) => { + const operationData = valueByPath[operationId] + if (operationData) { + pathItemObject[operationId] = { ...operationData } + } + return pathItemObject + }, {}), } + setValueByPath(sourceDocument, richReference.jsonPath, pathItem) } } } From 66af9d83708c08e71add9042223bade3bea3d68c Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 15 Sep 2025 14:44:47 +0400 Subject: [PATCH 5/6] feat: Cleaning --- src/apitypes/rest/rest.operation.ts | 17 ++++++++++++----- src/apitypes/rest/rest.operations.ts | 6 ++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/apitypes/rest/rest.operation.ts b/src/apitypes/rest/rest.operation.ts index 3859e10..3e7ff46 100644 --- a/src/apitypes/rest/rest.operation.ts +++ b/src/apitypes/rest/rest.operation.ts @@ -283,14 +283,12 @@ export function createSinglePathItemOperationSpec( sourcePathItem?.servers || [], ) - - const operationPath = basePath + path - const operationId = slugify(`${removeFirstSlash(operationPath)}-${httpMethod}`) + const operationId = getOperationId(basePath, httpMethod, path) return operations.includes(operationId) }) if (operationIds?.length) { - const pathItem = { + const pathItemObject = { ...extractCommonPathItemProperties(valueByPath), ...operationIds.reduce((pathItemObject: OpenAPIV3.PathItemObject, operationId: OpenAPIV3.HttpMethods) => { const operationData = valueByPath[operationId] @@ -300,7 +298,7 @@ export function createSinglePathItemOperationSpec( return pathItemObject }, {}), } - setValueByPath(sourceDocument, richReference.jsonPath, pathItem) + setValueByPath(sourceDocument, richReference.jsonPath, pathItemObject) } } } @@ -374,3 +372,12 @@ export const extractCommonPathItemProperties = ( function isValidHttpMethod(method: string): method is OpenAPIV3.HttpMethods { return (Object.values(OpenAPIV3.HttpMethods) as string[]).includes(method) } + +export function getOperationId( + basePath: string, + key: string, + path: string, +): string { + const operationPath = basePath + path + return slugify(`${removeFirstSlash(operationPath)}-${key}`) +} diff --git a/src/apitypes/rest/rest.operations.ts b/src/apitypes/rest/rest.operations.ts index c8a2af5..1ba81e3 100644 --- a/src/apitypes/rest/rest.operations.ts +++ b/src/apitypes/rest/rest.operations.ts @@ -16,13 +16,12 @@ import { OpenAPIV3 } from 'openapi-types' -import { buildRestOperation } from './rest.operation' +import { buildRestOperation, getOperationId } from './rest.operation' import { OperationIdNormalizer, OperationsBuilder } from '../../types' import { createBundlingErrorHandler, IGNORE_PATH_PARAM_UNIFIED_PLACEHOLDER, removeComponents, - removeFirstSlash, slugify, } from '../../utils' import { getOperationBasePath } from './rest.utils' @@ -77,9 +76,8 @@ export const buildRestOperations: OperationsBuilder = async await asyncFunction(() => { const methodData = pathData[key as OpenAPIV3.HttpMethods] const basePath = getOperationBasePath(methodData?.servers || pathData?.servers || servers || []) - const operationPath = basePath + path - const operationId = slugify(`${removeFirstSlash(operationPath)}-${key}`) + const operationId = getOperationId(basePath, key, path) if (ctx.operationResolver(operationId)) { ctx.notifications.push({ From f38895a03d002ff34f0bf1e20474f588562ea30c Mon Sep 17 00:00:00 2001 From: Pavel Makeev Date: Mon, 15 Sep 2025 14:58:07 +0400 Subject: [PATCH 6/6] feat: Cleaning --- src/apitypes/rest/rest.operation.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/apitypes/rest/rest.operation.ts b/src/apitypes/rest/rest.operation.ts index 3e7ff46..a160a43 100644 --- a/src/apitypes/rest/rest.operation.ts +++ b/src/apitypes/rest/rest.operation.ts @@ -247,11 +247,11 @@ export const calculateSpecRefs = ( }) if (operations?.length) { - createSinglePathItemOperationSpec(resultSpec, normalizedSpec, operations) + resolveComponentsPathItemOperationSpec(resultSpec, normalizedSpec, operations) } } -export function createSinglePathItemOperationSpec( +export function resolveComponentsPathItemOperationSpec( sourceDocument: RestOperationData, normalizedDocument: RestOperationData, operations: OperationId[], @@ -267,11 +267,12 @@ export function createSinglePathItemOperationSpec( if (refs.length === 0) { continue } - const richReference = parseRef(refs[0]) - if (!richReference) { + const { jsonPath } = parseRef(refs[0]) + if (!jsonPath) { continue } - const valueByPath = getValueByPath(sourceDocument, richReference.jsonPath) as OpenAPIV3.PathItemObject + + const valueByPath = getValueByPath(sourceDocument, jsonPath) as OpenAPIV3.PathItemObject const operationIds: OpenAPIV3.HttpMethods[] = (Object.keys(valueByPath) as OpenAPIV3.HttpMethods[]) .filter((httpMethod) => isValidHttpMethod(httpMethod)) @@ -298,7 +299,7 @@ export function createSinglePathItemOperationSpec( return pathItemObject }, {}), } - setValueByPath(sourceDocument, richReference.jsonPath, pathItemObject) + setValueByPath(sourceDocument, jsonPath, pathItemObject) } } }