Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 117 additions & 87 deletions src/apitypes/rest/rest.operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,41 @@ 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 {
buildSearchScope,
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,
grepValue,
JSON_SCHEMA_PROPERTY_DEPRECATED,
matchPaths,
OPEN_API_PROPERTY_COMPONENTS,
OPEN_API_PROPERTY_PATH_ITEMS,
OPEN_API_PROPERTY_PATHS,
OPEN_API_PROPERTY_SCHEMAS,
parseRef,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -182,7 +186,14 @@ export const buildRestOperation = (
}
}

export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unknown, resultSpec: unknown, models?: Record<string, string>, componentsHashMap?: Map<string, string>): void => {
export const calculateSpecRefs = (
sourceDocument: TYPE.RestOperationData,
normalizedSpec: TYPE.RestOperationData,
resultSpec: TYPE.RestOperationData,
operations: OperationId[],
models?: Record<string, string>,
componentsHashMap?: Map<string, string>,
): void => {
const handledObjects = new Set<unknown>()
const inlineRefs = new Set<string>()
syncCrawl(
Expand Down Expand Up @@ -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<string, unknown>
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<string>(Object.values(OpenAPIV3.HttpMethods) as string[])
const filteredSource: Record<string, unknown> = { ...(source as Record<string, unknown>) }
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<OpenAPIV3.PathItemObject>((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<string, unknown> {
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<string>(Object.values(OpenAPIV3.HttpMethods) as string[])
return Object.keys(resultComponents as Record<string, unknown>).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 => {
Expand All @@ -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
Expand All @@ -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 = (
Expand All @@ -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}`)
}
6 changes: 2 additions & 4 deletions src/apitypes/rest/rest.operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -77,9 +76,8 @@ export const buildRestOperations: OperationsBuilder<OpenAPIV3.Document> = 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({
Expand Down
Loading
Loading