Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1f9b49e
feat: Added pathItems
makeev-pavel Aug 20, 2025
050f7ae
feat: Added work with '$ref' in rest operation
makeev-pavel Aug 22, 2025
5661ae0
feat: Fixed merge pathItems operations
makeev-pavel Aug 25, 2025
df04f59
feat: Added tests
makeev-pavel Aug 25, 2025
37369ef
feat: refactoring
makeev-pavel Aug 26, 2025
63690cf
Merge branch 'develop' into feature/pathItems
makeev-pavel Aug 26, 2025
0c2cb21
feat: Added tests and review
makeev-pavel Aug 29, 2025
45225cf
feat: Added tests and review
makeev-pavel Sep 1, 2025
e3cbfa9
Merge branch 'develop' into feature/pathItems
makeev-pavel Sep 1, 2025
1caef30
feat: Refactoring
makeev-pavel Sep 3, 2025
30097c2
feat: Refactoring
makeev-pavel Sep 5, 2025
700ba7e
feat: Update tests
makeev-pavel Sep 5, 2025
225d22a
feat: Update dep
makeev-pavel Sep 5, 2025
bd3ad86
feat: Refactoring
makeev-pavel Sep 8, 2025
aa6f46a
feat: Added hash tests
makeev-pavel Sep 9, 2025
26cc1fd
feat: Fixed servers prefix for operations and added tests
makeev-pavel Sep 10, 2025
136d5ce
feat: Refactoring
makeev-pavel Sep 10, 2025
ffdc348
feat: Refactoring
makeev-pavel Sep 12, 2025
11f601d
feat: Refactoring
makeev-pavel Sep 12, 2025
793e0bc
feat: POC ref pathItems support
makeev-pavel Sep 12, 2025
c00bce3
feat: refactoring
makeev-pavel Sep 15, 2025
6203ae9
feat: typing
makeev-pavel Sep 15, 2025
665fec8
feat: Added cleaning PathItemObject in components
makeev-pavel Sep 15, 2025
66af9d8
feat: Cleaning
makeev-pavel Sep 15, 2025
f38895a
feat: Cleaning
makeev-pavel Sep 15, 2025
9dacc6a
Merge pull request #40 from Netcracker/feature/pathItems-test
makeev-pavel Sep 15, 2025
0a44196
feat: Update tests
makeev-pavel Sep 16, 2025
14ad3fa
feat: Update tests
makeev-pavel Sep 16, 2025
c4db313
Merge remote-tracking branch 'origin/feature/pathItems' into feature/…
makeev-pavel Sep 16, 2025
5b1b4e5
feat: Review
makeev-pavel Sep 17, 2025
21672c6
feat: Cleaning
makeev-pavel Sep 17, 2025
1d5b5d5
Merge branch 'develop' into feature/pathItems
makeev-pavel Sep 17, 2025
cacf54c
feat: Added info in yaml tests
makeev-pavel Sep 17, 2025
2d5d97f
feat: Fix linter
makeev-pavel Sep 17, 2025
d4b058e
feat: Linter
makeev-pavel Sep 17, 2025
9bf11d7
feat: Linter
makeev-pavel Sep 17, 2025
bbefe7e
feat: Review
makeev-pavel Sep 18, 2025
1ae91d7
feat: Review
makeev-pavel Sep 18, 2025
1c4e293
feat: Review
makeev-pavel Sep 18, 2025
a12e958
feat: Linter
makeev-pavel Sep 18, 2025
cfeeb6d
Merge branch 'develop' into feature/pathItems
makeev-pavel Sep 18, 2025
7f67976
feat: Review
makeev-pavel Sep 19, 2025
c70196e
feat: Review
makeev-pavel Sep 19, 2025
0f1f89d
feat: Review
makeev-pavel Sep 19, 2025
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"update-lock-file": "update-lock-file @netcracker"
},
"dependencies": {
"@netcracker/qubership-apihub-api-diff": "2.2.0",
"@netcracker/qubership-apihub-api-unifier": "2.3.0",
"@netcracker/qubership-apihub-api-diff": "dev",
"@netcracker/qubership-apihub-api-unifier": "dev",
"@netcracker/qubership-apihub-graphapi": "1.0.8",
"@netcracker/qubership-apihub-json-crawl": "1.0.4",
"adm-zip": "0.5.10",
Expand Down
99 changes: 85 additions & 14 deletions src/apitypes/rest/rest.operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
isOperationDeprecated,
normalizePath,
rawToApiKind,
resolveRefAndMap,
setValueByPath,
takeIf,
takeIfDefined,
Expand All @@ -49,6 +50,7 @@ import {
JSON_SCHEMA_PROPERTY_DEPRECATED,
matchPaths,
OPEN_API_PROPERTY_COMPONENTS,
OPEN_API_PROPERTY_PATH_ITEMS,
OPEN_API_PROPERTY_PATHS,
OPEN_API_PROPERTY_SCHEMAS,
parseRef,
Expand Down Expand Up @@ -212,21 +214,28 @@ export const calculateSpecRefs = (sourceDocument: unknown, normalizedSpec: unkno
return
}
const componentName = matchResult.grepValues[grepKey].toString()
const component = getKeyValue(sourceDocument, ...matchResult.path)
if (!component) {
let sourceComponents = getKeyValue(sourceDocument, ...matchResult.path)
if (!sourceComponents || typeof sourceComponents !== 'object') {
return
}

if (typeof sourceComponents === 'object') {
const allowedOps = getAllowedHttpOps(resultSpec, matchResult.path)
if (allowedOps.length > 0 && isComponentsPathItemRef(matchResult.path)) {
sourceComponents = filterPathItemOperations(sourceComponents, allowedOps)
}
}
if (models && !models[componentName] && isComponentsSchemaRef(matchResult.path)) {
let componentHash = componentsHashMap?.get(componentName)
if (componentHash) {
models[componentName] = componentHash
const existingHash = componentsHashMap?.get(componentName)
if (existingHash) {
models[componentName] = existingHash
} else {
componentHash = calculateObjectHash(component)
componentsHashMap?.set(componentName, componentHash)
models[componentName] = componentHash
const componentHashCalculated = calculateObjectHash(sourceComponents as object)
componentsHashMap?.set(componentName, componentHashCalculated)
models[componentName] = componentHashCalculated
}
}
setValueByPath(resultSpec, matchResult.path, component)
setValueByPath(resultSpec, matchResult.path, sourceComponents)
})
}

Expand All @@ -236,6 +245,38 @@ export const isComponentsSchemaRef = (path: JsonPath): boolean => {
[[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 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>) }

for (const key of Object.keys(filteredSource)) {
if (httpMethods.has(key) && !allowedMethods.includes(key)) {
delete filteredSource[key]
}
}

return filteredSource
}

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))
}

