diff --git a/src/apitypes/async/async.changes.ts b/src/apitypes/async/async.changes.ts index ab8f3f11..ed53a664 100644 --- a/src/apitypes/async/async.changes.ts +++ b/src/apitypes/async/async.changes.ts @@ -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, @@ -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, @@ -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) { @@ -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() } @@ -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}`) } @@ -134,11 +131,15 @@ export const compareDocuments: DocumentsCompare = async ( let operationDiffs: Diff[] = [] if (operationPotentiallyChanged) { operationDiffs = [ - ...(operationData as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] ?? [], + ...(operationObject as WithAggregatedDiffs)[DIFFS_AGGREGATED_META_KEY] ?? [], + // TODO: check + // ...extractAsyncApiVersionDiff(merged), + // ...extractRootServersDiffs(merged), + // ...extractChannelsDiffs(merged, operationChannel), ] } if (operationAddedOrRemoved) { - const operationAddedOrRemovedDiff = (merged.operations as WithDiffMetaRecord>)[DIFF_META_KEY]?.[operationKey] + const operationAddedOrRemovedDiff = (operations as WithDiffMetaRecord)[DIFF_META_KEY]?.[operationKey] if (operationAddedOrRemovedDiff) { operationDiffs.push(operationAddedOrRemovedDiff) } @@ -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, } diff --git a/src/apitypes/async/async.consts.ts b/src/apitypes/async/async.consts.ts index cfb067a8..5b90732b 100644 --- a/src/apitypes/async/async.consts.ts +++ b/src/apitypes/async/async.consts.ts @@ -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]' diff --git a/src/apitypes/async/async.document.ts b/src/apitypes/async/async.document.ts index ce240a65..e34afa4a 100644 --- a/src/apitypes/async/async.document.ts +++ b/src/apitypes/async/async.document.ts @@ -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 (value) === '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 = async (parsedFile, file, ctx) => { - const { fileId, slug = '', publish = true, apiKind, ...fileMetadata } = file +export const buildAsyncApiDocument: DocumentBuilder = async (parsedFile, file, ctx): Promise => { + 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}`, @@ -77,13 +88,68 @@ export const buildAsyncApiDocument: DocumentBuilder = 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 = (document, format) => { +export const dumpAsyncApiDocument: DocumentDumper = (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 { + 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[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, + } +} diff --git a/src/apitypes/async/async.operation.ts b/src/apitypes/async/async.operation.ts index cb502028..23ad9fc6 100644 --- a/src/apitypes/async/async.operation.ts +++ b/src/apitypes/async/async.operation.ts @@ -16,51 +16,68 @@ import { JsonPath, syncCrawl } from '@netcracker/qubership-apihub-json-crawl' import type * as TYPE from './async.types' +import { AsyncOperationActionType, VersionAsyncOperation } from './async.types' import { BuildConfig, DeprecateItem, NotificationMessage, SearchScopes } from '../../types' import { - capitalize, + getInlineRefsFomDocument, + getKeyValue, getSplittedVersionKey, isDeprecatedOperationItem, isOperationDeprecated, + setValueByPath, takeIf, - takeIfDefined, } from '../../utils' -import { APIHUB_API_COMPATIBILITY_KIND_BWC, ORIGINS_SYMBOL, VERSION_STATUS } from '../../consts' +import { ORIGINS_SYMBOL, VERSION_STATUS } from '../../consts' import { getCustomTags, resolveApiAudience } from '../../utils/apihubSpecificationExtensions' import { DebugPerformanceContext, syncDebugPerformance } from '../../utils/logs' import { + ASYNCAPI_PROPERTY_CHANNELS, + ASYNCAPI_PROPERTY_COMPONENTS, + ASYNCAPI_PROPERTY_MESSAGES, + ASYNCAPI_PROPERTY_SERVERS, calculateDeprecatedItems, + grepValue, + Jso, JSON_SCHEMA_PROPERTY_DEPRECATED, matchPaths, - OPEN_API_PROPERTY_PATHS, + parseRef, pathItemToFullPath, PREDICATE_ANY_VALUE, + PREDICATE_UNCLOSED_END, resolveOrigins, } from '@netcracker/qubership-apihub-api-unifier' import { calculateHash, ObjectHashCache } from '../../utils/hashes' -import { extractProtocol } from './async.utils' +import { calculateAsyncApiKind, extractKeyAfterPrefix, extractProtocol } from './async.utils' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' import { getApiKindProperty } from '../../components/document' +import { calculateTolerantHash } from '../../components/deprecated' +import { ASYNCAPI_API_TYPE, ASYNCAPI_DEPRECATION_EXTENSION_KEY, DEPRECATED_MESSAGE_PREFIX } from './async.consts' export const buildAsyncApiOperation = ( operationId: string, operationKey: string, - action: 'send' | 'receive', - channel: string, + action: AsyncOperationActionType, + channel: AsyncAPIV3.ChannelObject, + message: AsyncAPIV3.MessageObject, document: TYPE.VersionAsyncDocument, - effectiveDocument: TYPE.AsyncApiDocument, - refsOnlyDocument: TYPE.AsyncApiDocument, + effectiveDocument: AsyncAPIV3.AsyncAPIObject, + refsOnlyDocument: AsyncAPIV3.AsyncAPIObject, notifications: NotificationMessage[], config: BuildConfig, normalizedSpecFragmentsHashCache: ObjectHashCache, debugCtx?: DebugPerformanceContext, -): TYPE.VersionAsyncOperation => { +): VersionAsyncOperation => { + const { + data: documentData, + slug: documentSlug, + versionInternalDocument, + metadata: documentMetadata, + } = document + const effectiveOperationObject: AsyncAPIV3.OperationObject = effectiveDocument.operations?.[operationKey] as AsyncAPIV3.OperationObject || {} + const effectiveSingleOperationSpec = createOperationSpec(effectiveDocument, operationKey) - const { apiKind: documentApiKind, servers, components } = document.data - const { versionInternalDocument } = document - const effectiveOperationObject = effectiveDocument.operations?.[operationKey] || {} - const effectiveSingleOperationSpec = createSingleOperationSpec(effectiveDocument, operationKey) - - const tags = effectiveOperationObject.tags || [] + // TODO Out of scope + const tags: string[] = effectiveOperationObject?.tags?.map(tag => (tag as AsyncAPIV3.TagObject)?.name) || [] // Extract search scopes (similar to REST) const scopes: SearchScopes = {} @@ -82,47 +99,66 @@ export const buildAsyncApiOperation = ( ) }, debugCtx) - // Calculate deprecated items const deprecatedItems: DeprecateItem[] = [] syncDebugPerformance('[DeprecatedItems]', () => { - const foundedDeprecatedItems = calculateDeprecatedItems(effectiveSingleOperationSpec, ORIGINS_SYMBOL) + const [version] = getSplittedVersionKey(config.version) + const deprecatedInPreviousVersions = config.status === VERSION_STATUS.RELEASE ? [version] : [] - for (const item of foundedDeprecatedItems) { - const { description, deprecatedReason, value } = item + const resolveDeclarationJsonPaths = (value: Jso): JsonPath[] => ( + resolveOrigins(value, ASYNCAPI_DEPRECATION_EXTENSION_KEY, ORIGINS_SYMBOL)?.map(pathItemToFullPath) ?? [] + ) - const declarationJsonPaths = resolveOrigins(value, JSON_SCHEMA_PROPERTY_DEPRECATED, ORIGINS_SYMBOL)?.map(pathItemToFullPath) ?? [] - const isOperation = isOperationPaths(declarationJsonPaths) - const [version] = getSplittedVersionKey(config.version) + if (message[ASYNCAPI_DEPRECATION_EXTENSION_KEY]) { + const declarationJsonPaths = resolveDeclarationJsonPaths(message as Jso) + const messageTitle = message.title || extractKeyAfterPrefix(declarationJsonPaths, [ASYNCAPI_PROPERTY_COMPONENTS, ASYNCAPI_PROPERTY_MESSAGES]) + + deprecatedItems.push({ + declarationJsonPaths, + description: `${DEPRECATED_MESSAGE_PREFIX} message '${messageTitle}'`, + ...{ [isOperationDeprecated]: true }, + deprecatedInPreviousVersions, + }) + } + + if (channel[ASYNCAPI_DEPRECATION_EXTENSION_KEY]) { + const declarationJsonPaths = resolveDeclarationJsonPaths(channel as Jso) + const channelTitle = channel.title || extractKeyAfterPrefix(declarationJsonPaths, [ASYNCAPI_PROPERTY_CHANNELS]) + + deprecatedItems.push({ + declarationJsonPaths, + description: `${DEPRECATED_MESSAGE_PREFIX} channel '${channelTitle}'`, + deprecatedInPreviousVersions, + hash: calculateHash(channel, normalizedSpecFragmentsHashCache), + tolerantHash: calculateTolerantHash(channel as Jso, notifications), + }) + } - const tolerantHash = undefined // Skip tolerant hash for now - const hash = isOperation ? undefined : calculateHash(value, normalizedSpecFragmentsHashCache) + const foundDeprecatedItems = calculateDeprecatedItems(effectiveSingleOperationSpec, ORIGINS_SYMBOL) + for (const item of foundDeprecatedItems) { + const { description, value } = item + const declarationJsonPaths = resolveOrigins(value, JSON_SCHEMA_PROPERTY_DEPRECATED, ORIGINS_SYMBOL)?.map(pathItemToFullPath) ?? [] deprecatedItems.push({ declarationJsonPaths, description, - ...takeIfDefined({ deprecatedInfo: deprecatedReason }), - ...takeIf({ [isOperationDeprecated]: true }, isOperation), - deprecatedInPreviousVersions: config.status === VERSION_STATUS.RELEASE ? [version] : [], - ...takeIfDefined({ hash: hash }), - ...takeIfDefined({ tolerantHash: tolerantHash }), + deprecatedInPreviousVersions, + hash: calculateHash(value, normalizedSpecFragmentsHashCache), + tolerantHash: calculateTolerantHash(value as Jso, notifications), }) } }, debugCtx) - // Extract API kind - const operationApiKind = getApiKindProperty(effectiveOperationObject) || documentApiKind || APIHUB_API_COMPATIBILITY_KIND_BWC + const operationApiKind = getApiKindProperty(effectiveOperationObject) + const channelApiKind = getApiKindProperty(channel) - // Build operation data with models + // TODO: Populate models when AsyncAPI model extraction is implemented const models: Record = {} - const [specWithSingleOperation] = syncDebugPerformance('[ModelsAndOperationHashing]', () => { - const specWithSingleOperation = createSingleOperationSpec( - document.data, + const specWithSingleOperation = syncDebugPerformance('[ModelsAndOperationHashing]', () => { + return createOperationSpec( + documentData, operationKey, - servers, - components, + refsOnlyDocument, ) - // For AsyncAPI, we could calculate spec refs similar to REST, but keeping it simple for now - return [specWithSingleOperation] }, debugCtx) const deprecatedOperationItem = deprecatedItems.find(isDeprecatedOperationItem) @@ -131,25 +167,26 @@ export const buildAsyncApiOperation = ( const customTags = getCustomTags(effectiveOperationObject) // Resolve API audience - const apiAudience = resolveApiAudience(document.metadata?.info) - - // Extract protocol from servers or channel bindings - const protocol = extractProtocol(effectiveDocument, channel) + const apiAudience = resolveApiAudience(documentMetadata?.info) + const protocol = extractProtocol(channel) + // todo Get channelId and messageId. A new api-unifier is awaiting + const channelId = 'channelId' + const messageId = 'messageId' return { operationId, - documentId: document.slug, - apiType: 'asyncapi', - apiKind: operationApiKind, - deprecated: !!effectiveOperationObject.deprecated, - title: effectiveOperationObject.title || effectiveOperationObject.summary || operationKey.split('-').map(str => capitalize(str)).join(' '), + documentId: documentSlug, + apiType: ASYNCAPI_API_TYPE, + apiKind: calculateAsyncApiKind(operationApiKind, channelApiKind), + deprecated: !!message[ASYNCAPI_DEPRECATION_EXTENSION_KEY], + title: message.title || messageId, metadata: { action, - channel, + channel: channel.title || channelId, protocol, customTags, }, - tags: Array.isArray(tags) ? tags : tags ? [tags] : [], + tags, data: specWithSingleOperation, searchScopes: scopes, deprecatedItems, @@ -163,38 +200,101 @@ export const buildAsyncApiOperation = ( } } -const isOperationPaths = (paths: JsonPath[]): boolean => { - return !!matchPaths( - paths, - [[OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE]], - ) -} - /** - * Creates a single operation spec from AsyncAPI document - * Crops the document to contain only the specific operation + * Creates an operation spec (a cropped AsyncAPI document) that contains only the requested operation(s). + * + * By default, the returned object includes only `asyncapi`, `info`, and `operations`. + * If `refsDocument` is provided and contains inline refs for all requested operations, the function + * will also inline referenced `channels`, `servers`, and `components` from the original `document`. + * + * @param document AsyncAPI 3.0 document to crop. + * @param operationKey Operation key or an array of operation keys to include. + * @param refsDocument Optional "refs-only" document used to detect inline refs that must be copied. + * @throws Error when the document has no `operations`, when no operation keys are provided, or when any + * requested operation key is missing in the document. */ -const createSingleOperationSpec = ( - document: TYPE.AsyncApiDocument, - operationKey: string, - servers?: Record, - components?: Record, +export const createOperationSpec = ( + document: AsyncAPIV3.AsyncAPIObject, + operationKey: string | string[], + refsDocument?: AsyncAPIV3.AsyncAPIObject, ): TYPE.AsyncOperationData => { - const operation = document.operations?.[operationKey] + const operations = document?.operations + if (!operations) { + throw new Error( + 'AsyncAPI document has no operations. Expected a non-empty "operations" object at document.operations.', + ) + } - if (!operation) { - throw new Error(`Operation ${operationKey} not found in document`) + const operationKeys = Array.isArray(operationKey) ? operationKey : [operationKey] + if (operationKeys.length === 0) { + throw new Error( + 'No operation keys provided. Pass a non-empty operation key string or a non-empty array of operation keys.', + ) } - return { + const missingOperationKeys: string[] = [] + const selectedOperations: Record = {} + for (const key of operationKeys) { + const operation = operations[key] as AsyncAPIV3.OperationObject | undefined + if (!operation) { + missingOperationKeys.push(key) + continue + } + selectedOperations[key] = operation + } + + if (missingOperationKeys.length > 0) { + if (!Array.isArray(operationKey) && missingOperationKeys.length === 1) { + throw new Error( + `Operation "${missingOperationKeys[0]}" not found in document.operations`, + ) + } + throw new Error( + `Operations not found in document.operations: ${missingOperationKeys.join(', ')}`, + ) + } + + const resultSpec: TYPE.AsyncOperationData = { asyncapi: document.asyncapi || '3.0.0', info: document.info, - ...takeIfDefined({ servers }), - operations: { - [operationKey]: operation, - }, - channels: document.channels, - ...takeIfDefined({ components }), + operations: selectedOperations, } -} + const refsOnlyOperations = refsDocument?.operations + if (!refsOnlyOperations) { + return resultSpec + } + + // If there are not enough operations, we will get an incorrect result. + for (const key of operationKeys) { + if (!refsOnlyOperations[key]) { + return resultSpec + } + } + const inlineRefs = getInlineRefsFomDocument(refsDocument) + + const componentNameMatcher = grepValue('componentName') + const matchPatterns = [ + [ASYNCAPI_PROPERTY_COMPONENTS, PREDICATE_ANY_VALUE, componentNameMatcher, PREDICATE_UNCLOSED_END], + [ASYNCAPI_PROPERTY_CHANNELS, componentNameMatcher, PREDICATE_UNCLOSED_END], + [ASYNCAPI_PROPERTY_SERVERS, componentNameMatcher, PREDICATE_UNCLOSED_END], + ] + + inlineRefs.forEach(ref => { + const parsed = parseRef(ref) + const path = parsed?.jsonPath + if (!path) { + return + } + const matchResult = matchPaths([path], matchPatterns) + if (!matchResult) { + return + } + const component = getKeyValue(document, ...matchResult.path) as Record + if (!component) { + return + } + setValueByPath(resultSpec, matchResult.path, component) + }) + return resultSpec +} diff --git a/src/apitypes/async/async.operations.ts b/src/apitypes/async/async.operations.ts index cf61eac2..5e5cedfe 100644 --- a/src/apitypes/async/async.operations.ts +++ b/src/apitypes/async/async.operations.ts @@ -14,115 +14,131 @@ * limitations under the License. */ -import { buildAsyncApiOperation } from './async.operation' import { OperationsBuilder } from '../../types' -import { createBundlingErrorHandler, createSerializedInternalDocument, isNotEmpty, removeComponents, SLUG_OPTIONS_OPERATION_ID, slugify } from '../../utils' +import { + calculateAsyncOperationId, + createBundlingErrorHandler, + createSerializedInternalDocument, + isNotEmpty, + isObject, + removeComponents, +} from '../../utils' import type * as TYPE from './async.types' +import { AsyncOperationActionType } from './async.types' import { INLINE_REFS_FLAG } from '../../consts' import { asyncFunction } from '../../utils/async' import { logLongBuild, syncDebugPerformance } from '../../utils/logs' import { normalize, RefErrorType } from '@netcracker/qubership-apihub-api-unifier' import { ASYNC_EFFECTIVE_NORMALIZE_OPTIONS } from './async.consts' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' +import { buildAsyncApiOperation } from './async.operation' -type OperationInfo = { channel: string; action: string } +type OperationInfo = { operationKey: string; action: string } type DuplicateEntry = { operationId: string; operations: OperationInfo[] } -export const buildAsyncApiOperations: OperationsBuilder = async (document, ctx, debugCtx) => { - const documentWithoutComponents = removeComponents(document.data) - const bundlingErrorHandler = createBundlingErrorHandler(ctx, document.fileId) +export const buildAsyncApiOperations: OperationsBuilder = async (document, ctx, debugCtx) => { + const { data: documentData, fileId: documentFileId } = document + const documentWithoutComponents = removeComponents(documentData) + const bundlingErrorHandler = createBundlingErrorHandler(ctx, documentFileId) const { notifications, normalizedSpecFragmentsHashCache, config } = ctx const { effectiveDocument, refsOnlyDocument } = syncDebugPerformance('[NormalizeDocument]', () => { - const effectiveDocument = normalize( - documentWithoutComponents, - { - ...ASYNC_EFFECTIVE_NORMALIZE_OPTIONS, - source: document.data, - onRefResolveError: (message: string, _path: PropertyKey[], _ref: string, errorType: RefErrorType) => - bundlingErrorHandler([{ message, errorType }]), - }, - ) as TYPE.AsyncApiDocument - const refsOnlyDocument = normalize( - documentWithoutComponents, - { - mergeAllOf: false, - inlineRefsFlag: INLINE_REFS_FLAG, - source: document.data, - }, - ) as TYPE.AsyncApiDocument - return { effectiveDocument, refsOnlyDocument } - }, + const effectiveDocument = normalize( + documentWithoutComponents, + { + ...ASYNC_EFFECTIVE_NORMALIZE_OPTIONS, + source: documentData, + onRefResolveError: (message: string, _path: PropertyKey[], _ref: string, errorType: RefErrorType) => + bundlingErrorHandler([{ message, errorType }]), + }, + ) as AsyncAPIV3.AsyncAPIObject + const refsOnlyDocument = normalize( + documentWithoutComponents, + { + mergeAllOf: false, + inlineRefsFlag: INLINE_REFS_FLAG, + source: documentData, + }, + ) as AsyncAPIV3.AsyncAPIObject + return { effectiveDocument, refsOnlyDocument } + }, debugCtx, ) - const { operations: operationsObj } = effectiveDocument + const { operations } = effectiveDocument - const operations: TYPE.VersionAsyncOperation[] = [] - if (!operationsObj || typeof operationsObj !== 'object') { + const apihubOperations: TYPE.VersionAsyncOperation[] = [] + if (!isObject(operations)) { return [] } const operationIdMap = new Map() // Iterate through all operations in AsyncAPI 3.0 document - for (const [operationKey, operationData] of Object.entries(operationsObj)) { - if (!operationData || typeof operationData !== 'object') { + for (const [operationKey, operationData] of Object.entries(operations)) { + if (!isObject(operationData)) { continue } + const operationObject = operationData as AsyncAPIV3.OperationObject + const messages = operationData.messages as AsyncAPIV3.MessageObject[] - await asyncFunction(async () => { - // 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 (!Array.isArray(messages) || messages.length === 0) { + continue + } - if (!action || !channelRef) { - return + const action = operationObject.action as AsyncOperationActionType + const channel = operationObject.channel as AsyncAPIV3.ChannelObject + if (!action || !channel) { + continue + } + + for (const message of messages) { + if (!isObject(message)) { + continue } - // Extract channel name from reference (e.g., "#/channels/userSignup" -> "userSignup") - const channel = typeof channelRef === 'string' && channelRef.startsWith('#/channels/') - ? channelRef.split('/').pop() || operationKey - : operationKey - - // TODO: Consider using operationId from spec if present (operationData.operationId) - const operationId = slugify(`${action}-${channel}`, SLUG_OPTIONS_OPERATION_ID) - - const trackedOperations = operationIdMap.get(operationId) ?? [] - trackedOperations.push({ channel, action }) - operationIdMap.set(operationId, trackedOperations) - - syncDebugPerformance('[Operation]', (innerDebugCtx) => - logLongBuild(() => { - const operation = buildAsyncApiOperation( - operationId, - operationKey, - action, - channel, - document, - effectiveDocument, - refsOnlyDocument, - notifications, - config, - normalizedSpecFragmentsHashCache, - innerDebugCtx, - ) - operations.push(operation) - }, - `${config.packageId}/${config.version} ${operationId}`, - ), debugCtx, [operationId]) - }) + const operationId = calculateAsyncOperationId(operationKey, (message.title as string) || '') + + if (!operationIdMap.has(operationId)) { + operationIdMap.set(operationId, []) + } + operationIdMap.get(operationId)!.push({ operationKey, action }) + + await asyncFunction(() => { + syncDebugPerformance('[Operation]', (innerDebugCtx) => + logLongBuild(() => { + const builtOperation = buildAsyncApiOperation( + operationId, + operationKey, + action, + channel, + message, + document, + effectiveDocument, + refsOnlyDocument, + notifications, + config, + normalizedSpecFragmentsHashCache, + innerDebugCtx, + ) + apihubOperations.push(builtOperation) + }, + `${config.packageId}/${config.version} ${operationId}`, + ), debugCtx, [operationId]) + }) + } } const duplicates = findDuplicates(operationIdMap) if (isNotEmpty(duplicates)) { - throw createDuplicatesError(document.fileId, duplicates) + throw createDuplicatesError(documentFileId, duplicates) } - if (operations.length) { + if (apihubOperations.length) { createSerializedInternalDocument(document, effectiveDocument, ASYNC_EFFECTIVE_NORMALIZE_OPTIONS) } - return operations + return apihubOperations } function findDuplicates(operationIdMap: Map): DuplicateEntry[] { @@ -135,11 +151,10 @@ function createDuplicatesError(fileId: string, duplicates: DuplicateEntry[]): Er const duplicatesList = duplicates .map(({ operationId, operations }) => { const operationsList = operations - .map((operation: OperationInfo) => `${operation.action.toUpperCase()} ${operation.channel}`) + .map((operation: OperationInfo) => `${operation.action.toUpperCase()} ${operation.operationKey}`) .join(', ') return `- operationId '${operationId}': Found ${operations.length} operations: ${operationsList}` }) .join('\n') return new Error(`Duplicated operationIds found within document '${fileId}':\n${duplicatesList}`) } - diff --git a/src/apitypes/async/async.parser.ts b/src/apitypes/async/async.parser.ts index e64a9b8a..60c6b868 100644 --- a/src/apitypes/async/async.parser.ts +++ b/src/apitypes/async/async.parser.ts @@ -20,99 +20,127 @@ import type { Parser, ParseOutput, Diagnostic } from '@asyncapi/parser' import { ASYNC_DOCUMENT_TYPE, ASYNC_FILE_FORMAT } from './async.consts' import { FILE_KIND, TextFile } from '../../types' import { getFileExtension } from '../../utils' -import { AsyncApiDocument } from './async.types' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' interface ValidationError { message: string path?: string } -export const parseAsyncApiFile = async (fileId: string, source: Blob): Promise | undefined> => { - const sourceString = await source.text() - const extension = getFileExtension(fileId) +class AsyncApiValidationError extends Error { + constructor(message: string) { + super(message) + this.name = 'AsyncApiValidationError' + } +} - // Detect AsyncAPI 3.0 documents - const isAsyncApi3Json = /\s*?"asyncapi"\s*?:\s*?"3\..+?"/g.test(sourceString) - const isAsyncApi3Yaml = /\s*?'?"?asyncapi'?"?\s*?:\s*?\|?\s*'?"?3\..+?'?"?/g.test(sourceString) +const YAML_EXTENSIONS = new Set([ASYNC_FILE_FORMAT.YAML, 'yml']) - if (extension === ASYNC_FILE_FORMAT.JSON || sourceString.trimStart().startsWith('{')) { - if (isAsyncApi3Json) { - const data = JSON.parse(sourceString) as AsyncApiDocument - const errors = await validateAsyncApiDocument(sourceString) +const ASYNCAPI_3_JSON_PATTERN = /\s*?"asyncapi"\s*?:\s*?"3\..+?"/ +const ASYNCAPI_3_YAML_PATTERN = /\s*?'?"?asyncapi'?"?\s*?:\s*?\|?\s*'?"?3\..+?'?"?/ +type FormatInfo = { + format: typeof ASYNC_FILE_FORMAT[keyof typeof ASYNC_FILE_FORMAT] + parse: (source: string) => AsyncAPIV3.AsyncAPIObject +} + +function detectFormat(extension: string, sourceString: string): FormatInfo | undefined { + if (extension === ASYNC_FILE_FORMAT.JSON || sourceString.trimStart().startsWith('{')) { + if (ASYNCAPI_3_JSON_PATTERN.test(sourceString)) { return { - fileId, - type: ASYNC_DOCUMENT_TYPE.AAS3, format: ASYNC_FILE_FORMAT.JSON, - data, - source, - errors, - kind: FILE_KIND.TEXT, + parse: (s) => JSON.parse(s) as AsyncAPIV3.AsyncAPIObject, } } - } else if (([ASYNC_FILE_FORMAT.YAML, 'yml'] as string[]).includes(extension) || !extension) { - if (isAsyncApi3Yaml) { - const data = YAML.load(sourceString) as AsyncApiDocument - const errors = await validateAsyncApiDocument(sourceString) - + } else if (YAML_EXTENSIONS.has(extension) || !extension) { + if (ASYNCAPI_3_YAML_PATTERN.test(sourceString)) { return { - fileId, - type: ASYNC_DOCUMENT_TYPE.AAS3, format: ASYNC_FILE_FORMAT.YAML, - data, - source, - errors, - kind: FILE_KIND.TEXT, + parse: (s) => YAML.load(s) as AsyncAPIV3.AsyncAPIObject, } } } - return undefined } +export const parseAsyncApiFile = async (fileId: string, source: Blob): Promise | undefined> => { + const sourceString = await source.text() + const extension = getFileExtension(fileId) + + const formatInfo = detectFormat(extension, sourceString) + if (!formatInfo) { + return undefined + } + + let data: AsyncAPIV3.AsyncAPIObject + try { + data = formatInfo.parse(sourceString) + } catch (error) { + throw new Error(`Failed to parse AsyncAPI file '${fileId}': ${error instanceof Error ? error.message : 'Unknown parse error'}`) + } + + const errors = await validateAsyncApiDocument(sourceString) + + return { + fileId, + type: ASYNC_DOCUMENT_TYPE.AAS3, + format: formatInfo.format, + data, + source, + errors, + kind: FILE_KIND.TEXT, + } +} + /** - * Validates AsyncAPI document using official parser + * Validates AsyncAPI document using official parser. * This provides spec validation while avoiding circular reference issues - * by using the parser only for validation, not for the actual parsing + * by using the parser only for validation, not for the actual parsing. * - * @throws Error when critical validation errors (severity 0) are found + * @throws AsyncApiValidationError when critical validation errors (severity 0) are found * @returns Non-critical diagnostics (warnings, info) to be collected as notifications */ +let cachedParserClass: typeof Parser | undefined + +async function getParserClass(): Promise { + if (cachedParserClass) { + return cachedParserClass + } + + // Dynamic import — bundler will use appropriate version based on environment. + // Browser builds will use @asyncapi/parser/browser automatically via vite alias. + const parserModule = await import('@asyncapi/parser') + + // Handle different export formats (ESM named export vs default export) + const ParserClass = (parserModule as { Parser?: typeof Parser; default?: typeof Parser }).Parser || + parserModule.default as typeof Parser + + if (!ParserClass) { + throw new Error('AsyncAPI Parser class not found in module exports') + } + + cachedParserClass = ParserClass + return ParserClass +} + async function validateAsyncApiDocument(sourceString: string): Promise { try { - // Dynamic import - bundler will use appropriate version based on environment - // Browser builds will use @asyncapi/parser/browser automatically via vite alias - const parserModule = await import('@asyncapi/parser') - - // Handle different export formats (ESM named export vs default export) - // ESM: export { Parser } and export default Parser - // Browser (UMD converted by Vite): exposes both named and default - const ParserClass = (parserModule as { Parser?: typeof Parser; default?: typeof Parser }).Parser || - parserModule.default as typeof Parser - - if (!ParserClass) { - throw new Error('AsyncAPI Parser class not found in module exports') - } - + const ParserClass = await getParserClass() const parser: Parser = new ParserClass() const { diagnostics }: ParseOutput = await parser.parse(sourceString) - // Separate critical errors from non-critical diagnostics - // DiagnosticSeverity.Error = 0 const criticalErrors: Diagnostic[] = diagnostics.filter(diagnostic => diagnostic.severity === 0) const nonCriticalDiagnostics: Diagnostic[] = diagnostics.filter(diagnostic => diagnostic.severity > 0) - // Throw error if critical validation errors are found - this will fail the build if (criticalErrors.length > 0) { const errorMessages = criticalErrors.map(err => { const location = err.range ? ` at line ${err.range.start.line}` : '' return `${err.message}${location}` }).join('; ') - throw new Error(`AsyncAPI validation failed: ${errorMessages}`) + throw new AsyncApiValidationError(`AsyncAPI validation failed: ${errorMessages}`) } - // Return non-critical diagnostics to be added to notifications if (nonCriticalDiagnostics.length > 0) { return nonCriticalDiagnostics.map(diagnostic => ({ message: diagnostic.message, @@ -122,12 +150,10 @@ async function validateAsyncApiDocument(sourceString: string): Promise // Channels definition - operations?: Record // Operations definition - components?: Record // Reusable components - servers?: Record // Server definitions - [key: string]: any + info?: Partial + externalDocs?: Partial + tags: AsyncAPIV3.TagObject[] } /** @@ -64,22 +50,28 @@ export interface AsyncApiDocument { */ export interface AsyncOperationData { asyncapi: string - info: AsyncApiDocument['info'] - channels?: Record - operations?: Record - components?: Record - servers?: Record + info: AsyncAPIV3.InfoObject + channels?: AsyncAPIV3.ChannelsObject + operations?: AsyncAPIV3.OperationsObject + components?: AsyncAPIV3.ComponentsObject + servers?: AsyncAPIV3.ServersObject } -export type VersionAsyncDocument = VersionDocument +export type VersionAsyncDocument = VersionDocument export type VersionAsyncOperation = ApiOperation +export type AsyncOperation = ApiOperation & { + channels?: ApiOperation + messages?: ApiOperation +} +// TODO Delete AsyncRefCache if not used in future export interface AsyncRefCache { scopes: Record refs: string[] data: any } +// TODO Delete AsyncOperationContext if not used in future export interface AsyncOperationContext { operationId: string scopes: Record diff --git a/src/apitypes/async/async.utils.ts b/src/apitypes/async/async.utils.ts index cc84f747..5dac3744 100644 --- a/src/apitypes/async/async.utils.ts +++ b/src/apitypes/async/async.utils.ts @@ -14,42 +14,29 @@ * limitations under the License. */ -import { AsyncApiDocument } from './async.types' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' +import { isObject } from '../../utils' +import { AsyncOperationActionType } from './async.types' +import { normalize } from '@netcracker/qubership-apihub-api-unifier' +import { APIHUB_API_COMPATIBILITY_KIND_BWC, ApihubApiCompatibilityKind } from '../../consts' +import { JsonPath } from '@netcracker/qubership-apihub-json-crawl' // Re-export shared utilities export { dump, getCustomTags, resolveApiAudience } from '../../utils/apihubSpecificationExtensions' /** * Extracts protocol from AsyncAPI document servers or channel bindings - * @param document - AsyncAPI document - * @param channel - Channel name + * @param channel - Channel object to extract protocol from * @returns Protocol string (e.g., 'kafka', 'amqp', 'mqtt') or 'unknown' */ -export function extractProtocol(document: AsyncApiDocument, channel: string): string { - // Try to extract protocol from servers - if (document.servers && typeof document.servers === 'object') { - for (const server of Object.values(document.servers)) { - if (server && typeof server === 'object' && server.protocol) { - return String(server.protocol) - } - } +export function extractProtocol(channel: AsyncAPIV3.ChannelObject): string { + if (!isObject(channel.servers)) { + return 'unknown' } - - // Try to extract protocol from channel bindings - if (document.channels && document.channels[channel]) { - const channelObj = document.channels[channel] - if (channelObj && typeof channelObj === 'object') { - // Check for protocol in bindings - if (channelObj.bindings && typeof channelObj.bindings === 'object') { - const bindings = channelObj.bindings - // Common protocol bindings: kafka, amqp, mqtt, http, ws, etc. - const knownProtocols = ['kafka', 'amqp', 'mqtt', 'http', 'ws', 'websockets', 'jms', 'nats', 'redis', 'sns', 'sqs'] - for (const protocol of knownProtocols) { - if (bindings[protocol]) { - return protocol - } - } - } + for (const server of Object.values(channel.servers as AsyncAPIV3.ServerObject[])) { + if (isServerObject(server) && server.protocol) { + const { protocol } = server + return protocol } } @@ -61,9 +48,9 @@ export function extractProtocol(document: AsyncApiDocument, channel: string): st * @param operationData - Operation object * @returns 'send' or 'receive' */ -export function determineOperationAction(operationData: any): 'send' | 'receive' { +export function determineOperationAction(operationData: any): AsyncOperationActionType { if (operationData && typeof operationData === 'object') { - const action = operationData.action + const { action } = operationData if (action === 'send' || action === 'receive') { return action } @@ -72,3 +59,71 @@ export function determineOperationAction(operationData: any): 'send' | 'receive' return 'send' } +function isServerObject(server: AsyncAPIV3.ServerObject | AsyncAPIV3.ReferenceObject): server is AsyncAPIV3.ServerObject { + return isObject(server) && 'protocol' in server +} + +function isTagObject(item: AsyncAPIV3.TagObject | AsyncAPIV3.ReferenceObject): item is AsyncAPIV3.TagObject { + return (item as AsyncAPIV3.TagObject).name !== undefined +} + +function isExternalDocumentationObject(item: AsyncAPIV3.ExternalDocumentationObject | AsyncAPIV3.ReferenceObject): item is AsyncAPIV3.ExternalDocumentationObject { + return (item as AsyncAPIV3.ExternalDocumentationObject).url !== undefined +} + +export function toTagObjects( + data: AsyncAPIV3.AsyncAPIObject, +): AsyncAPIV3.TagObject[] { + return data?.info?.tags?.map(item => { + if (isTagObject(item)) { + return item + } + + return normalize(item, { + source: data, + }) as AsyncAPIV3.TagObject + }) ?? [] +} + +export function toExternalDocumentationObject( + data: AsyncAPIV3.AsyncAPIObject, +): AsyncAPIV3.ExternalDocumentationObject | undefined { + const externalDocs = data?.info?.externalDocs + if (!externalDocs) { + return undefined + } + + return isExternalDocumentationObject(externalDocs) + ? externalDocs + : normalize(externalDocs, { + source: data, + }) as AsyncAPIV3.ExternalDocumentationObject +} + +export const calculateAsyncApiKind = ( + operationApiKind: ApihubApiCompatibilityKind | undefined, + channelApiKind: ApihubApiCompatibilityKind | undefined, +): ApihubApiCompatibilityKind => { + return operationApiKind || channelApiKind || APIHUB_API_COMPATIBILITY_KIND_BWC +} + +export const extractKeyAfterPrefix = (paths: JsonPath[], prefix: PropertyKey[]): string | undefined => { + for (const path of paths) { + if (path.length <= prefix.length) { + continue + } + let matches = true + for (let i = 0; i < prefix.length; i++) { + if (path[i] !== prefix[i]) { + matches = false + break + } + } + if (!matches) { + continue + } + const key = path[prefix.length] + return key === undefined ? undefined : String(key) + } + return undefined +} diff --git a/src/apitypes/async/index.ts b/src/apitypes/async/index.ts index 068cdf7c..77cad66e 100644 --- a/src/apitypes/async/index.ts +++ b/src/apitypes/async/index.ts @@ -16,16 +16,16 @@ import { buildAsyncApiDocument, dumpAsyncApiDocument } from './async.document' import { buildAsyncApiOperations } from './async.operations' -import { ASYNCAPI_API_TYPE, ASYNC_DOCUMENT_TYPE } from './async.consts' +import { ASYNC_DOCUMENT_TYPE, ASYNCAPI_API_TYPE } from './async.consts' import { parseAsyncApiFile } from './async.parser' import { ApiBuilder } from '../../types' import { compareDocuments } from './async.changes' -import { AsyncApiDocument } from './async.types' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' export * from './async.consts' export * from './async.types' -export const asyncApiBuilder: ApiBuilder = { +export const asyncApiBuilder: ApiBuilder = { apiType: ASYNCAPI_API_TYPE, types: Object.values(ASYNC_DOCUMENT_TYPE), parser: parseAsyncApiFile, diff --git a/src/apitypes/rest/rest.document.ts b/src/apitypes/rest/rest.document.ts index 931f753b..094397af 100644 --- a/src/apitypes/rest/rest.document.ts +++ b/src/apitypes/rest/rest.document.ts @@ -34,7 +34,7 @@ import { createVersionInternalDocument, EXPORT_FORMAT_TO_FILE_FORMAT, getBundledFileDataWithDependencies, - getDocumentTitle, + getDocumentTitle, getStringValue, } from '../../utils' import { dump } from './rest.utils' import { generateHtmlPage } from '../../utils/export' @@ -49,8 +49,6 @@ const openApiDocumentMeta = (data: OpenAPIV3.Document): RestDocumentInfo => { const { title = '', version = '', description = '' } = data?.info || {} - const getStringValue = (value: unknown): string => (typeof (value) === 'string' ? value : '') - const info: Partial = { ...data?.info } delete info?.title delete info?.description diff --git a/src/apitypes/rest/rest.operation.ts b/src/apitypes/rest/rest.operation.ts index eb77d8a1..47ef56ea 100644 --- a/src/apitypes/rest/rest.operation.ts +++ b/src/apitypes/rest/rest.operation.ts @@ -34,7 +34,7 @@ import { buildSearchScope, calculateRestOperationId, capitalize, - extractSymbolProperty, + extractSymbolProperty, getInlineRefsFomDocument, getKeyValue, getSplittedVersionKey, getSymbolValueIfDefined, @@ -204,27 +204,7 @@ export const calculateSpecRefs = ( models?: Record, originalSpecComponentsHashCache?: Map, ): void => { - const handledObjects = new Set() - const inlineRefs = new Set() - syncCrawl( - normalizedSpec, - ({ key, value }) => { - if (typeof key === 'symbol' && key !== INLINE_REFS_FLAG) { - return { done: true } - } - if (handledObjects.has(value)) { - return { done: true } - } - handledObjects.add(value) - if (key !== INLINE_REFS_FLAG) { - return { value } - } - if (!Array.isArray(value)) { - return { done: true } - } - value.forEach(ref => inlineRefs.add(ref)) - }, - ) + const inlineRefs = getInlineRefsFomDocument(normalizedSpec) inlineRefs.forEach(ref => { const path = parseRef(ref).jsonPath const grepKey = 'componentName' diff --git a/src/strategies/export-rest-operations-group.strategy.ts b/src/strategies/export-rest-operations-group.strategy.ts index 7abb01f5..59022017 100644 --- a/src/strategies/export-rest-operations-group.strategy.ts +++ b/src/strategies/export-rest-operations-group.strategy.ts @@ -29,7 +29,6 @@ import { EXPORT_FORMAT_TO_FILE_FORMAT, getSplittedVersionKey } from '../utils' import { BUILD_TYPE, FILE_FORMAT_HTML, FILE_FORMAT_JSON } from '../consts' import { createCommonStaticExportDocuments, createUnknownExportDocument, generateIndexHtmlPage } from '../utils/export' import { createRestExportDocument } from '../apitypes/rest/rest.document' -import { isRestDocument } from '../apitypes' export class ExportRestOperationsGroupStrategy implements BuilderStrategy { async execute(config: ExportRestOperationsGroupBuildConfig, buildResult: BuildResult, contexts: BuildTypeContexts): Promise { diff --git a/src/types/internal/operation.ts b/src/types/internal/operation.ts index 5cae529b..d3380fd5 100644 --- a/src/types/internal/operation.ts +++ b/src/types/internal/operation.ts @@ -19,7 +19,7 @@ import { ApiAudience } from '../package' import { OpenAPIV3 } from 'openapi-types' import { GraphApiSchema } from '@netcracker/qubership-apihub-graphapi' import { ApihubApiCompatibilityKind } from '../../consts' -import { AsyncApiDocument } from '../../apitypes/async/async.types' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/cjs/spec-types' export type SearchScopes = Record> @@ -52,4 +52,4 @@ export interface ApiOperation { versionInternalDocumentId: string } -export type ApiDocument = OpenAPIV3.Document | GraphApiSchema | AsyncApiDocument +export type ApiDocument = OpenAPIV3.Document | GraphApiSchema | AsyncAPIV3.AsyncAPIObject diff --git a/src/utils/operations.utils.ts b/src/utils/operations.utils.ts index fc14d677..5016e768 100644 --- a/src/utils/operations.utils.ts +++ b/src/utils/operations.utils.ts @@ -19,7 +19,12 @@ import { GraphApiComponents, GraphApiDirectiveDefinition } from '@netcracker/qub import { OpenAPIV3 } from 'openapi-types' import { isObject } from './objects' import { serializeDocument } from './document' -import { SLUG_OPTIONS_DOCUMENT_ID, SLUG_OPTIONS_NORMALIZED_OPERATION_ID, SLUG_OPTIONS_OPERATION_ID, slugify } from './slugify' +import { + SLUG_OPTIONS_DOCUMENT_ID, + SLUG_OPTIONS_NORMALIZED_OPERATION_ID, + SLUG_OPTIONS_OPERATION_ID, + slugify, +} from './slugify' import { normalizePath, removeFirstSlash } from './builder' import { Diff, DiffAction } from '@netcracker/qubership-apihub-api-diff' import { @@ -30,7 +35,10 @@ import { PREDICATE_ANY_VALUE, } from '@netcracker/qubership-apihub-api-unifier' import { DirectiveLocation } from 'graphql/language' -import { HTTP_METHODS_SET } from '../consts' +import { HTTP_METHODS_SET, INLINE_REFS_FLAG } from '../consts' +import { syncCrawl } from '@netcracker/qubership-apihub-json-crawl' +import { RestOperationData } from '../apitypes/rest/rest.types' +import { AsyncOperationData } from '../apitypes' export function getOperationsList(buildResult: BuildResult): ApiOperation[] { return [...buildResult.operations.values()] @@ -165,3 +173,35 @@ export const createSerializedInternalDocument = (document: VersionDocument, effe } versionInternalDocument.serializedVersionDocument = serializeDocument(denormalize(effectiveDocument, options) as ApiDocument) } + +export const calculateAsyncOperationId = ( + normalizedOperationId: string, + normalizedMessageId: string, +): string => { + return `${normalizedOperationId}-${normalizedMessageId}` +} + +export const getInlineRefsFomDocument = (document: RestOperationData | AsyncOperationData): Set => { + const handledObjects = new Set() + const inlineRefs = new Set() + syncCrawl( + document, + ({ key, value }) => { + if (typeof key === 'symbol' && key !== INLINE_REFS_FLAG) { + return { done: true } + } + if (handledObjects.has(value)) { + return { done: true } + } + handledObjects.add(value) + if (key !== INLINE_REFS_FLAG) { + return { value } + } + if (!Array.isArray(value)) { + return { done: true } + } + value.forEach(ref => inlineRefs.add(ref)) + }, + ) + return inlineRefs +} diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 23f48259..09e1c878 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { isString } from './objects' // https://stackoverflow.com/questions/196972/convert-string-to-title-case-with-javascript export function toTitleCase(str: string): string { @@ -58,3 +59,5 @@ export function toTitleCase(str: string): string { export function toLowerCase(value: unknown): string { return `${value}`.toLowerCase() } + +export const getStringValue = (value: unknown): string => (isString(value) ? value : '') diff --git a/test/asyncapi-apikind.test.ts b/test/asyncapi-apikind.test.ts new file mode 100644 index 00000000..c3aae9a5 --- /dev/null +++ b/test/asyncapi-apikind.test.ts @@ -0,0 +1,113 @@ +import { + APIHUB_API_COMPATIBILITY_KIND_BWC, + APIHUB_API_COMPATIBILITY_KIND_NO_BWC, + ApihubApiCompatibilityKind, + ApiOperation, +} from '../src' +import { calculateAsyncApiKind } from '../src/apitypes/async/async.utils' +import { buildPackageDefaultConfig } from './helpers' + +describe('AsyncAPI apiKind calculation', () => { + describe('Unit tests', () => { + it('should calculate apiKind from operation and channel values', () => { + const data = [ + // Operation ApiKind, Channel ApiKnd, Result + [undefined, undefined, APIHUB_API_COMPATIBILITY_KIND_BWC], + [APIHUB_API_COMPATIBILITY_KIND_BWC, undefined, APIHUB_API_COMPATIBILITY_KIND_BWC], + [undefined, APIHUB_API_COMPATIBILITY_KIND_BWC, APIHUB_API_COMPATIBILITY_KIND_BWC], + [APIHUB_API_COMPATIBILITY_KIND_NO_BWC, undefined, APIHUB_API_COMPATIBILITY_KIND_NO_BWC], + [undefined, APIHUB_API_COMPATIBILITY_KIND_NO_BWC, APIHUB_API_COMPATIBILITY_KIND_NO_BWC], + [APIHUB_API_COMPATIBILITY_KIND_BWC, APIHUB_API_COMPATIBILITY_KIND_NO_BWC, APIHUB_API_COMPATIBILITY_KIND_BWC], + [APIHUB_API_COMPATIBILITY_KIND_NO_BWC, APIHUB_API_COMPATIBILITY_KIND_BWC, APIHUB_API_COMPATIBILITY_KIND_NO_BWC], + ] + data.forEach(([operationApiKind, channelApiKind, expected]) => { + const result = calculateAsyncApiKind(operationApiKind as ApihubApiCompatibilityKind, channelApiKind as ApihubApiCompatibilityKind) + expect(result).toBe(expected) + }) + }) + }) + + describe('AsyncAPI operation/channel compatibility apiKind application', () => { + let operation: ApiOperation + let operationWithCannelBwc: ApiOperation + let operationWithCannelNoBwc: ApiOperation + let operationBwc: ApiOperation + let operationNoBwc: ApiOperation + let operationBwcWithChannelBwc: ApiOperation + let operationBwcWithChannelNoBwc: ApiOperation + let operationNoBwcWithChannelBwc: ApiOperation + let operationNoBwcWithChannelNoBwc: ApiOperation + + beforeAll(async () => { + const result = await buildPackageDefaultConfig('asyncapi/api-kind/base') + ;[ + operation, + operationWithCannelBwc, + operationWithCannelNoBwc, + operationBwc, + operationNoBwc, + operationBwcWithChannelBwc, + operationBwcWithChannelNoBwc, + operationNoBwcWithChannelBwc, + operationNoBwcWithChannelNoBwc, + ] = Array.from(result.operations.values()) + }) + + it('should apply BWC apiKind when both operation and channel have no apiKind', () => { + expect(operation.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + }) + + it('should apply BWC apiKind when channel apiKind is BWC and operation has no apiKind', () => { + expect(operationWithCannelBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + }) + + it('should apply NO_BWC apiKind when channel apiKind is NO_BWC and operation has no apiKind', () => { + expect(operationWithCannelNoBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) + }) + + it('should apply BWC apiKind when operation apiKind is BWC and channel has no apiKind', () => { + expect(operationBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + }) + + it('should apply NO_BWC apiKind when operation apiKind is NO_BWC and channel has no apiKind', () => { + expect(operationNoBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) + }) + + it('should apply BWC apiKind when both operation and channel apiKind are BWC', () => { + expect(operationBwcWithChannelBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + }) + + it('should apply BWC apiKind when operation apiKind is BWC and channel apiKind is NO_BWC', () => { + expect(operationBwcWithChannelNoBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + }) + + it('should apply NO_BWC apiKind when operation apiKind is NO_BWC and channel apiKind is BWC', () => { + expect(operationNoBwcWithChannelBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) + }) + + it('should apply NO_BWC apiKind when both operation and channel apiKind are NO_BWC', () => { + expect(operationNoBwcWithChannelNoBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_NO_BWC) + }) + }) + + it('should apply channel apiKind to all operations using that channel', async () => { + const result = await buildPackageDefaultConfig('asyncapi/api-kind/share-channel-api-kind') + const operations = Array.from(result.operations.values()) + + expect(operations.every(operation => operation.apiKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC)).toBeTrue() + }) + + describe('Labels should not redefine AsyncAPI apiKind', () => { + it('should not override default apiKind by Label', async () => { + const result = await buildPackageDefaultConfig('asyncapi/api-kind/base', ['apihub/x-api-kind: no-BWC']) + const [operation] = Array.from(result.operations.values()) + expect(operation.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + }) + + it('should not override operation/channel apiKind by Label', async () => { + const result = await buildPackageDefaultConfig('asyncapi/api-kind/base', ['apihub/x-api-kind: no-BWC']) + const [operationWithCannelBwc] = Array.from(result.operations.values()) + expect(operationWithCannelBwc.apiKind).toEqual(APIHUB_API_COMPATIBILITY_KIND_BWC) + }) + }) +}) diff --git a/test/asyncapi-changes.test.ts b/test/asyncapi-changes.test.ts new file mode 100644 index 00000000..1137143a --- /dev/null +++ b/test/asyncapi-changes.test.ts @@ -0,0 +1,175 @@ +/** + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + buildChangelogPackage, + changesSummaryMatcher, + noChangesMatcher, + numberOfImpactedOperationsMatcher, + operationTypeMatcher, +} from './helpers' +import { + ANNOTATION_CHANGE_TYPE, + ASYNCAPI_API_TYPE, + BREAKING_CHANGE_TYPE, + NON_BREAKING_CHANGE_TYPE, + UNCLASSIFIED_CHANGE_TYPE, +} from '../src' + +describe.skip('AsyncAPI 3.0 Changelog', () => { + + test('no changes', async () => { + const result = await buildChangelogPackage('asyncapi-changes/no-changes') + + expect(result).toEqual(noChangesMatcher(ASYNCAPI_API_TYPE)) + }) + + describe('Channels', () => { + test('add channel', async () => { + const result = await buildChangelogPackage('asyncapi-changes/channel/add') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('remove channel', async () => { + const result = await buildChangelogPackage('asyncapi-changes/channel/remove') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('change channel address', async () => { + const result = await buildChangelogPackage('asyncapi-changes/channel/change') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + }) + + describe('Operations tests', () => { + test('add operation', async () => { + const result = await buildChangelogPackage('asyncapi-changes/operation/add') + expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('add multiple operations', async () => { + const result = await buildChangelogPackage('asyncapi-changes/operation/add-multiple') + expect(result).toEqual(changesSummaryMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [NON_BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + }) + + test('remove operation', async () => { + const result = await buildChangelogPackage('asyncapi-changes/operation/remove') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('add and remove operations', async () => { + const result = await buildChangelogPackage('asyncapi-changes/operation/add-remove') + expect(result).toEqual(changesSummaryMatcher({ + [BREAKING_CHANGE_TYPE]: 1, + [NON_BREAKING_CHANGE_TYPE]: 1, + }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ + [BREAKING_CHANGE_TYPE]: 1, + [NON_BREAKING_CHANGE_TYPE]: 1, + }, ASYNCAPI_API_TYPE)) + }) + + test('change operation', async () => { + const result = await buildChangelogPackage('asyncapi-changes/operation/change') + + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('renamed operation as add/remove', async () => { + const result = await buildChangelogPackage('asyncapi-changes/operation/rename') + expect(result).toEqual(changesSummaryMatcher({ + [BREAKING_CHANGE_TYPE]: 1, + [NON_BREAKING_CHANGE_TYPE]: 1, + }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ + [BREAKING_CHANGE_TYPE]: 1, + [NON_BREAKING_CHANGE_TYPE]: 1, + }, ASYNCAPI_API_TYPE)) + }) + }) + + describe('Servers', () => { + test('add server', async () => { + const result = await buildChangelogPackage('asyncapi-changes/server/add') + + expect(result).toEqual(changesSummaryMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [UNCLASSIFIED_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('remove server', async () => { + const result = await buildChangelogPackage('asyncapi-changes/server/remove') + + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('change server', async () => { + const result = await buildChangelogPackage('asyncapi-changes/server/change') + + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('add root servers', async () => { + const result = await buildChangelogPackage('asyncapi-changes/server/add-root') + + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('remove root servers', async () => { + const result = await buildChangelogPackage('asyncapi-changes/server/remove-root') + + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('change root servers', async () => { + const result = await buildChangelogPackage('asyncapi-changes/server/change-root') + + expect(result).toEqual(changesSummaryMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [ANNOTATION_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + }) + + describe('tags', () => { + test('Tags are not duplicated', async () => { + const result = await buildChangelogPackage('asyncapi-changes/tags') + + expect(result).toEqual(operationTypeMatcher({ + tags: expect.toIncludeSameMembers([ + 'sameTagInDifferentChannels1', + 'sameTagInDifferentChannels2', + 'sameTagInDifferentChannels3', + 'sameTagInDifferentSiblings1', + 'sameTagInDifferentSiblings2', + 'sameTagInDifferentSiblings3', + 'tag', + ]), + })) + }) + }) +}) diff --git a/test/asyncapi-deprecated.test.ts b/test/asyncapi-deprecated.test.ts new file mode 100644 index 00000000..13945c4f --- /dev/null +++ b/test/asyncapi-deprecated.test.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, test } from '@jest/globals' +import { buildPackageDefaultConfig, deprecatedItemDescriptionMatcher } from './helpers' +import { DeprecateItem } from '../src' +import { isOperationDeprecated } from '../src/utils' + +describe('AsyncAPI 3.0 Deprecated tests', () => { + + describe('Channel tests', () => { + let deprecatedItems: DeprecateItem[] + beforeAll(async () => { + const result = await buildPackageDefaultConfig('asyncapi/deprecated/channel') + deprecatedItems = Array.from(result.operations.values()).flatMap(operation => operation.deprecatedItems ?? []) + }) + + test('should deprecated channel has message', async () => { + const [deprecatedItem] = deprecatedItems + + expect(deprecatedItems.length).toBeGreaterThan(0) + expect(deprecatedItem).toEqual(deprecatedItemDescriptionMatcher('[Deprecated] channel \'userSignedUp\'')) + }) + + test('should deprecated channel item has hash', async () => { + const [deprecatedItem] = deprecatedItems + + expect(deprecatedItem).toHaveProperty('hash') + expect(deprecatedItem).toHaveProperty('tolerantHash') + }) + }) + + describe('Messages tests', () => { + let deprecatedItems: DeprecateItem[] + beforeAll(async () => { + const result = await buildPackageDefaultConfig('asyncapi/deprecated/messages') + deprecatedItems = Array.from(result.operations.values()).flatMap(operation => operation.deprecatedItems ?? []) + }) + + test('should detect deprecated messages', async () => { + const [deprecatedItem] = deprecatedItems + + expect(deprecatedItems.length).toBeGreaterThan(0) + expect(deprecatedItem).toEqual(deprecatedItemDescriptionMatcher('[Deprecated] message \'User Signed Up\'')) + }) + + test('should deprecated message item has no hash', async () => { + const [deprecatedItem] = deprecatedItems + + expect(deprecatedItem).not.toHaveProperty('hash') + expect(deprecatedItem).not.toHaveProperty('tolerantHash') + }) + + test('should deprecated message has OperationDeprecated symbol with true', async () => { + const [deprecatedItem] = deprecatedItems + const operationDeprecatedSymbol = (deprecatedItem as unknown as Record)[isOperationDeprecated] + expect(operationDeprecatedSymbol).toBeTruthy() + }) + }) + + test('should mark apihub operation is deprecated if message deprecated', async () => { + const result = await buildPackageDefaultConfig('asyncapi/deprecated/messages') + const operations = Array.from(result.operations.values()) + + const [operation] = operations + expect(operation.deprecated).toBe(true) + }) + + test('should detect deprecated schemas (deprecated flag in payload schema)', async () => { + const result = await buildPackageDefaultConfig('asyncapi/deprecated/schemas') + const deprecatedItems = Array.from(result.operations.values()).flatMap(operation => operation.deprecatedItems ?? []) + expect(deprecatedItems.length).toBeGreaterThan(0) + + const [deprecatedItem] = deprecatedItems + expect(deprecatedItem).toEqual(deprecatedItemDescriptionMatcher('[Deprecated] schema in \'components.schemas.DeprecatedEmail\'')) + + expect(deprecatedItem).toHaveProperty('hash') + expect(deprecatedItem).toHaveProperty('tolerantHash') + + expect(deprecatedItem.declarationJsonPaths.some(path => path.at(-1) === 'deprecated')).toBe(true) + }) +}) diff --git a/test/asyncapi-info.test.ts b/test/asyncapi-info.test.ts new file mode 100644 index 00000000..bec9749c --- /dev/null +++ b/test/asyncapi-info.test.ts @@ -0,0 +1,93 @@ +import { buildPackageDefaultConfig } from './helpers' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/esm/spec-types' + +describe('Info', () => { + describe('Tags', () => { + const TAGS_INLINE_PACKAGE_ID = 'asyncapi/info/tags/simple' + const TAGS_FROM_REF_PACKAGE_ID = 'asyncapi/info/tags/refs' + const TAGS_MIXED_INLINE_AND_REF_PACKAGE_ID = 'asyncapi/info/tags/mixed' + + test('should extract tags from inline tag definitions', async () => { + await runTagsTest( + TAGS_INLINE_PACKAGE_ID, + [ + { + 'name': 'simple_tag1', + 'description': 'Description for simple_tag1', + }, + { + 'name': 'simple_tag2', + 'description': 'Description for simple_tag2', + }, + ]) + }) + + test('should extract tags when tags are defined via $ref', async () => { + await runTagsTest( + TAGS_FROM_REF_PACKAGE_ID, + [ + { + 'name': 'ref_tag1', + 'description': 'Description for ref_tag1', + }, + { + 'name': 'ref_tag2', + 'description': 'Description for ref_tag2', + }, + ]) + }) + + test('should extract tags from a mix of inline and $ref tag definitions', async () => { + await runTagsTest( + TAGS_MIXED_INLINE_AND_REF_PACKAGE_ID, + [ + { + 'name': 'ref_tag1', + 'description': 'Description for ref_tag1', + }, + { + 'name': 'simple_tag2', + 'description': 'Description for simple_tag2', + }, + ]) + }) + + async function runTagsTest(packageId: string, expectedTags: AsyncAPIV3.TagObject[]): Promise { + const result = await buildPackageDefaultConfig(packageId) + + const [document] = Array.from(result.documents.values()) + const { tags } = document.metadata + expect(tags).toEqual(expectedTags) + } + }) + + describe('External documentation', () => { + const EXTERNAL_DOCS_INLINE_PACKAGE_ID = 'asyncapi/info/external-documentation/simple' + const EXTERNAL_DOCS_FROM_REF_PACKAGE_ID = 'asyncapi/info/external-documentation/refs' + + test('should extract external documentation from an inline externalDocs object', async () => { + await runExternalDocumentationTest(EXTERNAL_DOCS_INLINE_PACKAGE_ID, { + 'description': 'Simple', + 'url': 'https://example.com/docs', + }) + }) + + test('should extract external documentation when externalDocs is defined via $ref', async () => { + await runExternalDocumentationTest(EXTERNAL_DOCS_FROM_REF_PACKAGE_ID, { + 'description': 'Ref', + 'url': 'https://example.com/docs', + }) + }) + + async function runExternalDocumentationTest( + packageId: string, + expectedExternalDocumentationObject: AsyncAPIV3.ExternalDocumentationObject, + ): Promise { + const result = await buildPackageDefaultConfig(packageId) + + const [document] = Array.from(result.documents.values()) + const { externalDocs } = document.metadata + expect(externalDocs).toEqual(expectedExternalDocumentationObject) + } + }) +}) diff --git a/test/asyncapi-operation.test.ts b/test/asyncapi-operation.test.ts new file mode 100644 index 00000000..9cbd824b --- /dev/null +++ b/test/asyncapi-operation.test.ts @@ -0,0 +1,287 @@ +/** + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { beforeAll, describe, expect, it, test } from '@jest/globals' +import { v3 as AsyncAPIV3 } from '@asyncapi/parser/cjs/spec-types' +import { createOperationSpec } from '../src/apitypes/async/async.operation' +import { calculateAsyncOperationId } from '../src/utils' +import { buildPackageDefaultConfig, cloneDocument, loadYamlFile } from './helpers' +import { extractProtocol } from '../src/apitypes/async/async.utils' +import { INLINE_REFS_FLAG } from '../src/consts' + +describe('AsyncAPI 3.0 Operation Tests', () => { + + describe('Building Package with Operations', () => { + test('should ignore operation without message', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/broken-operation') + expect(Array.from(result.operations.values())).toHaveLength(0) + }) + + test('should extract single operation from package', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/single-operation') + expect(Array.from(result.operations.values())).toHaveLength(1) + }) + + test('should extract multiple operations from package', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/multiple-operations') + expect(Array.from(result.operations.values())).toHaveLength(3) + }) + }) + + describe('OperationId Tests', () => { + it.skip('should generate unique operationIds (unit)', () => { + const data = [ + ['channel1', 'message1', 'channel1message1-send'], + ['channel1', 'message1', 'channel1message1-receive'], + ['channel2', 'message1', 'channel2message1-send'], + ] + data.forEach(([data1, data2, expected]) => { + const result = calculateAsyncOperationId(data1, data2) + expect(result).toBe(expected) + }) + }) + + it.skip('should set operationId in built package (e2e)', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/single-operation') + const operations = Array.from(result.operations.values()) + const [operation] = operations + expect(operation.operationId).toBe( + calculateAsyncOperationId('User Signed Up', 'sendUserSignedup'), + ) + }) + }) + + describe('Operation title test', () => { + it('should set operation title in built package (e2e) as message title', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/single-operation') + const operations = Array.from(result.operations.values()) + const [operation] = operations + expect(operation.title).toBe('User Signed Up') + }) + + it.skip('should set operation title as message id if message title doesn\'t exist', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/single-operation') + const operations = Array.from(result.operations.values()) + const [operation] = operations + expect(operation.title).toBe('User Signed Up') + }) + }) + + describe('Operation protocol tests', () => { + it('should uses the (first) server protocol', () => { + const channel = { + title: 'channel1', + servers: [ + { protocol: 'amqp' }, + { protocol: 'kafka' }, + ], + } as AsyncAPIV3.ChannelObject + + expect(extractProtocol(channel)).toBe('amqp') + }) + + it('should skip $ref servers and return first server with protocol', () => { + const channel = { + title: 'channel1', + servers: [ + { $ref: '#/servers/amqp1' }, + { protocol: 'amqp' }, + ], + } as AsyncAPIV3.ChannelObject + + expect(extractProtocol(channel)).toBe('amqp') + }) + + it('should returns unknown when servers are missing or empty', () => { + expect(extractProtocol({ title: 'no-servers' } as unknown as AsyncAPIV3.ChannelObject)).toBe('unknown') + expect(extractProtocol({ + title: 'empty-servers', + servers: [], + } as unknown as AsyncAPIV3.ChannelObject)).toBe('unknown') + }) + + it('should operation has protocol', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/single-operation') + const operations = Array.from(result.operations.values()) + const [operation] = operations + expect(operation.metadata.protocol).toBeDefined() + }) + }) + + describe('Operation security tests', () => { + it('should preserve operation-level security in built package', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/security-operation') + const [apiHubOperation] = Array.from(result.operations.values()) + const asyncApiDocument: AsyncAPIV3.AsyncAPIObject = apiHubOperation.data + const operationEntries = Object.values(asyncApiDocument.operations ?? {}) as AsyncAPIV3.OperationObject[] + expect(operationEntries).toHaveLength(1) + + const [asyncOperation] = operationEntries + expect(asyncOperation.security).toBeDefined() + expect(asyncOperation.security).toHaveLength(2) + }) + + it('should include securitySchemes in components when inlined', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/security-operation') + const [apiHubOperation] = Array.from(result.operations.values()) + const asyncApiDocument: AsyncAPIV3.AsyncAPIObject = apiHubOperation.data + + const securitySchemes = asyncApiDocument.components?.securitySchemes + expect(securitySchemes).toHaveProperty('oauth2') + expect(securitySchemes).toHaveProperty('apiKey') + }) + + it('should not have security when operation has no security defined', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/single-operation') + const [apiHubOperation] = Array.from(result.operations.values()) + const asyncApiDocument: AsyncAPIV3.AsyncAPIObject = apiHubOperation.data + + const operationEntries = Object.values(asyncApiDocument.operations ?? {}) as AsyncAPIV3.OperationObject[] + const [asyncOperation] = operationEntries + expect(asyncOperation.security).toBeUndefined() + }) + + it('should have security in operations channel servers', async () => { + const result = await buildPackageDefaultConfig('asyncapi/operations/server-security-operation') + const operations = Array.from(result.operations.values()) + expect(operations).toHaveLength(1) + + const [apiHubOperation] = operations + const asyncApiDocument: AsyncAPIV3.AsyncAPIObject = apiHubOperation.data + + const serverEntries = asyncApiDocument?.servers ? Object.values(asyncApiDocument.servers) as AsyncAPIV3.ServerObject[] : [] + const serverWithSecurity = serverEntries.find(server => server.security) + expect(serverWithSecurity).toBeDefined() + expect(serverWithSecurity!.security).toHaveLength(2) + }) + }) + + describe('Create operation spec tests', () => { + const OPERATION_1 = 'sendUserSignedUp' + const OPERATION_2 = 'sendUserSignedOut' + let baseDocument: AsyncAPIV3.AsyncAPIObject + + beforeAll(async () => { + baseDocument = await loadYamlFile('asyncapi/operations/base.yaml') + }) + + test('should select a single operation by key', async () => { + const result = createOperationSpec(baseDocument, OPERATION_1) + + expect(Object.keys(result.operations || {})).toEqual([OPERATION_1]) + expect(result.operations?.[OPERATION_1]).toEqual(baseDocument.operations?.[OPERATION_1]) + }) + + test('should preserve asyncapi and info', async () => { + const result = createOperationSpec(baseDocument, OPERATION_1) + + expect(result.asyncapi).toBe(baseDocument.asyncapi) + expect(result.info).toEqual(baseDocument.info) + }) + + test('should not include channels/servers/components by default', async () => { + const result = createOperationSpec(baseDocument, OPERATION_1) + + expect(result.channels).toBeUndefined() + expect(result.servers).toBeUndefined() + expect(result.components).toBeUndefined() + }) + + test('should select multiple operations by keys (array) and preserve requested order', async () => { + const result = createOperationSpec(baseDocument, [OPERATION_1, OPERATION_2]) + + expect(Object.keys(result.operations || {})).toEqual([OPERATION_1, OPERATION_2]) + expect(result.operations?.[OPERATION_1]).toEqual(baseDocument.operations?.[OPERATION_1]) + expect(result.operations?.[OPERATION_2]).toEqual(baseDocument.operations?.[OPERATION_2]) + }) + + test('should accept duplicated requested keys (same operation) without duplicating output', async () => { + const result = createOperationSpec(baseDocument, [OPERATION_1, OPERATION_1, OPERATION_2, OPERATION_1]) + + expect(Object.keys(result.operations || {})).toEqual([OPERATION_1, OPERATION_2]) + expect(result.operations?.[OPERATION_1]).toEqual(baseDocument.operations?.[OPERATION_1]) + expect(result.operations?.[OPERATION_2]).toEqual(baseDocument.operations?.[OPERATION_2]) + }) + + test('defaults asyncapi version to 3.0.0 when missing', async () => { + const document = cloneDocument(baseDocument) + delete (document as Partial).asyncapi + + const result = createOperationSpec(document, OPERATION_1) + expect(result.asyncapi).toBe('3.0.0') + }) + + test('throws when document has no operations', async () => { + const document = cloneDocument(baseDocument) + delete document.operations + + expect(() => createOperationSpec(document, OPERATION_1)).toThrow( + 'AsyncAPI document has no operations. Expected a non-empty "operations" object at document.operations.', + ) + }) + + test('throws when operation keys array is empty', async () => { + expect(() => createOperationSpec(baseDocument, [])).toThrow( + 'No operation keys provided. Pass a non-empty operation key string or a non-empty array of operation keys.', + ) + }) + + test('throws when the requested operation key is not found (string)', async () => { + expect(() => createOperationSpec(baseDocument, 'missing-operation')).toThrow('Operation "missing-operation" not found in document.operations') + }) + + test('throws when one or more requested operation keys are not found (array)', async () => { + expect(() => createOperationSpec(baseDocument, [OPERATION_1, 'missing-1', 'missing-2'])).toThrow( + 'Operations not found in document.operations: missing-1, missing-2', + ) + }) + + test('should inline referenced channels/servers/components when refsOnlyDocument has inline refs (manual refs)', async () => { + const refsOnlyDocument = { + operations: { [OPERATION_1]: {} }, + [INLINE_REFS_FLAG]: [ + '#/servers/amqp1', + '#/channels/userSignedUp', + '#/channels/userSignedUp/messages/UserSignedUp', + '#/components/messages/UserSignedUp', + '#/info/title', // should be ignored by patterns + ], + } as unknown as AsyncAPIV3.AsyncAPIObject + + const result = createOperationSpec(baseDocument, OPERATION_1, refsOnlyDocument) + + expect(baseDocument).toHaveProperty(['servers', 'amqp1'], result?.servers?.amqp1) + expect(baseDocument).toHaveProperty(['channels', 'userSignedUp'], result?.channels?.userSignedUp) + expect(baseDocument).toHaveProperty(['channels', 'userSignedUp', 'messages', 'UserSignedUp'], (result?.channels?.userSignedUp as AsyncAPIV3.ChannelObject)?.messages?.UserSignedUp) + expect(baseDocument).toHaveProperty(['components', 'messages', 'UserSignedUp'], result?.components?.messages?.UserSignedUp) + }) + + test('should skip inlining when refsOnlyDocument does not contain all requested operations', async () => { + const document = baseDocument + + const refsOnlyDocument = { + operations: { [OPERATION_1]: {} }, + [INLINE_REFS_FLAG]: ['#/servers/amqp1', '#/channels/userSignedUp', '#/components/messages/UserSignedUp'], + } as unknown as AsyncAPIV3.AsyncAPIObject + + const result = createOperationSpec(document, [OPERATION_1, OPERATION_2], refsOnlyDocument) + + expect(result.channels).toBeUndefined() + expect(result.servers).toBeUndefined() + expect(result.components).toBeUndefined() + }) + }) +}) diff --git a/test/asyncapi-validation.test.ts b/test/asyncapi-validation.test.ts index a479f1ec..dcc808ca 100644 --- a/test/asyncapi-validation.test.ts +++ b/test/asyncapi-validation.test.ts @@ -52,10 +52,19 @@ describe('AsyncAPI Validation', () => { }) test('error should include file context', async () => { + await runTest('invalid-critical-async.yaml') + }) + + test('should operation message belong to the specified channel', async () => { + const errorMessage = await runTest('operation-message-not-belong-to-specified-channel.yaml') + expect(errorMessage).toContain('Operation message does not belong to the specified channel') + }) + + async function runTest(fileId: string): Promise { const editor = await Editor.openProject('asyncapi-validation', asyncValidationPackage) try { - await editor.run({ files: [{ fileId: 'invalid-critical-async.yaml', publish: true, labels: [] }] }) + await editor.run({ files: [{ fileId, publish: true, labels: [] }] }) fail('Expected error to be thrown') } catch (error) { expect(error).toBeInstanceOf(Error) @@ -65,9 +74,10 @@ describe('AsyncAPI Validation', () => { expect(errorMessage).toContain('AsyncAPI validation') // Should contain file name from parseFile error wrapping - expect(errorMessage).toContain('invalid-critical-async.yaml') + expect(errorMessage).toContain(fileId) + return errorMessage } - }) + } }) //TODO: add tests for AsyncAPI document with non-critical errors/warnings diff --git a/test/declarative-changes-in-asyncapi-operation.test.ts b/test/declarative-changes-in-asyncapi-operation.test.ts new file mode 100644 index 00000000..9872d46b --- /dev/null +++ b/test/declarative-changes-in-asyncapi-operation.test.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2024-2025 NetCracker Technology Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { buildChangelogPackage, changesSummaryMatcher, numberOfImpactedOperationsMatcher } from './helpers' +import { ASYNCAPI_API_TYPE, BREAKING_CHANGE_TYPE } from '../src' + +describe.skip('Number of declarative changes in asyncapi operation test', () => { + test('Multiple use of one schema in a message payload', async () => { + const result = await buildChangelogPackage('declarative-changes-in-asyncapi-operation/case1') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('Multiple use of one schema inside another schema used in a message payload', async () => { + const result = await buildChangelogPackage('declarative-changes-in-asyncapi-operation/case2') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('Multiple use of one schema in both message payload and headers', async () => { + const result = await buildChangelogPackage('declarative-changes-in-asyncapi-operation/case3') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 2 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) + + test('Circular reference in message payload with a schema type change', async () => { + const result = await buildChangelogPackage('declarative-changes-in-asyncapi-operation/case4') + expect(result).toEqual(changesSummaryMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + expect(result).toEqual(numberOfImpactedOperationsMatcher({ [BREAKING_CHANGE_TYPE]: 1 }, ASYNCAPI_API_TYPE)) + }) +}) diff --git a/test/helpers/utils.ts b/test/helpers/utils.ts index 93129025..21dfcf75 100644 --- a/test/helpers/utils.ts +++ b/test/helpers/utils.ts @@ -25,15 +25,16 @@ import { BuildConfigFile, BuildResult, ChangeSummary, - EMPTY_CHANGE_SUMMARY, + EMPTY_CHANGE_SUMMARY, Labels, SERIALIZE_SYMBOL_STRING_MAPPING, VERSION_STATUS, } from '../../src' import { buildSchema, introspectionFromSchema } from 'graphql/utilities' import { LocalRegistry } from './registry' import { Editor } from './editor' -import { getFileExtension } from '../../src/utils' +import { getFileExtension, takeIfDefined } from '../../src/utils' import { deserialize } from '@netcracker/qubership-apihub-api-unifier' +import YAML from 'js-yaml' export const loadFileAsString = async (filePath: string, folder: string, fileName: string): Promise => { return (await loadFile(filePath, folder, fileName))?.text() ?? null @@ -270,6 +271,31 @@ export async function prepareChangelogDashboard( }) } +export async function buildPackageDefaultConfig( + packageId: string, + fileLabels?: Labels, + versionLabels?: Labels, +): Promise { + const portal = new LocalRegistry(packageId) + + await portal.publish(packageId, { + packageId: packageId, + version: 'v1', + metadata: { ...takeIfDefined({ versionLabels: versionLabels }) }, + files: [{ fileId: 'spec.yaml', ...takeIfDefined({ labels: fileLabels }), publish: true }], + }) + + const editor = new Editor(packageId, { + packageId: packageId, + version: 'v1', + status: VERSION_STATUS.RELEASE, + buildType: BUILD_TYPE.BUILD, + files: [{ fileId: 'spec.yaml'}], + }, {}, portal) + + return editor.run() +} + const invertMap = (map: Map): Map => { return new Map( [...map].map(([key, value]: [K, V]) => [value, key]), @@ -281,3 +307,12 @@ const DESERIALIZE_SYMBOL_STRING_MAPPING = invertMap(SERIALIZE_SYMBOL_STRING_MAPP export function deserializeDocument(serializedDocument: string): ApiDocument { return deserialize(serializedDocument, DESERIALIZE_SYMBOL_STRING_MAPPING) as ApiDocument } + +export const cloneDocument = (value: T): T => JSON.parse(JSON.stringify(value)) as T + +// Helper function to load YAML test files +export const loadYamlFile = async (relativePath: string): Promise => { + const filePath = path.join(process.cwd(), 'test/projects', relativePath) + const content = await fs.readFile(filePath, 'utf8') + return YAML.load(content) as T +} diff --git a/test/projects/asyncapi-changes/channel/add/after.yaml b/test/projects/asyncapi-changes/channel/add/after.yaml new file mode 100644 index 00000000..ddc69e2e --- /dev/null +++ b/test/projects/asyncapi-changes/channel/add/after.yaml @@ -0,0 +1,32 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: address1 + messages: + userData: + payload: + type: object + properties: + userId: + type: string + channel2: + address: address2 + messages: + userData: + payload: + type: object + properties: + userId: + type: string +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/channel1' + onUserUpdate: + action: receive + channel: + $ref: '#/channels/channel2' diff --git a/test/projects/asyncapi-changes/channel/add/before.yaml b/test/projects/asyncapi-changes/channel/add/before.yaml new file mode 100644 index 00000000..23cccf81 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/add/before.yaml @@ -0,0 +1,23 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: address1 + messages: + userData: + payload: + type: object + properties: + userId: + type: string +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/channel1' + onUserUpdate: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/channel/add/config.json b/test/projects/asyncapi-changes/channel/add/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/add/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/channel/change/after.yaml b/test/projects/asyncapi-changes/channel/change/after.yaml new file mode 100644 index 00000000..97bbce29 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/change/after.yaml @@ -0,0 +1,23 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + userSignup: + address: user/registration + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/userSignup' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/channel/change/before.yaml b/test/projects/asyncapi-changes/channel/change/before.yaml new file mode 100644 index 00000000..2ca34783 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/change/before.yaml @@ -0,0 +1,23 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + userSignup: + address: user/signup + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/userSignup' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/channel/change/config.json b/test/projects/asyncapi-changes/channel/change/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/change/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/channel/remove/after.yaml b/test/projects/asyncapi-changes/channel/remove/after.yaml new file mode 100644 index 00000000..2ca34783 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/remove/after.yaml @@ -0,0 +1,23 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + userSignup: + address: user/signup + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/userSignup' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/channel/remove/before.yaml b/test/projects/asyncapi-changes/channel/remove/before.yaml new file mode 100644 index 00000000..29d446a3 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/remove/before.yaml @@ -0,0 +1,38 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + userSignup: + address: user/signup + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' + userDelete: + address: user/delete + messages: + userDeleted: + $ref: '#/components/messages/UserDeleted' +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/userSignup' + onUserDelete: + action: receive + channel: + $ref: '#/channels/userDelete' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string + UserDeleted: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/channel/remove/config.json b/test/projects/asyncapi-changes/channel/remove/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/channel/remove/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/no-changes/after.yaml b/test/projects/asyncapi-changes/no-changes/after.yaml new file mode 100644 index 00000000..ae5554e6 --- /dev/null +++ b/test/projects/asyncapi-changes/no-changes/after.yaml @@ -0,0 +1,19 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/no-changes/before.yaml b/test/projects/asyncapi-changes/no-changes/before.yaml new file mode 100644 index 00000000..ae5554e6 --- /dev/null +++ b/test/projects/asyncapi-changes/no-changes/before.yaml @@ -0,0 +1,19 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/no-changes/config.json b/test/projects/asyncapi-changes/no-changes/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/no-changes/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/operation/add-multiple/after.yaml b/test/projects/asyncapi-changes/operation/add-multiple/after.yaml new file mode 100644 index 00000000..41180c9e --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add-multiple/after.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' + onSend: + action: send + channel: + $ref: '#/channels/channel1' + onUpdate: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/operation/add-multiple/before.yaml b/test/projects/asyncapi-changes/operation/add-multiple/before.yaml new file mode 100644 index 00000000..ae5554e6 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add-multiple/before.yaml @@ -0,0 +1,19 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/operation/add-multiple/config.json b/test/projects/asyncapi-changes/operation/add-multiple/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add-multiple/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/operation/add-remove/after.yaml b/test/projects/asyncapi-changes/operation/add-remove/after.yaml new file mode 100644 index 00000000..6dcf02ec --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add-remove/after.yaml @@ -0,0 +1,23 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' + onUpdate: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/operation/add-remove/before.yaml b/test/projects/asyncapi-changes/operation/add-remove/before.yaml new file mode 100644 index 00000000..7c2f5664 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add-remove/before.yaml @@ -0,0 +1,23 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' + onSend: + action: send + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/operation/add-remove/config.json b/test/projects/asyncapi-changes/operation/add-remove/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add-remove/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/operation/add/after.yaml b/test/projects/asyncapi-changes/operation/add/after.yaml new file mode 100644 index 00000000..a6cf2e39 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add/after.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + $ref: '#/components/messages/message1' +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' + onSend: + action: send + channel: + $ref: '#/channels/channel1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + diff --git a/test/projects/asyncapi-changes/operation/add/before.yaml b/test/projects/asyncapi-changes/operation/add/before.yaml new file mode 100644 index 00000000..c82c5bc6 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add/before.yaml @@ -0,0 +1,24 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + $ref: '#/components/messages/message1' +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' +components: + messages: + message1: + payload: + type: object + properties: + userId: + type: string + diff --git a/test/projects/asyncapi-changes/operation/add/config.json b/test/projects/asyncapi-changes/operation/add/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/add/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/operation/change/after.yaml b/test/projects/asyncapi-changes/operation/change/after.yaml new file mode 100644 index 00000000..885583fb --- /dev/null +++ b/test/projects/asyncapi-changes/operation/change/after.yaml @@ -0,0 +1,22 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + title: title1 + channel: + $ref: '#/channels/channel1' + + diff --git a/test/projects/asyncapi-changes/operation/change/before.yaml b/test/projects/asyncapi-changes/operation/change/before.yaml new file mode 100644 index 00000000..2dea9d3f --- /dev/null +++ b/test/projects/asyncapi-changes/operation/change/before.yaml @@ -0,0 +1,22 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + title: title + channel: + $ref: '#/channels/channel1' + + diff --git a/test/projects/asyncapi-changes/operation/change/config.json b/test/projects/asyncapi-changes/operation/change/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/change/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/operation/remove/after.yaml b/test/projects/asyncapi-changes/operation/remove/after.yaml new file mode 100644 index 00000000..0f82f05b --- /dev/null +++ b/test/projects/asyncapi-changes/operation/remove/after.yaml @@ -0,0 +1,21 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' + + diff --git a/test/projects/asyncapi-changes/operation/remove/before.yaml b/test/projects/asyncapi-changes/operation/remove/before.yaml new file mode 100644 index 00000000..1183c5b9 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/remove/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' + onSend: + action: send + channel: + $ref: '#/channels/channel1' + + diff --git a/test/projects/asyncapi-changes/operation/remove/config.json b/test/projects/asyncapi-changes/operation/remove/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/remove/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/operation/rename/after.yaml b/test/projects/asyncapi-changes/operation/rename/after.yaml new file mode 100644 index 00000000..4d4e2c7c --- /dev/null +++ b/test/projects/asyncapi-changes/operation/rename/after.yaml @@ -0,0 +1,21 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onNewReceive: + action: receive + channel: + $ref: '#/channels/channel1' + + diff --git a/test/projects/asyncapi-changes/operation/rename/before.yaml b/test/projects/asyncapi-changes/operation/rename/before.yaml new file mode 100644 index 00000000..0f82f05b --- /dev/null +++ b/test/projects/asyncapi-changes/operation/rename/before.yaml @@ -0,0 +1,21 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: channel1 + messages: + userSignedUp: + payload: + type: object + properties: + userId: + type: string +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/channel1' + + diff --git a/test/projects/asyncapi-changes/operation/rename/config.json b/test/projects/asyncapi-changes/operation/rename/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/operation/rename/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/server/add-root/after.yaml b/test/projects/asyncapi-changes/server/add-root/after.yaml new file mode 100644 index 00000000..377a203e --- /dev/null +++ b/test/projects/asyncapi-changes/server/add-root/after.yaml @@ -0,0 +1,23 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + protocol: amqp + host: api.example.com +channels: + channel1: + address: address1 + messages: + userData: + payload: + type: object + properties: + userId: + type: string +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/server/add-root/before.yaml b/test/projects/asyncapi-changes/server/add-root/before.yaml new file mode 100644 index 00000000..536a8c2f --- /dev/null +++ b/test/projects/asyncapi-changes/server/add-root/before.yaml @@ -0,0 +1,19 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: address1 + messages: + userData: + payload: + type: object + properties: + userId: + type: string +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/server/add-root/config.json b/test/projects/asyncapi-changes/server/add-root/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/server/add-root/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/server/add/after.yaml b/test/projects/asyncapi-changes/server/add/after.yaml new file mode 100644 index 00000000..d6a32d88 --- /dev/null +++ b/test/projects/asyncapi-changes/server/add/after.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + protocol: amqp + host: api.example.com +channels: + channel1: + address: address1 + servers: + - $ref: '#/servers/production' + messages: + userData: + payload: + type: object + properties: + userId: + type: string +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/server/add/before.yaml b/test/projects/asyncapi-changes/server/add/before.yaml new file mode 100644 index 00000000..377a203e --- /dev/null +++ b/test/projects/asyncapi-changes/server/add/before.yaml @@ -0,0 +1,23 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + protocol: amqp + host: api.example.com +channels: + channel1: + address: address1 + messages: + userData: + payload: + type: object + properties: + userId: + type: string +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/server/add/config.json b/test/projects/asyncapi-changes/server/add/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/server/add/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/server/change-root/after.yaml b/test/projects/asyncapi-changes/server/change-root/after.yaml new file mode 100644 index 00000000..4cbdf277 --- /dev/null +++ b/test/projects/asyncapi-changes/server/change-root/after.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + host: newexample.com:9092 + protocol: kafka + description: New production server +channels: + userSignup: + address: user/signup + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/userSignup' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/server/change-root/before.yaml b/test/projects/asyncapi-changes/server/change-root/before.yaml new file mode 100644 index 00000000..91314713 --- /dev/null +++ b/test/projects/asyncapi-changes/server/change-root/before.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + host: example.com:9092 + protocol: kafka +channels: + userSignup: + address: user/signup + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/userSignup' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/server/change-root/config.json b/test/projects/asyncapi-changes/server/change-root/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/server/change-root/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/server/change/after.yaml b/test/projects/asyncapi-changes/server/change/after.yaml new file mode 100644 index 00000000..3626cf02 --- /dev/null +++ b/test/projects/asyncapi-changes/server/change/after.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + host: example.com:9093 + protocol: kafka + description: Updated server +channels: + userSignup: + address: user/signup + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/userSignup' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/server/change/before.yaml b/test/projects/asyncapi-changes/server/change/before.yaml new file mode 100644 index 00000000..91314713 --- /dev/null +++ b/test/projects/asyncapi-changes/server/change/before.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + host: example.com:9092 + protocol: kafka +channels: + userSignup: + address: user/signup + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/userSignup' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string diff --git a/test/projects/asyncapi-changes/server/change/config.json b/test/projects/asyncapi-changes/server/change/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/server/change/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/server/remove-root/after.yaml b/test/projects/asyncapi-changes/server/remove-root/after.yaml new file mode 100644 index 00000000..536a8c2f --- /dev/null +++ b/test/projects/asyncapi-changes/server/remove-root/after.yaml @@ -0,0 +1,19 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + channel1: + address: address1 + messages: + userData: + payload: + type: object + properties: + userId: + type: string +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/server/remove-root/before.yaml b/test/projects/asyncapi-changes/server/remove-root/before.yaml new file mode 100644 index 00000000..377a203e --- /dev/null +++ b/test/projects/asyncapi-changes/server/remove-root/before.yaml @@ -0,0 +1,23 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + protocol: amqp + host: api.example.com +channels: + channel1: + address: address1 + messages: + userData: + payload: + type: object + properties: + userId: + type: string +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/server/remove-root/config.json b/test/projects/asyncapi-changes/server/remove-root/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/server/remove-root/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/server/remove/after.yaml b/test/projects/asyncapi-changes/server/remove/after.yaml new file mode 100644 index 00000000..377a203e --- /dev/null +++ b/test/projects/asyncapi-changes/server/remove/after.yaml @@ -0,0 +1,23 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + protocol: amqp + host: api.example.com +channels: + channel1: + address: address1 + messages: + userData: + payload: + type: object + properties: + userId: + type: string +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/server/remove/before.yaml b/test/projects/asyncapi-changes/server/remove/before.yaml new file mode 100644 index 00000000..d6a32d88 --- /dev/null +++ b/test/projects/asyncapi-changes/server/remove/before.yaml @@ -0,0 +1,25 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +servers: + production: + protocol: amqp + host: api.example.com +channels: + channel1: + address: address1 + servers: + - $ref: '#/servers/production' + messages: + userData: + payload: + type: object + properties: + userId: + type: string +operations: + onUserSignup: + action: receive + channel: + $ref: '#/channels/channel1' diff --git a/test/projects/asyncapi-changes/server/remove/config.json b/test/projects/asyncapi-changes/server/remove/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/server/remove/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-changes/tags/after.yaml b/test/projects/asyncapi-changes/tags/after.yaml new file mode 100644 index 00000000..3d3c7f00 --- /dev/null +++ b/test/projects/asyncapi-changes/tags/after.yaml @@ -0,0 +1,98 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + userSignup: + address: user/signup + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' + userUpdate: + address: user/update + messages: + userUpdated: + $ref: '#/components/messages/UserUpdated' + orderCreate: + address: order/create + messages: + orderCreated: + $ref: '#/components/messages/OrderCreated' + orderUpdate: + address: order/update + messages: + orderUpdated: + $ref: '#/components/messages/OrderUpdated' + orderDelete: + address: order/delete + messages: + orderDeleted: + $ref: '#/components/messages/OrderDeleted' +operations: + added1: + action: receive + channel: + $ref: '#/channels/userSignup' + tags: + - name: sameTagInDifferentChannels1 + - name: sameTagInDifferentChannels3 + added2: + action: send + channel: + $ref: '#/channels/userUpdate' + tags: + - name: sameTagInDifferentChannels1 + - name: sameTagInDifferentChannels3 + - name: sameTagInDifferentSiblings2 + added3: + action: receive + channel: + $ref: '#/channels/orderCreate' + tags: + - name: sameTagInDifferentSiblings2 + changed1: + action: send + channel: + $ref: '#/channels/orderUpdate' + tags: + - name: sameTagInOperationSiblings1 + - name: sameTagInDifferentSiblings3 + - name: tag + changed2: + action: receive + channel: + $ref: '#/channels/orderDelete' + tags: + - name: sameTagInDifferentSiblings3 +components: + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string + UserUpdated: + payload: + type: object + properties: + userId: + type: string + OrderCreated: + payload: + type: object + properties: + orderId: + type: string + OrderUpdated: + payload: + type: object + properties: + orderId: + type: string + OrderDeleted: + payload: + type: object + properties: + orderId: + type: string diff --git a/test/projects/asyncapi-changes/tags/before.yaml b/test/projects/asyncapi-changes/tags/before.yaml new file mode 100644 index 00000000..f04171f2 --- /dev/null +++ b/test/projects/asyncapi-changes/tags/before.yaml @@ -0,0 +1,98 @@ +asyncapi: 3.0.0 +info: + title: Test AsyncAPI + version: 1.0.0 +channels: + userSignup: + address: user/signup + messages: + userSignedUp: + $ref: '#/components/messages/UserSignedUp' + userUpdate: + address: user/update + messages: + userUpdated: + $ref: '#/components/messages/UserUpdated' + orderCreate: + address: order/create + messages: + orderCreated: + $ref: '#/components/messages/OrderCreated' + orderUpdate: + address: order/update + messages: + orderUpdated: + $ref: '#/components/messages/OrderUpdated' + orderDelete: + address: order/delete + messages: + orderDeleted: + $ref: '#/components/messages/OrderDeleted' +operations: + removed1: + action: receive + channel: + $ref: '#/channels/userSignup' + tags: + - name: sameTagInDifferentChannels1 + - name: sameTagInDifferentChannels2 + removed2: + action: send + channel: + $ref: '#/channels/userUpdate' + tags: + - name: sameTagInDifferentChannels1 + - name: sameTagInDifferentChannels2 + - name: sameTagInDifferentSiblings1 + removed3: + action: receive + channel: + $ref: '#/channels/orderCreate' + tags: + - name: sameTagInDifferentSiblings1 + changed1: + action: send + channel: + $ref: '#/channels/orderUpdate' + tags: + - name: sameTagInOperationSiblings1 + - name: tag + - name: missingByDesign + changed2: + action: receive + channel: + $ref: '#/channels/orderDelete' + tags: + - name: missingByDesign +components: + messages: + UserSignedUp: + payload: + type: object + properties: + userId: + type: string + UserUpdated: + payload: + type: object + properties: + userId: + type: string + OrderCreated: + payload: + type: object + properties: + orderId: + type: string + OrderUpdated: + payload: + type: object + properties: + orderId: + type: string + OrderDeleted: + payload: + type: object + properties: + orderId: + type: string \ No newline at end of file diff --git a/test/projects/asyncapi-changes/tags/config.json b/test/projects/asyncapi-changes/tags/config.json new file mode 100644 index 00000000..f7fdb6c9 --- /dev/null +++ b/test/projects/asyncapi-changes/tags/config.json @@ -0,0 +1,9 @@ +{ + "files": [ + { + "fileId": "before.yaml", + "publish": true, + "labels": [] + } + ] +} diff --git a/test/projects/asyncapi-validation/operation-message-not-belong-to-specified-channel.yaml b/test/projects/asyncapi-validation/operation-message-not-belong-to-specified-channel.yaml new file mode 100644 index 00000000..8e817aba --- /dev/null +++ b/test/projects/asyncapi-validation/operation-message-not-belong-to-specified-channel.yaml @@ -0,0 +1,33 @@ +asyncapi: 3.0.0 + +info: + title: Broken example + version: 1.0.0 + +channels: + user/events: + address: user.events + messages: + UserCreated: + payload: + type: object + properties: + id: + type: string + +operations: + publishUserDeleted: + action: send + channel: + $ref: '#/channels/user~1events' + messages: + - $ref: '#/components/messages/UserDeleted' + +components: + messages: + UserDeleted: + payload: + type: object + properties: + id: + type: string diff --git a/test/projects/asyncapi/api-kind/base/spec.yaml b/test/projects/asyncapi/api-kind/base/spec.yaml new file mode 100644 index 00000000..50863103 --- /dev/null +++ b/test/projects/asyncapi/api-kind/base/spec.yaml @@ -0,0 +1,98 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +channels: + channel: + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' + channel-bwc: + x-api-kind: BWC + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' + channel-no-bwc: + x-api-kind: no-BWC + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + operation: + action: send + channel: + $ref: '#/channels/channel' + messages: + - $ref: '#/channels/channel/messages/UserSignedUp' + operation-with-cannel-bwc: + action: send + channel: + $ref: '#/channels/channel-bwc' + messages: + - $ref: '#/channels/channel-bwc/messages/UserSignedUp' + operation-with-cannel-no-bwc: + action: send + channel: + $ref: '#/channels/channel-no-bwc' + messages: + - $ref: '#/channels/channel-no-bwc/messages/UserSignedUp' + operation-bwc: + x-api-kind: BWC + action: send + channel: + $ref: '#/channels/channel' + messages: + - $ref: '#/channels/channel/messages/UserSignedUp' + operation-no-bwc: + x-api-kind: no-BWC + action: send + channel: + $ref: '#/channels/channel' + messages: + - $ref: '#/channels/channel/messages/UserSignedUp' + operation-bwc-with-channel-bwc: + x-api-kind: BWC + action: send + channel: + $ref: '#/channels/channel-bwc' + messages: + - $ref: '#/channels/channel-bwc/messages/UserSignedUp' + operation-bwc-with-channel-no-bwc: + x-api-kind: BWC + action: send + channel: + $ref: '#/channels/channel-no-bwc' + messages: + - $ref: '#/channels/channel-no-bwc/messages/UserSignedUp' + operation-no-bwc-with-channel-bwc: + x-api-kind: no-BWC + action: send + channel: + $ref: '#/channels/channel-bwc' + messages: + - $ref: '#/channels/channel-bwc/messages/UserSignedUp' + operation-no-bwc-with-channel-no-bwc: + x-api-kind: no-BWC + action: send + channel: + $ref: '#/channels/channel-no-bwc' + messages: + - $ref: '#/channels/channel-no-bwc/messages/UserSignedUp' +components: + messages: + UserSignedUp: + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user \ No newline at end of file diff --git a/test/projects/asyncapi/api-kind/share-channel-api-kind/spec.yaml b/test/projects/asyncapi/api-kind/share-channel-api-kind/spec.yaml new file mode 100644 index 00000000..96783f07 --- /dev/null +++ b/test/projects/asyncapi/api-kind/share-channel-api-kind/spec.yaml @@ -0,0 +1,39 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +channels: + channel: + address: user/signedup + x-api-kind: no-BWC + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + operation1: + action: send + channel: + $ref: '#/channels/channel' + messages: + - $ref: '#/channels/channel/messages/UserSignedUp' + operation2: + action: send + channel: + $ref: '#/channels/channel' + messages: + - $ref: '#/channels/channel/messages/UserSignedUp' +components: + messages: + UserSignedUp: + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user \ No newline at end of file diff --git a/test/projects/asyncapi/deprecated/channel/spec.yaml b/test/projects/asyncapi/deprecated/channel/spec.yaml new file mode 100644 index 00000000..c84e88e3 --- /dev/null +++ b/test/projects/asyncapi/deprecated/channel/spec.yaml @@ -0,0 +1,33 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +channels: + userSignedUp: + x-deprecated: true + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + sendUserSignedUp: + action: send + channel: + $ref: '#/channels/userSignedUp' + messages: + - $ref: '#/channels/userSignedUp/messages/UserSignedUp' +components: + messages: + UserSignedUp: + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user \ No newline at end of file diff --git a/test/projects/asyncapi/deprecated/messages/spec.yaml b/test/projects/asyncapi/deprecated/messages/spec.yaml new file mode 100644 index 00000000..a3747e65 --- /dev/null +++ b/test/projects/asyncapi/deprecated/messages/spec.yaml @@ -0,0 +1,33 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +channels: + userSignedUp: + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + sendUserSignedUp: + action: send + channel: + $ref: '#/channels/userSignedUp' + messages: + - $ref: '#/channels/userSignedUp/messages/UserSignedUp' +components: + messages: + UserSignedUp: + x-deprecated: true + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user \ No newline at end of file diff --git a/test/projects/asyncapi/deprecated/schemas/spec.yaml b/test/projects/asyncapi/deprecated/schemas/spec.yaml new file mode 100644 index 00000000..bf7222f8 --- /dev/null +++ b/test/projects/asyncapi/deprecated/schemas/spec.yaml @@ -0,0 +1,36 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +channels: + userSignedUp: + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + sendUserSignedUp: + action: send + channel: + $ref: '#/channels/userSignedUp' + messages: + - $ref: '#/channels/userSignedUp/messages/UserSignedUp' +components: + messages: + UserSignedUp: + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + $ref: '#/components/schemas/DeprecatedEmail' + schemas: + DeprecatedEmail: + type: string + format: email + description: Email of the user + deprecated: true diff --git a/test/projects/asyncapi/info/external-documentation/refs/spec.yaml b/test/projects/asyncapi/info/external-documentation/refs/spec.yaml new file mode 100644 index 00000000..e82d6efe --- /dev/null +++ b/test/projects/asyncapi/info/external-documentation/refs/spec.yaml @@ -0,0 +1,33 @@ +asyncapi: 3.0.0 + +info: + title: External documentation + version: 1.0.0 + description: AsyncAPI + externalDocs: + $ref: '#/components/externalDocs/myDocs' + +servers: + production: + host: broker.mycompany.com + protocol: amqp + +channels: + channels1: + messages: + testMessage: + name: test + payload: + type: object + +operations: + operation1: + action: send + channel: + $ref: '#/channels/channels1' + +components: + externalDocs: + myDocs: + description: Ref + url: https://example.com/docs \ No newline at end of file diff --git a/test/projects/asyncapi/info/external-documentation/simple/spec.yaml b/test/projects/asyncapi/info/external-documentation/simple/spec.yaml new file mode 100644 index 00000000..a60603ca --- /dev/null +++ b/test/projects/asyncapi/info/external-documentation/simple/spec.yaml @@ -0,0 +1,28 @@ +asyncapi: 3.0.0 + +info: + title: External documentation + version: 1.0.0 + description: AsyncAPI + externalDocs: + description: Simple + url: https://example.com/docs + +servers: + production: + host: broker.mycompany.com + protocol: amqp + +channels: + channels1: + messages: + testMessage: + name: test + payload: + type: object + +operations: + operation1: + action: send + channel: + $ref: '#/channels/channels1' diff --git a/test/projects/asyncapi/info/tags/mixed/spec.yaml b/test/projects/asyncapi/info/tags/mixed/spec.yaml new file mode 100644 index 00000000..957c75b8 --- /dev/null +++ b/test/projects/asyncapi/info/tags/mixed/spec.yaml @@ -0,0 +1,35 @@ +asyncapi: 3.0.0 + +info: + title: Tags + version: 1.0.0 + description: AsyncAPI example with tags + tags: + - $ref: '#/components/tags/ref_tag1' + - name: simple_tag2 + description: Description for simple_tag2 + +servers: + production: + host: broker.mycompany.com + protocol: amqp + +channels: + channels1: + messages: + testMessage: + name: test + payload: + type: object + +operations: + operation1: + action: send + channel: + $ref: '#/channels/channels1' + +components: + tags: + ref_tag1: + name: ref_tag1 + description: Description for ref_tag1 diff --git a/test/projects/asyncapi/info/tags/refs/spec.yaml b/test/projects/asyncapi/info/tags/refs/spec.yaml new file mode 100644 index 00000000..63d8de7b --- /dev/null +++ b/test/projects/asyncapi/info/tags/refs/spec.yaml @@ -0,0 +1,38 @@ +asyncapi: 3.0.0 + +info: + title: Tags + version: 1.0.0 + description: AsyncAPI example with tag references + tags: + - $ref: '#/components/tags/ref_tag1' + - $ref: '#/components/tags/ref_tag2' + +servers: + production: + host: broker.mycompany.com + protocol: amqp + +channels: + channels1: + messages: + testMessage: + name: test + payload: + type: object + +operations: + operation1: + action: send + channel: + $ref: '#/channels/channels1' + +components: + tags: + ref_tag1: + name: ref_tag1 + description: Description for ref_tag1 + + ref_tag2: + name: ref_tag2 + description: Description for ref_tag2 diff --git a/test/projects/asyncapi/info/tags/simple/spec.yaml b/test/projects/asyncapi/info/tags/simple/spec.yaml new file mode 100644 index 00000000..574b82c7 --- /dev/null +++ b/test/projects/asyncapi/info/tags/simple/spec.yaml @@ -0,0 +1,30 @@ +asyncapi: 3.0.0 + +info: + title: Tags + version: 1.0.0 + description: AsyncAPI example with tags + tags: + - name: simple_tag1 + description: Description for simple_tag1 + - name: simple_tag2 + description: Description for simple_tag2 + +servers: + production: + host: broker.mycompany.com + protocol: amqp + +channels: + channels1: + messages: + testMessage: + name: test + payload: + type: object + +operations: + operation1: + action: send + channel: + $ref: '#/channels/channels1' diff --git a/test/projects/asyncapi/operations/base.yaml b/test/projects/asyncapi/operations/base.yaml new file mode 100644 index 00000000..46cc5627 --- /dev/null +++ b/test/projects/asyncapi/operations/base.yaml @@ -0,0 +1,63 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +servers: + amqp1: + host: broker-amqp.example.com + protocol: amqp + kafka1: + host: broker-kafka.example.com + protocol: kafka +channels: + userSignedUp: + address: user/signedup + servers: + - $ref: '#/servers/amqp1' + - $ref: '#/servers/kafka1' + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' + userSignedOut: + address: user/signedOut + servers: + - $ref: '#/servers/amqp1' + messages: + UserSignedOut: + $ref: '#/components/messages/UserSignedOut' +operations: + sendUserSignedUp: + action: send + channel: + $ref: '#/channels/userSignedUp' + messages: + - $ref: '#/channels/userSignedUp/messages/UserSignedUp' + sendUserSignedOut: + action: receive + channel: + $ref: '#/channels/userSignedOut' + messages: + - $ref: '#/channels/userSignedOut/messages/UserSignedOut' +components: + messages: + UserSignedUp: + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user + UserSignedOut: + title: User Signed Out + payload: + type: object + properties: + displayName: + type: string + description: Name of the user diff --git a/test/projects/asyncapi/operations/broken-operation/spec.yaml b/test/projects/asyncapi/operations/broken-operation/spec.yaml new file mode 100644 index 00000000..b97f6807 --- /dev/null +++ b/test/projects/asyncapi/operations/broken-operation/spec.yaml @@ -0,0 +1,30 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +channels: + userSignedup: + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + brokenOperation: + action: send + channel: + $ref: '#/channels/userSignedup' +components: + messages: + UserSignedUp: + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user diff --git a/test/projects/asyncapi/operations/multiple-operations/spec.yaml b/test/projects/asyncapi/operations/multiple-operations/spec.yaml new file mode 100644 index 00000000..76ec7aad --- /dev/null +++ b/test/projects/asyncapi/operations/multiple-operations/spec.yaml @@ -0,0 +1,53 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +channels: + userSignedup: + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' + UserSignedUpV2: + $ref: '#/components/messages/UserSignedUpV2' +operations: + sendUserSignedup: + action: send + channel: + $ref: '#/channels/userSignedup' + messages: + - $ref: '#/channels/userSignedup/messages/UserSignedUp' + receiveUserSignedup: + action: receive + channel: + $ref: '#/channels/userSignedup' + messages: + - $ref: '#/channels/userSignedup/messages/UserSignedUp' + - $ref: '#/channels/userSignedup/messages/UserSignedUpV2' +components: + messages: + UserSignedUp: + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user + UserSignedUpV2: + title: User Signed Up (v2) + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user \ No newline at end of file diff --git a/test/projects/asyncapi/operations/security-operation/spec.yaml b/test/projects/asyncapi/operations/security-operation/spec.yaml new file mode 100644 index 00000000..5ec7b423 --- /dev/null +++ b/test/projects/asyncapi/operations/security-operation/spec.yaml @@ -0,0 +1,53 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +servers: + amqp1: + host: broker-amqp.example.com + protocol: amqp +channels: + userSignedup: + address: user/signedup + servers: + - $ref: '#/servers/amqp1' + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + sendUserSignedup: + action: send + channel: + $ref: '#/channels/userSignedup' + security: + - $ref: '#/components/securitySchemes/oauth2' + - $ref: '#/components/securitySchemes/apiKey' + messages: + - $ref: '#/channels/userSignedup/messages/UserSignedUp' +components: + messages: + UserSignedUp: + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user + securitySchemes: + oauth2: + type: oauth2 + flows: + implicit: + authorizationUrl: https://example.com/oauth/authorize + availableScopes: + read:users: Read users + apiKey: + type: httpApiKey + name: api_key + in: header diff --git a/test/projects/asyncapi/operations/server-security-operation/spec.yaml b/test/projects/asyncapi/operations/server-security-operation/spec.yaml new file mode 100644 index 00000000..85032f4c --- /dev/null +++ b/test/projects/asyncapi/operations/server-security-operation/spec.yaml @@ -0,0 +1,53 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +servers: + amqp1: + host: broker-amqp.example.com + protocol: amqp + security: + - $ref: '#/components/securitySchemes/oauth2' + - $ref: '#/components/securitySchemes/apiKey' +channels: + userSignedup: + address: user/signedup + servers: + - $ref: '#/servers/amqp1' + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + sendUserSignedup: + action: send + channel: + $ref: '#/channels/userSignedup' + messages: + - $ref: '#/channels/userSignedup/messages/UserSignedUp' +components: + messages: + UserSignedUp: + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user + securitySchemes: + oauth2: + type: oauth2 + flows: + implicit: + authorizationUrl: https://example.com/oauth/authorize + availableScopes: + read:users: Read users + apiKey: + type: httpApiKey + name: api_key + in: header diff --git a/test/projects/asyncapi/operations/single-operation/spec.yaml b/test/projects/asyncapi/operations/single-operation/spec.yaml new file mode 100644 index 00000000..92138868 --- /dev/null +++ b/test/projects/asyncapi/operations/single-operation/spec.yaml @@ -0,0 +1,42 @@ +asyncapi: 3.0.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +servers: + amqp1: + host: broker-amqp.example.com + protocol: amqp + kafka1: + host: broker-kafka.example.com + protocol: kafka +channels: + userSignedup: + address: user/signedup + servers: + - $ref: '#/servers/amqp1' + - $ref: '#/servers/kafka1' + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + sendUserSignedup: + action: send + channel: + $ref: '#/channels/userSignedup' + messages: + - $ref: '#/channels/userSignedup/messages/UserSignedUp' +components: + messages: + UserSignedUp: + title: User Signed Up + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user \ No newline at end of file diff --git a/test/projects/comparison-internal-documents/case3/after.gql b/test/projects/comparison-internal-documents/case3/after.gql new file mode 100644 index 00000000..47456efb --- /dev/null +++ b/test/projects/comparison-internal-documents/case3/after.gql @@ -0,0 +1,4 @@ +type Query { + fruits: String + fruitsByColor: String +} diff --git a/test/projects/comparison-internal-documents/case3/before.gql b/test/projects/comparison-internal-documents/case3/before.gql new file mode 100644 index 00000000..31ee120d --- /dev/null +++ b/test/projects/comparison-internal-documents/case3/before.gql @@ -0,0 +1,3 @@ +type Query { + fruits: String +} diff --git a/test/projects/declarative-changes-in-asyncapi-operation/case1/after.yaml b/test/projects/declarative-changes-in-asyncapi-operation/case1/after.yaml new file mode 100644 index 00000000..af183111 --- /dev/null +++ b/test/projects/declarative-changes-in-asyncapi-operation/case1/after.yaml @@ -0,0 +1,35 @@ +asyncapi: 3.0.0 +info: + title: Sample AsyncAPI + version: 1.0.0 +channels: + salesOrder: + address: salesOrder + messages: + salesOrderUpdated: + $ref: '#/components/messages/SalesOrderUpdated' +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/salesOrder' +components: + messages: + SalesOrderUpdated: + payload: + type: object + properties: + oneTimeDiscount: + $ref: '#/components/schemas/MonetaryValue' + oneTimeTotalWithTax: + $ref: '#/components/schemas/MonetaryValue' + oneTimeTotalWithTaxDiscounted: + $ref: '#/components/schemas/MonetaryValue' + schemas: + MonetaryValue: + type: object + properties: + value: + type: string #! + currency: + type: string diff --git a/test/projects/declarative-changes-in-asyncapi-operation/case1/before.yaml b/test/projects/declarative-changes-in-asyncapi-operation/case1/before.yaml new file mode 100644 index 00000000..0845f1a4 --- /dev/null +++ b/test/projects/declarative-changes-in-asyncapi-operation/case1/before.yaml @@ -0,0 +1,35 @@ +asyncapi: 3.0.0 +info: + title: Sample AsyncAPI + version: 1.0.0 +channels: + salesOrder: + address: salesOrder + messages: + salesOrderUpdated: + $ref: '#/components/messages/SalesOrderUpdated' +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/salesOrder' +components: + messages: + SalesOrderUpdated: + payload: + type: object + properties: + oneTimeDiscount: + $ref: '#/components/schemas/MonetaryValue' + oneTimeTotalWithTax: + $ref: '#/components/schemas/MonetaryValue' + oneTimeTotalWithTaxDiscounted: + $ref: '#/components/schemas/MonetaryValue' + schemas: + MonetaryValue: + type: object + properties: + value: + type: number #! + currency: + type: string diff --git a/test/projects/declarative-changes-in-asyncapi-operation/case2/after.yaml b/test/projects/declarative-changes-in-asyncapi-operation/case2/after.yaml new file mode 100644 index 00000000..e2abd79f --- /dev/null +++ b/test/projects/declarative-changes-in-asyncapi-operation/case2/after.yaml @@ -0,0 +1,37 @@ +asyncapi: 3.0.0 +info: + title: Sample AsyncAPI + version: 1.0.0 +channels: + price: + address: price + messages: + priceUpdated: + $ref: '#/components/messages/PriceUpdated' +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/price' +components: + messages: + PriceUpdated: + payload: + $ref: '#/components/schemas/Price' + schemas: + Price: + type: object + properties: + oneTimeDiscount: + $ref: '#/components/schemas/MonetaryValue' + oneTimeTotalWithTax: + $ref: '#/components/schemas/MonetaryValue' + oneTimeTotalWithTaxDiscounted: + $ref: '#/components/schemas/MonetaryValue' + MonetaryValue: + type: object + properties: + value: + type: string #! + currency: + type: string diff --git a/test/projects/declarative-changes-in-asyncapi-operation/case2/before.yaml b/test/projects/declarative-changes-in-asyncapi-operation/case2/before.yaml new file mode 100644 index 00000000..369dd430 --- /dev/null +++ b/test/projects/declarative-changes-in-asyncapi-operation/case2/before.yaml @@ -0,0 +1,37 @@ +asyncapi: 3.0.0 +info: + title: Sample AsyncAPI + version: 1.0.0 +channels: + price: + address: price + messages: + priceUpdated: + $ref: '#/components/messages/PriceUpdated' +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/price' +components: + messages: + PriceUpdated: + payload: + $ref: '#/components/schemas/Price' + schemas: + Price: + type: object + properties: + oneTimeDiscount: + $ref: '#/components/schemas/MonetaryValue' + oneTimeTotalWithTax: + $ref: '#/components/schemas/MonetaryValue' + oneTimeTotalWithTaxDiscounted: + $ref: '#/components/schemas/MonetaryValue' + MonetaryValue: + type: object + properties: + value: + type: number #! + currency: + type: string diff --git a/test/projects/declarative-changes-in-asyncapi-operation/case3/after.yaml b/test/projects/declarative-changes-in-asyncapi-operation/case3/after.yaml new file mode 100644 index 00000000..6b5ae379 --- /dev/null +++ b/test/projects/declarative-changes-in-asyncapi-operation/case3/after.yaml @@ -0,0 +1,31 @@ +asyncapi: 3.0.0 +info: + title: Sample AsyncAPI + version: 1.0.0 +channels: + correlated: + address: correlated + messages: + correlatedEvent: + $ref: '#/components/messages/CorrelatedEvent' +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/correlated' +components: + messages: + CorrelatedEvent: + headers: + $ref: '#/components/schemas/CorrelationHeaders' + payload: + type: object + properties: + correlation: + $ref: '#/components/schemas/CorrelationHeaders' + schemas: + CorrelationHeaders: + type: object + properties: + correlationId: + type: number #! diff --git a/test/projects/declarative-changes-in-asyncapi-operation/case3/before.yaml b/test/projects/declarative-changes-in-asyncapi-operation/case3/before.yaml new file mode 100644 index 00000000..f0dbfd68 --- /dev/null +++ b/test/projects/declarative-changes-in-asyncapi-operation/case3/before.yaml @@ -0,0 +1,31 @@ +asyncapi: 3.0.0 +info: + title: Sample AsyncAPI + version: 1.0.0 +channels: + correlated: + address: correlated + messages: + correlatedEvent: + $ref: '#/components/messages/CorrelatedEvent' +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/correlated' +components: + messages: + CorrelatedEvent: + headers: + $ref: '#/components/schemas/CorrelationHeaders' + payload: + type: object + properties: + correlation: + $ref: '#/components/schemas/CorrelationHeaders' + schemas: + CorrelationHeaders: + type: object + properties: + correlationId: + type: string #! diff --git a/test/projects/declarative-changes-in-asyncapi-operation/case4/after.yaml b/test/projects/declarative-changes-in-asyncapi-operation/case4/after.yaml new file mode 100644 index 00000000..aaff4a13 --- /dev/null +++ b/test/projects/declarative-changes-in-asyncapi-operation/case4/after.yaml @@ -0,0 +1,30 @@ +asyncapi: 3.0.0 +info: + title: Sample AsyncAPI + version: 1.0.0 +channels: + package: + address: package + messages: + packageUpdated: + $ref: '#/components/messages/PackageUpdated' +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/package' +components: + messages: + PackageUpdated: + payload: + $ref: '#/components/schemas/Package' + schemas: + Package: + type: object + properties: + id: + type: string + name: + type: string #! + parentPackage: + $ref: '#/components/schemas/Package' diff --git a/test/projects/declarative-changes-in-asyncapi-operation/case4/before.yaml b/test/projects/declarative-changes-in-asyncapi-operation/case4/before.yaml new file mode 100644 index 00000000..59ac360d --- /dev/null +++ b/test/projects/declarative-changes-in-asyncapi-operation/case4/before.yaml @@ -0,0 +1,30 @@ +asyncapi: 3.0.0 +info: + title: Sample AsyncAPI + version: 1.0.0 +channels: + package: + address: package + messages: + packageUpdated: + $ref: '#/components/messages/PackageUpdated' +operations: + onReceive: + action: receive + channel: + $ref: '#/channels/package' +components: + messages: + PackageUpdated: + payload: + $ref: '#/components/schemas/Package' + schemas: + Package: + type: object + properties: + id: + type: string + name: + type: number #! + parentPackage: + $ref: '#/components/schemas/Package' diff --git a/test/rest.operation.test.ts b/test/rest.operation.test.ts index 7718952d..67f11e16 100644 --- a/test/rest.operation.test.ts +++ b/test/rest.operation.test.ts @@ -17,19 +17,10 @@ import { OpenAPIV3 } from 'openapi-types' import { createSingleOperationSpec } from '../src/apitypes/rest/rest.operation' -import { describe, test, expect } from '@jest/globals' -import * as fs from 'fs/promises' -import * as path from 'path' -import YAML from 'js-yaml' +import { describe, expect, test } from '@jest/globals' import { securitySchemesFromRequirementsMatcher } from './helpers/matchers' import { RestOperationData } from '../src/apitypes/rest/rest.types' - -// Helper function to load YAML test files -const loadYamlFile = async (relativePath: string): Promise => { - const filePath = path.join(process.cwd(), 'test/projects', relativePath) - const content = await fs.readFile(filePath, 'utf8') - return YAML.load(content) as OpenAPIV3.Document -} +import { loadYamlFile } from './helpers' describe('REST Operation Unit Tests', () => { describe('createSingleOperationSpec', () => { @@ -57,7 +48,7 @@ describe('REST Operation Unit Tests', () => { describe('Security Scheme Filtering', () => { test('should include only used security schemes when operation security is defined', async () => { - const document = await loadYamlFile('rest.operation/security-schemes-filtering-operation/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/security-schemes-filtering-operation/base.yaml') const operationSecurity = getOperationSecurity(document, TEST_DATA_PATH, TEST_DATA_METHOD) @@ -70,7 +61,7 @@ describe('REST Operation Unit Tests', () => { }) test('should include only used security schemes when root security is defined but no operation security', async () => { - const document = await loadYamlFile('rest.operation/security-schemes-filtering-root/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/security-schemes-filtering-root/base.yaml') const result = createTestSingleOperationSpec(document) @@ -81,7 +72,7 @@ describe('REST Operation Unit Tests', () => { }) test('should not include security schemes when none are used', async () => { - const document = await loadYamlFile('rest.operation/security-schemes-filtering-none-used/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/security-schemes-filtering-none-used/base.yaml') const result = createTestSingleOperationSpec(document) @@ -89,7 +80,7 @@ describe('REST Operation Unit Tests', () => { }) test('should handle empty root security requirements', async () => { - const document = await loadYamlFile('rest.operation/security-schemes-filtering-empty-root/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/security-schemes-filtering-empty-root/base.yaml') const result = createTestSingleOperationSpec(document) @@ -97,7 +88,7 @@ describe('REST Operation Unit Tests', () => { }) test('should handle empty operation security requirements', async () => { - const document = await loadYamlFile('rest.operation/security-schemes-filtering-empty-operation/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/security-schemes-filtering-empty-operation/base.yaml') const result = createTestSingleOperationSpec(document) @@ -107,7 +98,7 @@ describe('REST Operation Unit Tests', () => { describe('Conditional Security Handling', () => { test('should not include root security when operation security is explicitly defined', async () => { - const document = await loadYamlFile('rest.operation/conditional-security-explicit/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/conditional-security-explicit/base.yaml') const result = createTestSingleOperationSpec(document) @@ -115,7 +106,7 @@ describe('REST Operation Unit Tests', () => { }) test('should include root security when no operation security is defined', async () => { - const document = await loadYamlFile('rest.operation/security-schemes-filtering-root/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/security-schemes-filtering-root/base.yaml') const result = createTestSingleOperationSpec(document) @@ -125,7 +116,7 @@ describe('REST Operation Unit Tests', () => { describe('Info, ExternalDocs, and Tags Handling', () => { test('should include info object from source document', async () => { - const document = await loadYamlFile('rest.operation/info-externaldocs-tags-filtering/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/info-externaldocs-tags-filtering/base.yaml') const result = createTestSingleOperationSpec(document) @@ -134,7 +125,7 @@ describe('REST Operation Unit Tests', () => { }) test('should include externalDocs object from source document', async () => { - const document = await loadYamlFile('rest.operation/info-externaldocs-tags-filtering/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/info-externaldocs-tags-filtering/base.yaml') const result = createTestSingleOperationSpec(document) @@ -143,7 +134,7 @@ describe('REST Operation Unit Tests', () => { }) test('should filter tags to only include those used by the operation', async () => { - const document = await loadYamlFile('rest.operation/info-externaldocs-tags-filtering/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/info-externaldocs-tags-filtering/base.yaml') const result = createTestSingleOperationSpec(document) @@ -156,7 +147,7 @@ describe('REST Operation Unit Tests', () => { }) test('should handle document with no tags', async () => { - const document = await loadYamlFile('rest.operation/info-externaldocs-no-tags/base.yaml') + const document: OpenAPIV3.Document = await loadYamlFile('rest.operation/info-externaldocs-no-tags/base.yaml') const result = createTestSingleOperationSpec(document)