Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f9ffb67
feat: Typing
makeev-pavel Dec 15, 2025
54b2e0c
Merge branch 'feature/asyncapi-basic-e2e' into feature/asyncapi-basic…
makeev-pavel Dec 19, 2025
cbd3361
feat: Fixed some types
makeev-pavel Dec 22, 2025
d892759
Merge branch 'feature/asyncapi-basic-e2e' into feature/asyncapi-basic…
makeev-pavel Jan 12, 2026
a0fe4a8
feat: Added tests
makeev-pavel Jan 15, 2026
2b926d3
feat: Added tests
makeev-pavel Jan 15, 2026
21d6aef
feat: More tests
makeev-pavel Jan 15, 2026
118105b
feat: Added async operation tests
makeev-pavel Jan 15, 2026
e671b40
feat: Added async declarative tests
makeev-pavel Jan 16, 2026
cabab56
Merge branch 'feature/asyncapi-basic-e2e' into feature/asyncapi-basic…
makeev-pavel Jan 23, 2026
69f3a43
feat: Update
makeev-pavel Jan 30, 2026
70c6659
feat: Update
makeev-pavel Feb 2, 2026
75e6858
feat: Update calculate ExternalDocumentation and Tags
makeev-pavel Feb 2, 2026
b8cb25f
feat: Update operations and deprecated
makeev-pavel Feb 4, 2026
cf52d63
feat: Update deprecated
makeev-pavel Feb 5, 2026
86b45b4
feat: Update deprecated, apiKind, protocol
makeev-pavel Feb 6, 2026
1e0465f
feat: Add tests for apikind
makeev-pavel Feb 6, 2026
2af1824
feat: Update tests for asyncapi protocol
makeev-pavel Feb 6, 2026
d6716a3
feat: Update
makeev-pavel Feb 6, 2026
ff27d3c
feat: Update createOperationSpec
makeev-pavel Feb 9, 2026
7421f2a
feat: Update tests
makeev-pavel Feb 9, 2026
b17407b
feat: Update createOperationSpec
makeev-pavel Feb 10, 2026
d4effc8
feat: Update createOperationSpec tests
makeev-pavel Feb 11, 2026
1912011
feat: Update createOperationSpec tests
makeev-pavel Feb 11, 2026
f5a3e96
feat: Refactoring
makeev-pavel Feb 12, 2026
6bd97e2
feat: Refactoring
makeev-pavel Feb 12, 2026
4c28a51
feat: Refactoring tests
makeev-pavel Feb 12, 2026
f69a1a6
feat: Refactoring
makeev-pavel Feb 12, 2026
3333dd2
feat: Added tests for asyncapi security
makeev-pavel Feb 13, 2026
9de15bc
feat: refactoring
makeev-pavel Feb 16, 2026
976fda1
feat: Refactoring
makeev-pavel Feb 16, 2026
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
49 changes: 25 additions & 24 deletions src/apitypes/async/async.changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
* limitations under the License.
*/