const isOperationPaths = (paths: JsonPath[]): boolean => {
return !!matchPaths(
paths,
Expand All @@ -255,21 +296,50 @@ const createSingleOperationSpec = (
security?: OpenAPIV3.SecurityRequirementObject[],
securitySchemes?: { [p: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.SecuritySchemeObject },
): TYPE.RestOperationData => {
const pathData = document.paths[path] as OpenAPIV3.PathItemObject
const pathData = document.paths[path] as OpenAPIV3.PathItemObject | undefined
if (!pathData) {
throw new Error(`Path "${path}" not found in the document`)
}

return {
const baseSpec = {
openapi: openapi ?? '3.0.0',
...takeIfDefined({ servers }),
...takeIfDefined({ security }), // TODO: remove duplicates in security
components: {
...takeIfDefined({ securitySchemes }),
},
}

if (pathData.$ref) {
const cleanedDocument = resolveRefAndMap(
document,
pathData.$ref,
(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] },
},
},
components: {
...takeIfDefined({ securitySchemes }),
},
}
}

Expand All @@ -281,3 +351,4 @@ export const extractCommonPathItemProperties = (
...takeIfDefined({ servers: pathData?.servers }),
...takeIfDefined({ parameters: pathData?.parameters }),
})

2 changes: 1 addition & 1 deletion src/apitypes/rest/rest.operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const buildRestOperations: OperationsBuilder<OpenAPIV3.Document> = async
debugCtx,
)

const { paths, servers } = document.data
const { paths, servers } = effectiveDocument

