diff --git a/src/apitypes/rest/rest.operation.ts b/src/apitypes/rest/rest.operation.ts index 3f9b0b4..a160a43 100644 --- a/src/apitypes/rest/rest.operation.ts +++ b/src/apitypes/rest/rest.operation.ts @@ -19,12 +19,14 @@ 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 { RestOperationData } from './rest.types' +import { BuildConfig, CrawlRule, DeprecateItem, NotificationMessage, OperationCrawlState, + OperationId, SearchScopes, } from '../../types' import { @@ -32,17 +34,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 +54,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 +64,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 +149,7 @@ export const buildRestOperation = ( security, components?.securitySchemes, ) - calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, models, componentsHashMap) + calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, [operationId], models, componentsHashMap) const dataHash = calculateObjectHash(specWithSingleOperation) return [specWithSingleOperation, dataHash] }, debugCtx) @@ -182,7 +186,14 @@ export const buildRestOperation = ( } } -export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unknown, resultSpec: unknown, 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( @@ -214,67 +225,94 @@ 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 = getKeyValue(sourceDocument, ...matchResult.path) as Record + 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]], - ) + if (operations?.length) { + resolveComponentsPathItemOperationSpec(resultSpec, normalizedSpec, operations) + } } -export const filterPathItemOperations = ( - source: unknown, - allowedMethods: string[], -): unknown => { - const httpMethods = new Set(Object.values(OpenAPIV3.HttpMethods) as string[]) - const filteredSource: Record = { ...(source as Record) } +export function resolveComponentsPathItemOperationSpec( + sourceDocument: RestOperationData, + normalizedDocument: RestOperationData, + operations: OperationId[], +): void { + const { paths } = normalizedDocument - for (const key of Object.keys(filteredSource)) { - if (httpMethods.has(key) && !allowedMethods.includes(key)) { - delete filteredSource[key] + for (const path of Object.keys(paths)) { + const sourcePathItem = paths[path] as OpenAPIV3.PathItemObject + if (!isNonNullObject(sourcePathItem)) { + continue + } + const refs: string[] = hasInlineRefsFlag(sourcePathItem) ? sourcePathItem[INLINE_REFS_FLAG] : [] + if (refs.length === 0) { + continue + } + const { jsonPath } = parseRef(refs[0]) + if (!jsonPath) { + continue + } + + const valueByPath = getValueByPath(sourceDocument, 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 operationId = getOperationId(basePath, httpMethod, path) + return operations.includes(operationId) + }) + + if (operationIds?.length) { + const pathItemObject = { + ...extractCommonPathItemProperties(valueByPath), + ...operationIds.reduce((pathItemObject: OpenAPIV3.PathItemObject, operationId: OpenAPIV3.HttpMethods) => { + const operationData = valueByPath[operationId] + if (operationData) { + pathItemObject[operationId] = { ...operationData } + } + return pathItemObject + }, {}), + } + setValueByPath(sourceDocument, jsonPath, pathItemObject) } } +} - 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 => { @@ -284,6 +322,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 @@ -296,51 +338,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 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]: isContainsRef + ? 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 +370,15 @@ export const extractCommonPathItemProperties = ( ...takeIfDefined({ parameters: pathData?.parameters }), }) +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({ diff --git a/src/strategies/document-group.strategy.ts b/src/strategies/document-group.strategy.ts index 3381c4d..925f246 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' @@ -49,7 +48,7 @@ function getTransformedDocument(document: ResolvedGroupDocument, format: FileFor const normalizedDocument = normalizeOpenApi(versionDocument.data, sourceDocument) versionDocument.publish = true - calculateSpecRefs(sourceDocument, normalizedDocument, versionDocument.data) + calculateSpecRefs(sourceDocument, normalizedDocument, versionDocument.data, versionDocument.operationIds) // dashboard case if (document.packageRef) { @@ -128,7 +127,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 +148,19 @@ 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 isContainsRef = !!pathData.$ref + resultDocument.paths[path] = isContainsRef + ? pathData + : { + ...resultDocument.paths[path], + ...commonPathItemProperties, + [httpMethod]: { ...pathData[httpMethod] }, + } - if (sourceComponents?.securitySchemes) { resultDocument.components = { - ...resultDocument.components, - securitySchemes: sourceComponents.securitySchemes, + ...takeIfDefined({ securitySchemes: sourceComponents?.securitySchemes }), } } } @@ -180,48 +169,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