import { AsyncApiDocument } from './async.types'
import { isEmpty, SLUG_OPTIONS_OPERATION_ID, slugify } from '../../utils'
import { isEmpty, isObject, SLUG_OPTIONS_OPERATION_ID, slugify } from '../../utils'
import {
aggregateDiffsWithRollup,
apiDiff,
Expand Down Expand Up @@ -46,6 +45,7 @@ import {
getOperationTags,
OperationsMap,
} from '../../components'
import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types'

export const compareDocuments: DocumentsCompare = async (
operationsMap: OperationsMap,
Expand All @@ -67,8 +67,8 @@ export const compareDocuments: DocumentsCompare = async (
const comparisonInternalDocumentId = createComparisonInternalDocumentId(prevDoc, currDoc, previousVersion, currentVersion)
const prevFile = prevDoc && await rawDocumentResolver(previousVersion, previousPackageId, prevDoc.slug)
const currFile = currDoc && await rawDocumentResolver(currentVersion, currentPackageId, currDoc.slug)
let prevDocData = prevFile && JSON.parse(await prevFile.text()) as AsyncApiDocument
let currDocData = currFile && JSON.parse(await currFile.text()) as AsyncApiDocument
let prevDocData = prevFile && JSON.parse(await prevFile.text()) as AsyncAPIV3.AsyncAPIObject
let currDocData = currFile && JSON.parse(await currFile.text()) as AsyncAPIV3.AsyncAPIObject

// Create an empty counterpart of the document for the case when one of the documents is empty
if (!prevDocData && currDocData) {
Expand All @@ -89,7 +89,7 @@ export const compareDocuments: DocumentsCompare = async (
afterValueNormalizedProperty: AFTER_VALUE_NORMALIZED_PROPERTY,
beforeValueNormalizedProperty: BEFORE_VALUE_NORMALIZED_PROPERTY,
},
) as { merged: AsyncApiDocument; diffs: Diff[] }
) as { merged: AsyncAPIV3.AsyncAPIObject; diffs: Diff[] }

if (isEmpty(diffs)) {
return { operationChanges: [], tags: new Set() }
Expand All @@ -101,29 +101,26 @@ export const compareDocuments: DocumentsCompare = async (
const operationChanges: OperationChanges[] = []

// Iterate through operations in merged document
if (merged.operations && typeof merged.operations === 'object') {
for (const [operationKey, operationData] of Object.entries(merged.operations)) {
if (!operationData || typeof operationData !== 'object') {
const { operations } = merged
if (operations && isObject(operations)) {
for (const [operationKey, operationData] of Object.entries(operations)) {
if (!operationData || !isObject(operationData)) {
continue
}

const operationObject = operationData as AsyncAPIV3.OperationObject
// Extract action and channel from operation
const action = (operationData as any).action as 'send' | 'receive' //TODO: fix type
const channelRef = (operationData as any).channel

if (!action || !channelRef) {
const { action, channel: operationChannel } = operationObject
if (!action || !operationChannel) {
continue
}

// Extract channel name from reference
const channel = typeof channelRef === 'string' && channelRef.startsWith('#/channels/')
? channelRef.split('/').pop() || operationKey
: operationKey

// Use simple operation ID (no normalization needed for AsyncAPI)
const operationId = slugify(`${action}-${channel}`, SLUG_OPTIONS_OPERATION_ID)
const operationId = slugify(`${action}-${operationKey}`, SLUG_OPTIONS_OPERATION_ID)

const { current, previous } = operationsMap[operationId] ?? {}
const {
current,
previous ,
} = operationsMap[operationId] ?? {}
if (!current && !previous) {
throw new Error(`Can't find the ${operationId} operation from documents pair ${prevDoc?.fileId} and ${currDoc?.fileId}`)
}
Expand All @@ -134,11 +131,15 @@ export const compareDocuments: DocumentsCompare = async (
let operationDiffs: Diff[] = []
if (operationPotentiallyChanged) {
operationDiffs = [
...(operationData as WithAggregatedDiffs<any>)[DIFFS_AGGREGATED_META_KEY] ?? [],
...(operationObject as WithAggregatedDiffs<AsyncAPIV3.OperationObject>)[DIFFS_AGGREGATED_META_KEY] ?? [],
// TODO: check
// ...extractAsyncApiVersionDiff(merged),
// ...extractRootServersDiffs(merged),
// ...extractChannelsDiffs(merged, operationChannel),
]
}
if (operationAddedOrRemoved) {
const operationAddedOrRemovedDiff = (merged.operations as WithDiffMetaRecord<Record<string, any>>)[DIFF_META_KEY]?.[operationKey]
const operationAddedOrRemovedDiff = (operations as WithDiffMetaRecord<AsyncAPIV3.OperationsObject>)[DIFF_META_KEY]?.[operationKey]
if (operationAddedOrRemovedDiff) {
operationDiffs.push(operationAddedOrRemovedDiff)
}
Expand Down Expand Up @@ -171,12 +172,12 @@ export const compareDocuments: DocumentsCompare = async (
* Creates a copy of the AsyncAPI document with empty operations
* Used for comparison when one document doesn't exist
*/
function createCopyWithEmptyOperations(template: AsyncApiDocument): AsyncApiDocument {
function createCopyWithEmptyOperations(template: AsyncAPIV3.AsyncAPIObject): AsyncAPIV3.AsyncAPIObject {
const { operations, ...rest } = template

return {
operations: operations ? Object.fromEntries(
Object.keys(operations).map(key => [key, {}]),
Object.keys(operations).map(key => [key, {} as AsyncAPIV3.OperationObject]),
) : {},
...rest,
}
Expand Down
4 changes: 4 additions & 0 deletions src/apitypes/async/async.consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ export const ASYNC_EFFECTIVE_NORMALIZE_OPTIONS: NormalizeOptions = {
originsFlag: ORIGINS_SYMBOL,
hashFlag: HASH_FLAG,
}

export const ASYNCAPI_DEPRECATION_EXTENSION_KEY = 'x-deprecated'
// todo move to unifier
export const DEPRECATED_MESSAGE_PREFIX = '[Deprecated]'
112 changes: 89 additions & 23 deletions src/apitypes/async/async.document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,59 +14,70 @@
* limitations under the License.
*/

import { ASYNC_KIND_KEY } from './async.consts'
import type { AsyncDocumentInfo, AsyncApiDocument } from './async.types'
import {
_TemplateResolver,
DocumentBuilder,
DocumentDumper,
ExportDocument,
ExportFormat,
VersionDocument,
} from '../../types'
import { FILE_FORMAT } from '../../consts'
import { FILE_FORMAT, FILE_FORMAT_HTML } from '../../consts'
import {
createBundlingErrorHandler,
createVersionInternalDocument,
EXPORT_FORMAT_TO_FILE_FORMAT,
getBundledFileDataWithDependencies,
getDocumentTitle,
getDocumentTitle, getStringValue,
isObject,
} from '../../utils'
import { dump } from '../../utils/apihubSpecificationExtensions'
import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types'
import { AsyncDocumentInfo } from './async.types'
import { OpenApiExtensionKey } from '@netcracker/qubership-apihub-api-unifier'
import { removeOasExtensions } from '../../utils/removeOasExtensions'
import { generateHtmlPage } from '../../utils/export'
import { toExternalDocumentationObject, toTagObjects } from './async.utils'

const asyncApiDocumentMeta = (data: AsyncApiDocument): AsyncDocumentInfo => {
if (typeof data !== 'object' || !data) {
return { title: '', description: '', version: '' }
const asyncApiDocumentMeta = (data: AsyncAPIV3.AsyncAPIObject): AsyncDocumentInfo => {
if (!isObject(data)) {
return { title: '', description: '', version: '', tags: [] }
}

const { title = '', version = '', description = '' } = data?.info || {}

const getStringValue = (value: unknown): string => (typeof (<unknown>value) === 'string' ? <string>value : '')
const { title = '', version = '', description = '', externalDocs: _externalDocs, tags: _tags, ...restInfo } = data.info || {}

return {
title: getStringValue(title),
description: getStringValue(description),
version: getStringValue(version),
info: Object.keys(restInfo).length ? restInfo : undefined,
externalDocs: toExternalDocumentationObject(data),
tags: toTagObjects(data),
}
}

export const buildAsyncApiDocument: DocumentBuilder<AsyncApiDocument> = async (parsedFile, file, ctx) => {
const { fileId, slug = '', publish = true, apiKind, ...fileMetadata } = file
export const buildAsyncApiDocument: DocumentBuilder<AsyncAPIV3.AsyncAPIObject> = async (parsedFile, file, ctx): Promise<VersionDocument> => {
const { fileId, slug = '', publish = true, ...fileMetadata } = file

const {
data,
dependencies,
} = await getBundledFileDataWithDependencies(fileId, ctx.parsedFileResolver, createBundlingErrorHandler(ctx, fileId))

const bundledFileData = data as AsyncApiDocument

const documentKind = bundledFileData?.info?.[ASYNC_KIND_KEY] || apiKind
const bundledFileData = data as AsyncAPIV3.AsyncAPIObject

const { description, title, version } = asyncApiDocumentMeta(bundledFileData)
const { description, title, version, info, externalDocs, tags } = asyncApiDocumentMeta(bundledFileData)
const metadata = {
...fileMetadata,
info,
externalDocs,
tags,
}

const { type, fileId: parsedFileId, source, errors } = parsedFile
return {
fileId: parsedFile.fileId,
type: parsedFile.type,
fileId: parsedFileId,
type,
format: FILE_FORMAT.JSON,
apiKind: documentKind,
data: bundledFileData,
slug, // unique slug should be already generated
filename: `${slug}.${FILE_FORMAT.JSON}`,
Expand All @@ -77,13 +88,68 @@ export const buildAsyncApiDocument: DocumentBuilder<AsyncApiDocument> = async (p
version,
metadata,
publish,
source: parsedFile.source,
errors: parsedFile.errors?.length ?? 0,
source,
errors: errors?.length ?? 0,
versionInternalDocument: createVersionInternalDocument(slug),
}
}

export const dumpAsyncApiDocument: DocumentDumper<AsyncApiDocument> = (document, format) => {
export const dumpAsyncApiDocument: DocumentDumper<AsyncAPIV3.AsyncAPIObject> = (document, format) => {
return new Blob(...dump(document.data, format ?? FILE_FORMAT.JSON))
}

/**
* Creates an export document from AsyncAPI data.
*
* Note: When `format` is HTML, the resulting document is also pushed into
* `generatedHtmlExportDocuments` (if provided) as a side effect.
*/
export async function createAsyncExportDocument(
filename: string,
data: string,
format: ExportFormat,
packageName: string,
version: string,
templateResolver: _TemplateResolver,
allowedOasExtensions?: OpenApiExtensionKey[],
generatedHtmlExportDocuments?: ExportDocument[],
): Promise<ExportDocument> {
const exportFilename = `${getDocumentTitle(filename)}.${format}`

let parsed: object
try {
parsed = JSON.parse(data)
} catch (e) {
throw new Error(`Failed to parse document '${filename}': ${(e as Error).message}`)
}

const fileFormat = EXPORT_FORMAT_TO_FILE_FORMAT.get(format)
if (!fileFormat) {
throw new Error(`Unsupported export format: ${format}`)
}

const [[document], blobProperties] = dump(
removeOasExtensions(parsed as Parameters<typeof removeOasExtensions>[0], allowedOasExtensions),
fileFormat,
)

if (format === FILE_FORMAT_HTML) {
const htmlExportDocument = {
data: await generateHtmlPage(
document,
getDocumentTitle(filename),
packageName,
version,
templateResolver,
),
filename: exportFilename,
}
generatedHtmlExportDocuments?.push(htmlExportDocument)
return htmlExportDocument
}

return {
data: new Blob([document], blobProperties),
filename: exportFilename,
}
}
Loading