const operations: TYPE.VersionRestOperation[] = []
if (!paths) { return [] }
Expand Down
159 changes: 121 additions & 38 deletions src/strategies/document-group.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { REST_API_TYPE } from '../apitypes'
import {
EXPORT_FORMAT_TO_FILE_FORMAT,
fromBase64,
getParentValueByRef,
resolveRefAndMap,
removeFirstSlash,
slugify,
takeIfDefined,
Expand All @@ -43,19 +45,12 @@ import { calculateSpecRefs, extractCommonPathItemProperties } from '../apitypes/
function getTransformedDocument(document: ResolvedGroupDocument, format: FileFormat, packages: ResolvedReferenceMap): VersionRestDocument {
const versionDocument = toVersionDocument(document, format)

const source = extractDocumentData(versionDocument)
const sourceDocument = extractDocumentData(versionDocument)
versionDocument.data = transformDocumentData(versionDocument)
const normalizedDocument = normalize(
versionDocument.data,
{
...NORMALIZE_OPTIONS,
inlineRefsFlag: INLINE_REFS_FLAG,
source,
},
) as OpenAPIV3.Document
const normalizedDocument = normalizeOpenApi(versionDocument.data, sourceDocument)
versionDocument.publish = true

calculateSpecRefs(source, normalizedDocument, versionDocument.data)
calculateSpecRefs(sourceDocument, normalizedDocument, versionDocument.data)

// dashboard case
if (document.packageRef) {
Expand Down Expand Up @@ -103,7 +98,7 @@ export class DocumentGroupStrategy implements BuilderStrategy {
}
}

function parseBase64String(value: string): object {
function parseBase64String(value: string): unknown {
return JSON.parse(fromBase64(value))
}

Expand All @@ -116,48 +111,136 @@ function extractDocumentData(versionDocument: VersionDocument): OpenAPIV3.Docume
}

function transformDocumentData(versionDocument: VersionDocument): OpenAPIV3.Document {
const sourceDocument = extractDocumentData(versionDocument)
const normalizedDocument = normalizeOpenApi(sourceDocument)
const { paths: sourcePaths, components: sourceComponents, ...restOfSource } = sourceDocument
const { paths: normalizedPaths } = normalizedDocument

const documentData = extractDocumentData(versionDocument)
const { paths, components, ...rest } = documentData
const result: OpenAPIV3.Document = {
...rest,
const resultDocument: OpenAPIV3.Document = {
...restOfSource,
paths: {},
}

for (const path of Object.keys(paths)) {
const pathData = paths[path]
if (typeof pathData !== 'object' || !pathData) {
for (const path of Object.keys(normalizedPaths)) {
const sourcePathItem = sourcePaths[path]
const normalizedPathItem = normalizedPaths[path]

if (!isNonNullObject(sourcePathItem) || !isNonNullObject(normalizedPathItem)) {
continue
}
const commonPathItemProperties = extractCommonPathItemProperties(pathData)

for (const method of Object.keys(pathData)) {
const inferredMethod = method as OpenAPIV3.HttpMethods
const commonProps = extractCommonPathItemProperties(sourcePathItem)

// check if field is a valid openapi http method defined in OpenAPIV3.HttpMethods
if (!Object.values(OpenAPIV3.HttpMethods).includes(inferredMethod)) {
continue
}
for (const method of Object.keys(normalizedPathItem)) {
const httpMethod = method as OpenAPIV3.HttpMethods
if (!isValidHttpMethod(httpMethod)) continue

const methodData = pathData[inferredMethod]
const basePath = getOperationBasePath(methodData?.servers || pathData?.servers || documentData?.servers || [])
const operationPath = basePath + path
const methodData = normalizedPathItem[httpMethod]
const basePath = getOperationBasePath(
methodData?.servers ||
sourcePathItem?.servers ||
sourceDocument?.servers ||
[],
)

const operationPath = basePath + path
const operationId = slugify(`${removeFirstSlash(operationPath)}-${method}`)

if (versionDocument.operationIds.includes(operationId)) {
const pathData = documentData.paths[path]!
result.paths[path] = {
...result.paths[path],
...commonPathItemProperties,
[inferredMethod]: { ...pathData[inferredMethod] },
}
result.components = {
...takeIfDefined({ securitySchemes: components?.securitySchemes }),
if (!versionDocument.operationIds.includes(operationId)) {
continue
}

const pathData = sourceDocument.paths[path]!
if (sourcePathItem?.$ref) {
handleRefPathItem(
resultDocument,
sourceDocument,
path,
pathData,
httpMethod,
)
} else {
handleInlinePathItem(
resultDocument,
path,
pathData,
httpMethod,
commonProps,
)
}

if (sourceComponents?.securitySchemes) {
resultDocument.components = {
...resultDocument.components,
securitySchemes: sourceComponents.securitySchemes,
}
}
}
}

return result
return resultDocument
}

function handleRefPathItem(
resultDoc: OpenAPIV3.Document,
sourceDoc: OpenAPIV3.Document,
path: string,
pathData: OpenAPIV3.PathItemObject | OpenAPIV3.ReferenceObject,
method: OpenAPIV3.HttpMethods,
): void {
const targetFromResultDoc = getParentValueByRef(resultDoc, (pathData as any).$ref ?? '')

const target = resolveRefAndMap(
sourceDoc,
(pathData as any).$ref ?? '',
(pathItemObject: OpenAPIV3.PathItemObject) => ({
...targetFromResultDoc,
...extractCommonPathItemProperties(pathItemObject),
[method]: { ...pathItemObject[method] },
}),
)

resultDoc.paths[path] = pathData

if (target.components) {
resultDoc.components = {
...resultDoc.components,
...target.components,
}
}
}

function handleInlinePathItem(
resultDoc: OpenAPIV3.Document,
path: string,
pathData: OpenAPIV3.PathItemObject,
method: OpenAPIV3.HttpMethods,
commonProps: Partial<OpenAPIV3.PathItemObject>,
): 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,
{
...NORMALIZE_OPTIONS,
inlineRefsFlag: INLINE_REFS_FLAG,
...(source ? { source } : {}),
},
) as OpenAPIV3.Document
}

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

function isNonNullObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
Loading
Loading