diff --git a/README.md b/README.md index 39831bcf..49f15848 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AsyncAPI Converter -Convert [AsyncAPI](https://asyncapi.com) documents older to newer versions. +Convert [AsyncAPI](https://asyncapi.com) documents older to newer versions and you can also convert OpenAPI documents to AsyncAPI documents. [![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) @@ -17,6 +17,7 @@ Convert [AsyncAPI](https://asyncapi.com) documents older to newer versions. * [In TS](#in-ts) - [Conversion 2.x.x to 3.x.x](#conversion-2xx-to-3xx) - [Known missing features](#known-missing-features) +- [OpenAPI 3.0 to AsyncAPI 3.0 Conversion](#openapi-30-to-asyncapi-30-conversion) - [Development](#development) - [Contribution](#contribution) - [Contributors ✨](#contributors-%E2%9C%A8) @@ -194,6 +195,49 @@ Conversion to version `3.x.x` from `2.x.x` has several assumptions that should b examples: ["test"] ``` +### OpenAPI 3.0 to AsyncAPI 3.0 Conversion + +The converter now supports transformation from OpenAPI 3.0 to AsyncAPI 3.0. This feature enables easy transition of existing OpenAPI 3.0 documents to AsyncAPI 3.0. + +To use this new conversion feature: + +```js +const fs = require('fs'); +const { convert } = require('@asyncapi/converter') + +try { + const openapi = fs.readFileSync('openapi.yml', 'utf-8') + const asyncapi = convert(openapi, '3.0.0', { from: 'openapi' }); + console.log(asyncapi); +} catch (e) { + console.error(e); +} +``` + +When converting from OpenAPI to AsyncAPI you can now specify the perspective of the conversion using the `perspective` option. This allows you to choose whether the conversion should be from an application or client point of view + +```js +const { convert } = require('@asyncapi/converter') + +try { + const asyncapi2 = fs.readFileSync('asyncapi2.yml', 'utf-8') + const asyncapi3 = convert(asyncapi2, '3.0.0', { openAPIToAsyncAPI: { perspective: 'client' } }); + console.log(asyncapi3); +} catch (e) { + console.error(e); +} +``` + +The perspective option can be set to either 'server' (default) or 'client'. + +- With `server` perspective: `action` becomes `receive` + +- With `client` perspective: `action` becomes `send` + +#### Limitations + +- External to internal references: The converter does not support scenarios where an external schema file references internal components of the OpenAPI document. In such cases, manual adjustment of the converted document may be necessary. + ## Development 1. Setup project by installing dependencies `npm install` diff --git a/src/convert.ts b/src/convert.ts index b7d77eb6..9a6ea63d 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -3,25 +3,31 @@ import { dump } from 'js-yaml'; import { converters as firstConverters } from "./first-version"; import { converters as secondConverters } from "./second-version"; import { converters as thirdConverters } from "./third-version"; +import { converters as openapiConverters } from "./openapi"; import { serializeInput } from "./utils"; -import type { AsyncAPIDocument, ConvertVersion, ConvertOptions, ConvertFunction } from './interfaces'; +import type { AsyncAPIDocument, AsyncAPIConvertVersion, OpenAPIConvertVersion, ConvertOptions, ConvertFunction, ConvertOpenAPIFunction, OpenAPIDocument, OpenAPIToAsyncAPIOptions } from './interfaces'; /** * Value for key (version) represents the function which converts specification from previous version to the given as key. */ -const converters: Record = { +const asyncAPIconverters: Record = { ...firstConverters, ...secondConverters, ...thirdConverters, }; -const conversionVersions = Object.keys(converters); -export function convert(asyncapi: string, version?: ConvertVersion, options?: ConvertOptions): string; -export function convert(asyncapi: AsyncAPIDocument, version?: ConvertVersion, options?: ConvertOptions): AsyncAPIDocument; -export function convert(asyncapi: string | AsyncAPIDocument, version: ConvertVersion = '2.6.0', options: ConvertOptions = {}): string | AsyncAPIDocument { - const { format, document } = serializeInput(asyncapi); +const conversionVersions = Object.keys(asyncAPIconverters); + +export function convert(input: string, version: AsyncAPIConvertVersion, options?: ConvertOptions): string; +export function convert(input: AsyncAPIDocument, version: AsyncAPIConvertVersion, options?: ConvertOptions): AsyncAPIDocument; +export function convert(input: string | AsyncAPIDocument, version: AsyncAPIConvertVersion , options: ConvertOptions= {}): string | AsyncAPIDocument { + const { format, document } = serializeInput(input); + + if ('openapi' in document) { + throw new Error('Cannot convert OpenAPI document. Use convertOpenAPI function instead.'); + } const asyncapiVersion = document.asyncapi; let fromVersion = conversionVersions.indexOf(asyncapiVersion); @@ -41,8 +47,8 @@ export function convert(asyncapi: string | AsyncAPIDocument, version: ConvertVer fromVersion++; let converted = document; for (let i = fromVersion; i <= toVersion; i++) { - const v = conversionVersions[i] as ConvertVersion; - converted = converters[v](converted, options); + const v = conversionVersions[i] as AsyncAPIConvertVersion; + converted = asyncAPIconverters[v](converted, options); } if (format === 'yaml') { @@ -50,3 +56,25 @@ export function convert(asyncapi: string | AsyncAPIDocument, version: ConvertVer } return converted; } + +export function convertOpenAPI(input: string ,version: OpenAPIConvertVersion,options?: OpenAPIToAsyncAPIOptions): string; +export function convertOpenAPI(input: OpenAPIDocument, version: OpenAPIConvertVersion ,options?: OpenAPIToAsyncAPIOptions): AsyncAPIDocument; +export function convertOpenAPI(input: string | OpenAPIDocument, version: OpenAPIConvertVersion, options: OpenAPIToAsyncAPIOptions = {}): string | AsyncAPIDocument { + + const { format, document } = serializeInput(input); + const openApiVersion = document.openapi; + const converterVersion = openApiVersion; + + const openapiToAsyncapiConverter = openapiConverters[converterVersion as OpenAPIConvertVersion] as ConvertOpenAPIFunction; + + if (!openapiToAsyncapiConverter) { + throw new Error(`We are not able to convert OpenAPI ${converterVersion} to AsyncAPI, please raise a feature request.`); + } + + const convertedAsyncAPI = openapiToAsyncapiConverter(document as OpenAPIDocument, options); + + if (format === "yaml") { + return dump(convertedAsyncAPI, { skipInvalid: true }); + } + return convertedAsyncAPI; +} diff --git a/src/index.ts b/src/index.ts index 5520a774..808abac0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export { convert } from './convert'; -export type { AsyncAPIDocument, ConvertVersion, ConvertOptions } from './interfaces'; +export type { AsyncAPIDocument, AsyncAPIConvertVersion, OpenAPIConvertVersion, ConvertOptions } from './interfaces'; diff --git a/src/interfaces.ts b/src/interfaces.ts index 31aa98a3..779c6a53 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -2,7 +2,10 @@ * PUBLIC TYPES */ export type AsyncAPIDocument = { asyncapi: string } & Record; -export type ConvertVersion = '1.1.0' | '1.2.0' | '2.0.0-rc1' | '2.0.0-rc2' | '2.0.0' | '2.1.0' | '2.2.0' | '2.3.0' | '2.4.0' | '2.5.0' | '2.6.0' | '3.0.0'; +export type OpenAPIDocument = { openapi: string } & Record; +export type AsyncAPIConvertVersion = '1.1.0' | '1.2.0' | '2.0.0-rc1' | '2.0.0-rc2' | '2.0.0' | '2.1.0' | '2.2.0' | '2.3.0' | '2.4.0' | '2.5.0' | '2.6.0' | '3.0.0'; + +export type OpenAPIConvertVersion = '3.0.0'; export type ConvertV2ToV3Options = { idGenerator?: (data: { asyncapi: AsyncAPIDocument, kind: 'channel' | 'operation' | 'message', key: string | number | undefined, path: Array, object: any, parentId?: string }) => string, pointOfView?: 'application' | 'client', @@ -10,12 +13,18 @@ export type ConvertV2ToV3Options = { convertServerComponents?: boolean; convertChannelComponents?: boolean; } + +export type OpenAPIToAsyncAPIOptions = { + perspective?: 'client' | 'server'; +}; export type ConvertOptions = { v2tov3?: ConvertV2ToV3Options; + openAPIToAsyncAPI?: OpenAPIToAsyncAPIOptions; } /** * PRIVATE TYPES */ export type ConvertFunction = (asyncapi: AsyncAPIDocument, options: ConvertOptions) => AsyncAPIDocument; +export type ConvertOpenAPIFunction = (openapi: OpenAPIDocument, options: OpenAPIToAsyncAPIOptions) => AsyncAPIDocument; diff --git a/src/openapi.ts b/src/openapi.ts new file mode 100644 index 00000000..a8afb3aa --- /dev/null +++ b/src/openapi.ts @@ -0,0 +1,657 @@ +import { sortObjectKeys, isRefObject, isPlainObject, removeEmptyObjects, createRefObject, isRemoteRef } from "./utils"; +import { AsyncAPIDocument, ConvertOpenAPIFunction, OpenAPIToAsyncAPIOptions, OpenAPIDocument } from "./interfaces"; + +export const converters: Record = { + '3.0.0': from_openapi_to_asyncapi, +} + +/** + * Converts an OpenAPI document to an AsyncAPI document. + * @param {OpenAPIDocument} openapi - The OpenAPI document to convert. + * @param {ConvertOptions} options - Conversion options. + * @returns {AsyncAPIDocument} The converted AsyncAPI document. + */ +function from_openapi_to_asyncapi(openapi: OpenAPIDocument, options: OpenAPIToAsyncAPIOptions = {}): AsyncAPIDocument { + const perspective = options.perspective || 'server'; + const asyncapi: Partial = { + asyncapi: '3.0.0', + info: convertInfoObject(openapi.info, openapi), + servers: openapi.servers ? convertServerObjects(openapi.servers, openapi) : undefined, + channels: {}, + operations: {}, + components: convertComponents(openapi) + }; + + const { channels, operations } = convertPaths(openapi.paths, perspective); + asyncapi.channels = channels; + asyncapi.operations = operations; + + removeEmptyObjects(asyncapi); + + return sortObjectKeys( + asyncapi as AsyncAPIDocument, + ['asyncapi', 'info', 'defaultContentType', 'servers', 'channels', 'operations', 'components'] + ); +} + +interface InfoObject { + title: string; + version: string; + description?: string; + termsOfService?: string; + contact?: ContactObject; + license?: LicenseObject; +} + +interface ContactObject { + name?: string; + url?: string; + email?: string; +} + +interface LicenseObject { + name: string; + url?: string; +} + +/** + * Converts openAPI info objects to asyncAPI info objects. + * @param info - The openAPI info object to convert. + * @param openapi - The complete openAPI document. + * @returns openAPI info object + */ +function convertInfoObject(info: InfoObject, openapi: OpenAPIDocument): AsyncAPIDocument['info'] { + return sortObjectKeys({ + ...info, + tags: [openapi.tags], + externalDocs: openapi.externalDocs, + }, [ + "title", + "version", + "description", + "termsOfService", + "contact", + "license", + "tags", + "externalDocs", + ]); +} + +interface ServerObject { + url: string; + description?: string; + variables?: Record; +} + +interface ServerVariableObject { + enum?: string[]; + default: string; + description?: string; +} + +/** + * Converts OpenAPI server objects to AsyncAPI server objects. + * @param {ServerObject[]} servers - The OpenAPI server objects to convert. + * @param {OpenAPIDocument} openapi - The complete OpenAPI document. + * @returns {AsyncAPIDocument['servers']} The converted AsyncAPI server objects. + */ +function convertServerObjects(servers: ServerVariableObject[], openapi: OpenAPIDocument): AsyncAPIDocument['servers'] { + const newServers: Record = {}; + const security: Record = openapi.security; + servers.forEach((server: any) => { + + const serverName = generateServerName(server.url); + if (isRefObject(server)) { + newServers[serverName] = server; + return; + } + + const { host, pathname, protocol } = resolveServerUrl(server.url); + server.host = host; + if (pathname !== undefined) { + server.pathname = pathname; + } + + if (protocol !== undefined && server.protocol === undefined) { + server.protocol = protocol; + } + delete server.url; + + if (security) { + server.security = security.map((securityRequirement: Record) => { + // pass through the security requirement, conversion will happen in components + return securityRequirement; + }); + delete openapi.security; + } + + newServers[serverName] = sortObjectKeys( + server, + ['host', 'pathname', 'protocol', 'protocolVersion', 'title', 'summary', 'description', 'variables', 'security', 'tags', 'externalDocs', 'bindings'], + ); + }); + + return newServers; +} + +/** + * Generates a server name based on the server URL. + * @param {string} url - The server URL. + * @returns {string} The generated server name. + */ +function generateServerName(url: string): string { + const { host, pathname } = resolveServerUrl(url); + const baseName = host.split('.').slice(-2).join('.'); + const pathSegment = pathname ? pathname.split('/')[1] : ''; + return `${baseName}${pathSegment ? `_${pathSegment}` : ''}`.replace(/[^a-zA-Z0-9_]/g, '_'); +} + +function resolveServerUrl(url: string): { + host: string; + pathname?: string; + protocol: string; +} { + let [maybeProtocol, maybeHost] = url.split("://"); + if (!maybeHost) { + maybeHost = maybeProtocol; + } + const [host, ...pathnames] = maybeHost.split("/"); + + if (pathnames.length) { + return { + host, + pathname: `/${pathnames.join("/")}`, + protocol: maybeProtocol, + }; + } + return { host, pathname: undefined , protocol: maybeProtocol }; +} + +/** + * Converts OpenAPI paths to AsyncAPI channels and operations. + * @param {Record} paths - The OpenAPI paths object. + * @param {'client' | 'server'} perspective - The perspective of the conversion (client or server). + * @returns {{ channels: AsyncAPIDocument['channels'], operations: AsyncAPIDocument['operations'] }} + */ +function convertPaths(paths: OpenAPIDocument['paths'], perspective: 'client' | 'server'): { + channels: AsyncAPIDocument['channels'], + operations: AsyncAPIDocument['operations'] +} { + const channels: AsyncAPIDocument['channels'] = {}; + const operations: AsyncAPIDocument['operations'] = {}; + + if(paths) { + for (const [path, pathItemOrRef] of Object.entries(paths)) { + if (!isPlainObject(pathItemOrRef)) continue; + + const pathItem = isRefObject(pathItemOrRef) ? pathItemOrRef : pathItemOrRef as any; + const channelName = path.replace(/^\//, '').replace(/\//g, '_') || 'root'; + channels[channelName] = { + address: path, + messages: {}, + parameters: convertPathParameters(path, pathItem.parameters) + }; + + for (const [method, operation] of Object.entries(pathItem)) { + if (['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'].includes(method) && isPlainObject(operation)) { + const operationObject = operation as any; + const operationId = operationObject.operationId || `${method}${channelName}`; + + // Create operation + operations[operationId] = { + action: perspective === 'client' ? 'send' : 'receive', + channel: createRefObject('channels', channelName), + summary: operationObject.summary, + description: operationObject.description, + tags: operationObject.tags?.map((tag: string) => ({ name: tag })), + bindings: { + http: { + method: method.toUpperCase(), + } + }, + messages: [] + }; + + // Convert request body to message + if (operationObject.requestBody) { + const requestMessages = convertRequestBodyToMessages(operationObject.requestBody, operationId, method); + Object.assign(channels[channelName].messages, requestMessages); + operations[operationId].messages.push(...Object.keys(requestMessages).map(msgName => + createRefObject('channels', channelName, 'messages', msgName) + )); + } + + // Convert responses to messages + if (operationObject.responses) { + const responseMessages = convertResponsesToMessages(operationObject.responses, operationId, method); + Object.assign(channels[channelName].messages, responseMessages); + operations[operationId].reply = { + channel: createRefObject('channels', channelName), + messages: Object.keys(responseMessages).map(msgName => + createRefObject('channels', channelName, 'messages', msgName) + ) + }; + } + + // Add reply section if there are responses + if (operationObject.responses && Object.keys(operationObject.responses).length > 0) { + operations[operationId].reply = { + channel: createRefObject('channels', channelName), + messages: Object.entries(operationObject.responses).map(([statusCode, response]) => + createRefObject('channels', channelName, 'messages', `${operationId}Response${statusCode}`) + ) + }; + } + + // Convert parameters + if (operationObject.parameters) { + const params = convertOperationParameters(operationObject.parameters); + if (Object.keys(params).length > 0) { + channels[channelName].parameters = { + ...channels[channelName].parameters, + ...params + }; + } + } + } + } + + removeEmptyObjects(channels[channelName]); + } + } + + return { channels, operations }; +} + +/** + * Converts OpenAPI path parameters to AsyncAPI channel parameters. + * @param {any[]} parameters - The OpenAPI path parameters. + * @returns {Record} The converted AsyncAPI channel parameters. + */ +function convertPathParameters( path:string, parameters: any[] = []): Record { + const convertedParams: Record = {}; + + const paramNames = path.match(/\{([^}]+)\}/g)?.map(param => param.slice(1, -1)) || []; + + paramNames.forEach(paramName => { + const param = parameters.find(p => p.name === paramName && p.in === 'path'); + if (param) { + convertedParams[paramName] = convertParameter(param); + } else { + // If the parameter is not defined in the OpenAPI spec, create a default one + convertedParams[paramName] = { + description: `Path parameter ${paramName}`, + }; + } + }); + + return convertedParams; +} + +/** + * Converts OpenAPI operatiion parameters to AsyncAPI operation parameters. + * @param {any[]} parameters - The OpenAPI operation parameters. + * @returns {Record} The converted AsyncAPI operation parameters. + */ +function convertOperationParameters(parameters: any[]): Record { + const convertedParams: Record = {}; + + parameters.forEach(param => { + if (!isRefObject(param) && param.in === 'query') { + convertedParams[param.name] = convertParameter(param); + } + }); + + return convertedParams; +} + +/** + * Converts an OpenAPI Parameter Object to an AsyncAPI Parameter Object. + * @param {ParameterObject} param - The OpenAPI Parameter Object. + * @returns {any} The converted AsyncAPI Parameter Object. + */ +function convertParameter(param: any): any { + const convertedParam: any = { + description: param.description, + }; + + if (param.required) { + convertedParam.required = param.required; + } + + if (param.schema && !isRefObject(param.schema)) { + if (param.schema.enum) { + convertedParam.enum = param.schema.enum; + } + if (param.schema.default !== undefined) { + convertedParam.default = param.schema.default; + } + } + + if (param.examples) { + convertedParam.examples = Object.values(param.examples).map((example:any) => + isRefObject(example) ? example : example.value + ); + } + + // the location based on the parameter's 'in' property + switch (param.in) { + case 'query': + case 'header': + case 'cookie': + convertedParam.location = `$message.header#/${param.name}`; + break; + case 'path': + // Path parameters are part of the channel address + break; + default: + // If 'in' is not recognized, default to payload + convertedParam.location = `$message.payload#/${param.name}`; + } + + return convertedParam; +} + +function convertRequestBodyToMessages(requestBody: any, operationId: string, method: string): Record { + const messages: Record = {}; + + if (isPlainObject(requestBody.content)) { + Object.entries(requestBody.content).forEach(([contentType, mediaType]: [string, any]) => { + const messageName = `${operationId}Request`; + messages[messageName] = { + name: messageName, + title: `${method.toUpperCase()} request`, + contentType: contentType, + payload: convertSchema(mediaType.schema), + summary: requestBody.description, + }; + }); + } + + return messages; +} + +/** + * Converts OpenAPI Response Objects to AsyncAPI Message Objects. + * @param {ResponsesObject} responses - The OpenAPI Response Objects to convert. + * @param {string} operationId - The ID of the operation these responses belong to. + * @param {string} method - The HTTP method of the operation. + * @returns {Record} A record of converted AsyncAPI Message Objects. + */ +function convertResponsesToMessages(responses: Record, operationId: string, method: string): Record { + const messages: Record = {}; + + Object.entries(responses).forEach(([statusCode, response]) => { + if (isPlainObject(response.content)) { + Object.entries(response.content).forEach(([contentType, mediaType]: [string, any]) => { + const messageName = `${operationId}Response${statusCode}`; + messages[messageName] = { + name: messageName, + title: `${method.toUpperCase()} response ${statusCode}`, + contentType: contentType, + payload: convertSchema(mediaType.schema), + summary: response.description, + headers: response.headers ? convertHeadersToSchema(response.headers) : undefined, + }; + }); + } else { + const messageName = `${operationId}Response${statusCode}`; + messages[messageName] = { + name: messageName, + title: `${method.toUpperCase()} response ${statusCode}`, + summary: response.description, + }; + } + }); + + return messages; +} + +/** + * Converts OpenAPI Components Object to AsyncAPI Components Object. + * @param {OpenAPIDocument} openapi - The complete OpenAPI document. + * @returns {AsyncAPIDocument['components']} The converted AsyncAPI Components Object. + */ +function convertComponents(openapi: OpenAPIDocument): AsyncAPIDocument['components'] { + const asyncComponents: AsyncAPIDocument['components'] = {}; + + if (openapi.components) { + if (openapi.components.schemas) { + asyncComponents.schemas = convertSchemas(openapi.components.schemas); + } + + if (openapi.components.securitySchemes) { + asyncComponents.securitySchemes = convertSecuritySchemes(openapi.components.securitySchemes); + } + + if (openapi.components.parameters) { + asyncComponents.parameters = {}; + for (const [name, param] of Object.entries(openapi.components.parameters)) { + if (!isRefObject(param)) { + asyncComponents.parameters[name] = convertParameter(param); + } else { + asyncComponents.parameters[name] = param; + } + } + } + + if (openapi.components.responses) { + asyncComponents.messages = convertComponentResponsesToMessages(openapi.components.responses); + } + + if (openapi.components.requestBodies) { + asyncComponents.messageTraits = convertRequestBodiesToMessageTraits(openapi.components.requestBodies); + } + + if (openapi.components.headers) { + asyncComponents.messageTraits = { + ...(asyncComponents.messageTraits || {}), + ...convertHeadersToMessageTraits(openapi.components.headers) + }; + } + + if (openapi.components.examples) { + asyncComponents.examples = openapi.components.examples; + } + } + + return removeEmptyObjects(asyncComponents); +} + +/** + * converts openAPI schema object to multiformat/schema object + * @param schema openAPI schema object + * @returns multiformat/schema object + */ +function convertSchema(schema: any): any { + if (isRefObject(schema)) { + // Check if it's an external reference + if (schema.$ref.startsWith('./') || schema.$ref.startsWith('http')) { + // Convert external references to multi-format schema objects + return { + schemaFormat: 'application/vnd.oai.openapi;version=3.0.0', + schema: schema + }; + } + return schema; + } + + return { + schemaFormat: 'application/vnd.oai.openapi;version=3.0.0', + schema: schema + }; +} + +/** + * Converts OpenAPI Schema Objects to AsyncAPI Schema Objects. + * @param {Record} schemas - The OpenAPI Schema Objects to convert. + * @returns {Record} The converted AsyncAPI Schema Objects. + */ +function convertSchemas(schemas: Record): Record { + const convertedSchemas: Record = {}; + + for (const [name, schema] of Object.entries(schemas)) { + convertedSchemas[name] = convertSchema(schema); + } + + return convertedSchemas; +} + +/** + * Converts a single OpenAPI Security Scheme Object to an AsyncAPI Security Scheme Object. + * @param {Record} scheme - The OpenAPI Security Scheme Object to convert. + * @returns {Record} The converted AsyncAPI Security Scheme Object. + */ +function convertSecuritySchemes(securitySchemes: Record): Record { + const convertedSchemes: Record = {}; + + for (const [name, scheme] of Object.entries(securitySchemes)) { + convertedSchemes[name] = convertSecurityScheme(scheme); + } + + return convertedSchemes; +} + +/** + * Converts a single OpenAPI Security Scheme Object to an AsyncAPI Security Scheme Object. + * @param {any} scheme - The OpenAPI Security Scheme Object to convert. + * @returns {Record} The converted AsyncAPI Security Scheme Object. + */ +function convertSecurityScheme(scheme: any): Record { + const convertedScheme: any = { + type: scheme.type, + description: scheme.description + }; + + if (scheme.type === 'oauth2' && scheme.flows) { + const newFlows = JSON.parse(JSON.stringify(scheme.flows)); + function convertScopesToAvailableScopes(obj: any) { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + if (key === 'scopes') { + obj['availableScopes'] = obj[key]; + delete obj[key]; + } else if (typeof obj[key] === 'object') { + convertScopesToAvailableScopes(obj[key]); + } + } + } + } + convertScopesToAvailableScopes(newFlows); + convertedScheme.flows = newFlows; + if (scheme.scopes) { + convertedScheme.scopes = Object.keys(scheme.scopes); + } + } else if (scheme.type === 'http') { + convertedScheme.scheme = scheme.scheme; + if (scheme.scheme === 'bearer') { + convertedScheme.bearerFormat = scheme.bearerFormat; + } + } else if (scheme.type === 'apiKey') { + convertedScheme.in = scheme.in; + convertedScheme.name = scheme.name; + } + + return convertedScheme; +} + +/** + * Converts OpenAPI Response Objects from the components section to AsyncAPI Message Objects. + * @param {Record} responses - The OpenAPI Response Objects to convert. + * @returns {Record} A record of converted AsyncAPI Message Objects. + */ +function convertComponentResponsesToMessages(responses: Record): Record { + const messages: Record = {}; + + for (const [name, response] of Object.entries(responses)) { + if (isPlainObject(response.content)) { + Object.entries(response.content).forEach(([contentType, mediaType]: [string, any]) => { + messages[name] = { + name: name, + contentType: contentType, + payload: convertSchema(mediaType.schema), + summary: response.description, + headers: response.headers ? convertHeadersToSchema(response.headers) : undefined, + }; + }); + } else { + messages[name] = { + name: name, + summary: response.description, + }; + } + } + + return messages; +} + +/** + * Converts OpenAPI Request Body Objects from the components section to AsyncAPI Message Trait Objects. + * @param {Record} requestBodies - The OpenAPI Request Body Objects to convert. + * @returns {Record} A record of converted AsyncAPI Message Trait Objects. + */ +function convertRequestBodiesToMessageTraits(requestBodies: Record): Record { + const messageTraits: Record = {}; + + for (const [name, requestBodyOrRef] of Object.entries(requestBodies)) { + if (!isRefObject(requestBodyOrRef) && requestBodyOrRef.content) { + const contentType = Object.keys(requestBodyOrRef.content)[0]; + messageTraits[name] = { + name: name, + contentType: contentType, + description: requestBodyOrRef.description, + }; + + if (requestBodyOrRef.content[contentType].schema && + requestBodyOrRef.content[contentType].schema.properties && + requestBodyOrRef.content[contentType].schema.properties.headers) { + messageTraits[name].headers = requestBodyOrRef.content[contentType].schema.properties.headers; + } + } + } + + return messageTraits; +} + +/** + * Converts OpenAPI Header Objects from the components section to AsyncAPI Message Trait Objects. + * @param {Record} headers - The OpenAPI Header Objects to convert. + * @returns {Record} A record of converted AsyncAPI Message Trait Objects. + */ +function convertHeadersToMessageTraits(headers: Record): Record { + const messageTraits: Record = {}; + + for (const [name, header] of Object.entries(headers)) { + messageTraits[`Header${name}`] = { + headers: { + type: 'object', + properties: { + [name]: header.schema, + }, + required: [name], + }, + }; + } + + return messageTraits; +} + +/** + * Converts OpenAPI Header Objects to an AsyncAPI Schema Object representing the headers. + * @param {Record} headers - The OpenAPI Header Objects to convert. + * @returns {SchemaObject} An AsyncAPI Schema Object representing the headers. + */ +function convertHeadersToSchema(headers: Record): any { + const properties: Record = {}; + + for (const [name, headerOrRef] of Object.entries(headers)) { + if (!isRefObject(headerOrRef)) { + properties[name] = headerOrRef.schema || {}; + } + } + + return { + type: 'object', + properties, + }; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index cde51507..7ef17ae3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,8 @@ import { load } from 'js-yaml'; -import type { AsyncAPIDocument } from "./interfaces"; +import type { AsyncAPIDocument, OpenAPIDocument } from "./interfaces"; -export function serializeInput(document: string | AsyncAPIDocument): { format: 'json' | 'yaml', document: AsyncAPIDocument } | never { +export function serializeInput(document: string | AsyncAPIDocument | OpenAPIDocument): { format: 'json' | 'yaml', document: AsyncAPIDocument | OpenAPIDocument } | never { let triedConvertToYaml = false; try { if (typeof document === 'object') { @@ -14,10 +14,17 @@ export function serializeInput(document: string | AsyncAPIDocument): { format: ' const maybeJSON = JSON.parse(document); if (typeof maybeJSON === 'object') { - return { - format: 'json', - document: maybeJSON, - }; + if ('openapi' in maybeJSON) { + return { + format: 'json', + document: maybeJSON, + }; + } else { + return { + format: 'json', + document: maybeJSON, + }; + } } triedConvertToYaml = true; // NOSONAR @@ -25,7 +32,7 @@ export function serializeInput(document: string | AsyncAPIDocument): { format: ' // but if it's `string` then we have option that it can be YAML but it doesn't have to be return { format: 'yaml', - document: load(document) as AsyncAPIDocument, + document: load(document) as AsyncAPIDocument | OpenAPIDocument, }; } catch (e) { try { @@ -36,7 +43,7 @@ export function serializeInput(document: string | AsyncAPIDocument): { format: ' // try to parse (again) YAML, because the text itself may not have a JSON representation and cannot be represented as a JSON object/string return { format: 'yaml', - document: load(document as string) as AsyncAPIDocument, + document: load(document as string) as AsyncAPIDocument | OpenAPIDocument, }; } catch (err) { throw new Error('AsyncAPI document must be a valid JSON or YAML document.'); @@ -134,3 +141,17 @@ function untilde(str: string) { return sub; }); } + +export function removeEmptyObjects(obj: Record): Record { + Object.keys(obj).forEach(key => { + if (obj[key] && typeof obj[key] === 'object') { + removeEmptyObjects(obj[key]); + if (Object.keys(obj[key]).length === 0) { + delete obj[key]; + } + } else if (obj[key] === undefined) { + delete obj[key]; + } + }); + return obj; +} \ No newline at end of file diff --git a/test/helpers.ts b/test/helpers.ts index 69e7fca6..de31b908 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,16 +1,16 @@ /* It is a helper required for testing on windows. It can't be solved by editor configuration and the end line setting because expected result is converted during tests. We need to remove all line breaks from the string + as well as all multiple spaces and trim the string. */ export function removeLineBreaks(str: string) { - return str.replace(/\r?\n|\r/g, '') + return str.replace(/\r?\n|\r/g, '').replace(/\s+/g, ' ').trim(); } export function assertResults(output: string, result: string){ try{ expect(removeLineBreaks(output)).toEqual(removeLineBreaks(result)); - }catch(e) { - console.log(result) + } catch(e) { throw e; } } diff --git a/test/input/openapi/callbacks_and_contents.yml b/test/input/openapi/callbacks_and_contents.yml new file mode 100644 index 00000000..a9e12038 --- /dev/null +++ b/test/input/openapi/callbacks_and_contents.yml @@ -0,0 +1,137 @@ +openapi: 3.0.0 +info: + title: Callbacks, Links, and Content Types API + version: 1.0.0 + description: An API showcasing callbacks, links, and various content types +servers: + - url: https://api.example.com/v1 +paths: + /webhooks: + post: + summary: Subscribe to webhook + operationId: subscribeWebhook + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + callbackUrl: + type: string + format: uri + responses: + '201': + description: Subscription created + callbacks: + onEvent: + '{$request.body#/callbackUrl}': + post: + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + eventType: + type: string + eventData: + type: object + responses: + '200': + description: Webhook processed + /users/{userId}: + get: + summary: Get a user + operationId: getUser + parameters: + - in: path + name: userId + required: true + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/User' + links: + userPosts: + operationId: getUserPosts + parameters: + userId: '$response.body#/id' + /users/{userId}/posts: + get: + summary: Get user posts + operationId: getUserPosts + parameters: + - in: path + name: userId + required: true + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Post' + /upload: + post: + summary: Upload a file + operationId: uploadFile + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + '200': + description: Successful upload + content: + application/json: + schema: + type: object + properties: + fileId: + type: string + /stream: + get: + summary: Get a data stream + operationId: getStream + responses: + '200': + description: Successful response + content: + application/octet-stream: + schema: + type: string + format: binary +components: + schemas: + User: + type: object + properties: + id: + type: string + name: + type: string + Post: + type: object + properties: + id: + type: string + title: + type: string + content: + type: string \ No newline at end of file diff --git a/test/input/openapi/components_and_security.yml b/test/input/openapi/components_and_security.yml new file mode 100644 index 00000000..b88359ac --- /dev/null +++ b/test/input/openapi/components_and_security.yml @@ -0,0 +1,97 @@ +openapi: 3.0.0 +info: + title: Components and Security API + version: 1.0.0 + description: An API showcasing various components and security schemes +servers: + - url: https://api.example.com/v1 +paths: + /secure: + get: + summary: Secure endpoint + security: + - bearerAuth: [] + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/SecureResponse' + /oauth: + get: + summary: OAuth protected endpoint + security: + - oAuth2: + - read + - write + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/OAuthResponse' +components: + schemas: + SecureResponse: + type: object + properties: + message: + type: string + OAuthResponse: + type: object + properties: + data: + type: string + Error: + type: object + properties: + code: + type: integer + message: + type: string + securitySchemes: + bearerAuth: + type: http + scheme: bearer + oAuth2: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + scopes: + read: Read access + write: Write access + parameters: + limitParam: + in: query + name: limit + schema: + type: integer + required: false + description: Maximum number of items to return + responses: + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + requestBodies: + ItemInput: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: string + headers: + X-Rate-Limit: + schema: + type: integer + description: Calls per hour allowed by the user \ No newline at end of file diff --git a/test/input/openapi/external_reference.yml b/test/input/openapi/external_reference.yml new file mode 100644 index 00000000..e6e24bbe --- /dev/null +++ b/test/input/openapi/external_reference.yml @@ -0,0 +1,14 @@ +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: './external.json' \ No newline at end of file diff --git a/test/input/openapi/no-channel-operation.yml b/test/input/openapi/no-channel-operation.yml new file mode 100644 index 00000000..0f0c4e30 --- /dev/null +++ b/test/input/openapi/no-channel-operation.yml @@ -0,0 +1,35 @@ +openapi: 3.0.0 +info: + title: Sample Pet Store App + description: This is a sample server for a pet store. + termsOfService: 'http://example.com/terms/' + contact: + name: API Support + url: 'http://www.example.com/support' + email: support@example.com + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' + version: 1.0.1 + +servers: + - url: 'https://{username}.gigantic-server.com:{port}/{basePath}' + description: The production API server + variables: + username: + default: demo + description: this value is assigned by the service provider, in this example `gigantic-server.com` + port: + enum: + - '8443' + - '443' + default: '8443' + basePath: + default: v2 +tags: + name: pet + description: Pets operations + +externalDocs: + description: Find more info here + url: 'https://example.com' diff --git a/test/input/openapi/operation_and_parameter.yml b/test/input/openapi/operation_and_parameter.yml new file mode 100644 index 00000000..329d23fc --- /dev/null +++ b/test/input/openapi/operation_and_parameter.yml @@ -0,0 +1,125 @@ +openapi: 3.0.0 +info: + title: Operations and Parameters API + version: 1.0.0 + description: An API showcasing various operations and parameter types +servers: + - url: https://api.example.com/v1 +paths: + /items: + get: + summary: List items + operationId: listItems + parameters: + - in: query + name: limit + schema: + type: integer + required: false + description: Maximum number of items to return + - in: query + name: offset + schema: + type: integer + required: false + description: Number of items to skip + - in: header + name: X-API-Key + schema: + type: string + required: true + description: API Key for authentication + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Item' + post: + summary: Create an item + operationId: createItem + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ItemInput' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + /items/{itemId}: + get: + summary: Get an item + operationId: getItem + parameters: + - in: path + name: itemId + required: true + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + put: + summary: Update an item + operationId: updateItem + parameters: + - in: path + name: itemId + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ItemInput' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + delete: + summary: Delete an item + operationId: deleteItem + parameters: + - in: path + name: itemId + required: true + schema: + type: string + responses: + '204': + description: Successful response +components: + schemas: + Item: + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + ItemInput: + type: object + properties: + name: + type: string + description: + type: string \ No newline at end of file diff --git a/test/openapi-to-asyncapi.spec.ts b/test/openapi-to-asyncapi.spec.ts new file mode 100644 index 00000000..b4636417 --- /dev/null +++ b/test/openapi-to-asyncapi.spec.ts @@ -0,0 +1,40 @@ +import fs from 'fs'; +import path from 'path'; + +import { convertOpenAPI } from '../src/convert'; +import { assertResults } from './helpers'; +import { OpenAPIToAsyncAPIOptions } from '../src/interfaces' + +describe("convert() - openapi to asyncapi", () => { + it("should convert the basic structure of openapi to asyncapi", () => { + const input = fs.readFileSync(path.resolve(__dirname, "input", "openapi", "no-channel-operation.yml"), "utf8"); + const output = fs.readFileSync(path.resolve(__dirname, "output", "openapi-to-asyncapi", "no-channel-parameter.yml"), "utf8"); + const result = convertOpenAPI(input, '3.0.0'); + assertResults(output, result); + }); + it("should convert the openapi operation and parameter keywoards to asyncapi", () => { + const input = fs.readFileSync(path.resolve(__dirname, "input", "openapi", "operation_and_parameter.yml"), "utf8"); + const output = fs.readFileSync(path.resolve(__dirname, "output", "openapi-to-asyncapi", "operation_and_parameter.yml"), "utf8"); + const result = convertOpenAPI(input, '3.0.0'); + assertResults(output, result); + }); + it("should convert the openapi components and securitySchemes keywoards to asyncapi", () => { + const input = fs.readFileSync(path.resolve(__dirname, "input", "openapi", "components_and_security.yml"), "utf8"); + const output = fs.readFileSync(path.resolve(__dirname, "output", "openapi-to-asyncapi", "components_and_security.yml"), "utf8"); + const result = convertOpenAPI(input, '3.0.0'); + assertResults(output, result); + }); + it("should convert the openapi contents and callbacks keywoards to asyncapi", () => { + const input = fs.readFileSync(path.resolve(__dirname, "input", "openapi", "callbacks_and_contents.yml"), "utf8"); + const output = fs.readFileSync(path.resolve(__dirname, "output", "openapi-to-asyncapi", "callbacks_and_contents.yml"), "utf8"); + const result = convertOpenAPI(input, '3.0.0'); + assertResults(output, result); + }); + it("should convert with 'client' perspective", () => { + const input = fs.readFileSync(path.resolve(__dirname, "input", "openapi", "operation_and_parameter.yml"), "utf8"); + const output = fs.readFileSync(path.resolve(__dirname, "output", "openapi-to-asyncapi", "operation_and_parameter_client.yml"), "utf8"); + const options: OpenAPIToAsyncAPIOptions = { perspective: 'client' }; + const result = convertOpenAPI(input, '3.0.0', options); + assertResults(output, result); + }); +}); \ No newline at end of file diff --git a/test/output/openapi-to-asyncapi/callbacks_and_contents.yml b/test/output/openapi-to-asyncapi/callbacks_and_contents.yml new file mode 100644 index 00000000..ec7814c9 --- /dev/null +++ b/test/output/openapi-to-asyncapi/callbacks_and_contents.yml @@ -0,0 +1,192 @@ +asyncapi: 3.0.0 +info: + title: 'Callbacks, Links, and Content Types API' + version: 1.0.0 + description: 'An API showcasing callbacks, links, and various content types' +servers: + example_com_v1: + host: api.example.com + pathname: /v1 + protocol: https +channels: + webhooks: + address: /webhooks + messages: + subscribeWebhookRequest: + name: subscribeWebhookRequest + title: POST request + contentType: application/json + payload: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + callbackUrl: + type: string + format: uri + subscribeWebhookResponse201: + name: subscribeWebhookResponse201 + title: POST response 201 + summary: Subscription created + 'users_{userId}': + address: '/users/{userId}' + messages: + getUserResponse200: + name: getUserResponse200 + title: GET response 200 + contentType: application/json + payload: + $ref: '#/components/schemas/User' + summary: Successful response + parameters: + userId: + description: Path parameter userId + 'users_{userId}_posts': + address: '/users/{userId}/posts' + messages: + getUserPostsResponse200: + name: getUserPostsResponse200 + title: GET response 200 + contentType: application/json + payload: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: array + items: + $ref: '#/components/schemas/Post' + summary: Successful response + parameters: + userId: + description: Path parameter userId + upload: + address: /upload + messages: + uploadFileRequest: + name: uploadFileRequest + title: POST request + contentType: multipart/form-data + payload: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + file: + type: string + format: binary + uploadFileResponse200: + name: uploadFileResponse200 + title: POST response 200 + contentType: application/json + payload: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + fileId: + type: string + summary: Successful upload + stream: + address: /stream + messages: + getStreamResponse200: + name: getStreamResponse200 + title: GET response 200 + contentType: application/octet-stream + payload: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: string + format: binary + summary: Successful response +operations: + subscribeWebhook: + action: receive + channel: + $ref: '#/channels/webhooks' + summary: Subscribe to webhook + bindings: + http: + method: POST + messages: + - $ref: '#/channels/webhooks/messages/subscribeWebhookRequest' + reply: + channel: + $ref: '#/channels/webhooks' + messages: + - $ref: '#/channels/webhooks/messages/subscribeWebhookResponse201' + getUser: + action: receive + channel: + $ref: '#/channels/users_{userId}' + summary: Get a user + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/users_{userId}' + messages: + - $ref: '#/channels/users_{userId}/messages/getUserResponse200' + getUserPosts: + action: receive + channel: + $ref: '#/channels/users_{userId}_posts' + summary: Get user posts + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/users_{userId}_posts' + messages: + - $ref: '#/channels/users_{userId}_posts/messages/getUserPostsResponse200' + uploadFile: + action: receive + channel: + $ref: '#/channels/upload' + summary: Upload a file + bindings: + http: + method: POST + messages: + - $ref: '#/channels/upload/messages/uploadFileRequest' + reply: + channel: + $ref: '#/channels/upload' + messages: + - $ref: '#/channels/upload/messages/uploadFileResponse200' + getStream: + action: receive + channel: + $ref: '#/channels/stream' + summary: Get a data stream + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/stream' + messages: + - $ref: '#/channels/stream/messages/getStreamResponse200' +components: + schemas: + User: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + id: + type: string + name: + type: string + Post: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + id: + type: string + title: + type: string + content: + type: string \ No newline at end of file diff --git a/test/output/openapi-to-asyncapi/components_and_security.yml b/test/output/openapi-to-asyncapi/components_and_security.yml new file mode 100644 index 00000000..7cc9d246 --- /dev/null +++ b/test/output/openapi-to-asyncapi/components_and_security.yml @@ -0,0 +1,119 @@ +asyncapi: 3.0.0 +info: + title: Components and Security API + version: 1.0.0 + description: An API showcasing various components and security schemes +servers: + example_com_v1: + host: api.example.com + pathname: /v1 + protocol: https +channels: + secure: + address: /secure + messages: + getsecureResponse200: + name: getsecureResponse200 + title: GET response 200 + contentType: application/json + payload: + $ref: '#/components/schemas/SecureResponse' + summary: Successful response + oauth: + address: /oauth + messages: + getoauthResponse200: + name: getoauthResponse200 + title: GET response 200 + contentType: application/json + payload: + $ref: '#/components/schemas/OAuthResponse' + summary: Successful response +operations: + getsecure: + action: receive + channel: + $ref: '#/channels/secure' + summary: Secure endpoint + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/secure' + messages: + - $ref: '#/channels/secure/messages/getsecureResponse200' + getoauth: + action: receive + channel: + $ref: '#/channels/oauth' + summary: OAuth protected endpoint + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/oauth' + messages: + - $ref: '#/channels/oauth/messages/getoauthResponse200' +components: + schemas: + SecureResponse: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + message: + type: string + OAuthResponse: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + data: + type: string + Error: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + code: + type: integer + message: + type: string + securitySchemes: + bearerAuth: + type: http + scheme: bearer + oAuth2: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: 'https://example.com/oauth/authorize' + tokenUrl: 'https://example.com/oauth/token' + availableScopes: + read: Read access + write: Write access + parameters: + limitParam: + description: Maximum number of items to return + location: $message.header#/limit + messages: + NotFound: + name: NotFound + contentType: application/json + payload: + $ref: '#/components/schemas/Error' + summary: Resource not found + messageTraits: + ItemInput: + name: ItemInput + contentType: application/json + HeaderX-Rate-Limit: + headers: + type: object + properties: + X-Rate-Limit: + type: integer + required: + - X-Rate-Limit \ No newline at end of file diff --git a/test/output/openapi-to-asyncapi/external_reference.yml b/test/output/openapi-to-asyncapi/external_reference.yml new file mode 100644 index 00000000..5066a2fe --- /dev/null +++ b/test/output/openapi-to-asyncapi/external_reference.yml @@ -0,0 +1,30 @@ +asyncapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +channels: + test: + address: /test + messages: + gettestResponse200: + name: gettestResponse200 + title: GET response 200 + contentType: application/json + payload: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + $ref: ./external.json + summary: Successful response +operations: + gettest: + action: receive + channel: + $ref: '#/channels/test' + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/test' + messages: + - $ref: '#/channels/test/messages/gettestResponse200' \ No newline at end of file diff --git a/test/output/openapi-to-asyncapi/no-channel-parameter.yml b/test/output/openapi-to-asyncapi/no-channel-parameter.yml new file mode 100644 index 00000000..1a96efd1 --- /dev/null +++ b/test/output/openapi-to-asyncapi/no-channel-parameter.yml @@ -0,0 +1,38 @@ +asyncapi: 3.0.0 +info: + title: Sample Pet Store App + version: 1.0.1 + description: This is a sample server for a pet store. + termsOfService: 'http://example.com/terms/' + contact: + name: API Support + url: 'http://www.example.com/support' + email: support@example.com + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' + tags: + - name: pet + description: Pets operations + externalDocs: + description: Find more info here + url: 'https://example.com' +servers: + gigantic_server_com__port___basePath_: + host: '{username}.gigantic-server.com:{port}' + pathname: '/{basePath}' + protocol: https + description: The production API server + variables: + username: + default: demo + description: >- + this value is assigned by the service provider, in this example + `gigantic-server.com` + port: + enum: + - '8443' + - '443' + default: '8443' + basePath: + default: v2 diff --git a/test/output/openapi-to-asyncapi/operation_and_parameter.yml b/test/output/openapi-to-asyncapi/operation_and_parameter.yml new file mode 100644 index 00000000..55532975 --- /dev/null +++ b/test/output/openapi-to-asyncapi/operation_and_parameter.yml @@ -0,0 +1,167 @@ +asyncapi: 3.0.0 +info: + title: Operations and Parameters API + version: 1.0.0 + description: An API showcasing various operations and parameter types +servers: + example_com_v1: + host: api.example.com + pathname: /v1 + protocol: https +channels: + items: + address: /items + messages: + listItemsResponse200: + name: listItemsResponse200 + title: GET response 200 + contentType: application/json + payload: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: array + items: + $ref: '#/components/schemas/Item' + summary: Successful response + createItemRequest: + name: createItemRequest + title: POST request + contentType: application/json + payload: + $ref: '#/components/schemas/ItemInput' + createItemResponse201: + name: createItemResponse201 + title: POST response 201 + contentType: application/json + payload: + $ref: '#/components/schemas/Item' + summary: Created + parameters: + limit: + description: Maximum number of items to return + location: $message.header#/limit + offset: + description: Number of items to skip + location: $message.header#/offset + 'items_{itemId}': + address: '/items/{itemId}' + messages: + getItemResponse200: + name: getItemResponse200 + title: GET response 200 + contentType: application/json + payload: + $ref: '#/components/schemas/Item' + summary: Successful response + updateItemRequest: + name: updateItemRequest + title: PUT request + contentType: application/json + payload: + $ref: '#/components/schemas/ItemInput' + updateItemResponse200: + name: updateItemResponse200 + title: PUT response 200 + contentType: application/json + payload: + $ref: '#/components/schemas/Item' + summary: Successful response + deleteItemResponse204: + name: deleteItemResponse204 + title: DELETE response 204 + summary: Successful response + parameters: + itemId: + description: Path parameter itemId +operations: + listItems: + action: receive + channel: + $ref: '#/channels/items' + summary: List items + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/items' + messages: + - $ref: '#/channels/items/messages/listItemsResponse200' + createItem: + action: receive + channel: + $ref: '#/channels/items' + summary: Create an item + bindings: + http: + method: POST + messages: + - $ref: '#/channels/items/messages/createItemRequest' + reply: + channel: + $ref: '#/channels/items' + messages: + - $ref: '#/channels/items/messages/createItemResponse201' + getItem: + action: receive + channel: + $ref: '#/channels/items_{itemId}' + summary: Get an item + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/items_{itemId}' + messages: + - $ref: '#/channels/items_{itemId}/messages/getItemResponse200' + updateItem: + action: receive + channel: + $ref: '#/channels/items_{itemId}' + summary: Update an item + bindings: + http: + method: PUT + messages: + - $ref: '#/channels/items_{itemId}/messages/updateItemRequest' + reply: + channel: + $ref: '#/channels/items_{itemId}' + messages: + - $ref: '#/channels/items_{itemId}/messages/updateItemResponse200' + deleteItem: + action: receive + channel: + $ref: '#/channels/items_{itemId}' + summary: Delete an item + bindings: + http: + method: DELETE + reply: + channel: + $ref: '#/channels/items_{itemId}' + messages: + - $ref: '#/channels/items_{itemId}/messages/deleteItemResponse204' +components: + schemas: + Item: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + ItemInput: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + name: + type: string + description: + type: string \ No newline at end of file diff --git a/test/output/openapi-to-asyncapi/operation_and_parameter_client.yml b/test/output/openapi-to-asyncapi/operation_and_parameter_client.yml new file mode 100644 index 00000000..3e3f1678 --- /dev/null +++ b/test/output/openapi-to-asyncapi/operation_and_parameter_client.yml @@ -0,0 +1,167 @@ +asyncapi: 3.0.0 +info: + title: Operations and Parameters API + version: 1.0.0 + description: An API showcasing various operations and parameter types +servers: + example_com_v1: + host: api.example.com + pathname: /v1 + protocol: https +channels: + items: + address: /items + messages: + listItemsResponse200: + name: listItemsResponse200 + title: GET response 200 + contentType: application/json + payload: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: array + items: + $ref: '#/components/schemas/Item' + summary: Successful response + createItemRequest: + name: createItemRequest + title: POST request + contentType: application/json + payload: + $ref: '#/components/schemas/ItemInput' + createItemResponse201: + name: createItemResponse201 + title: POST response 201 + contentType: application/json + payload: + $ref: '#/components/schemas/Item' + summary: Created + parameters: + limit: + description: Maximum number of items to return + location: $message.header#/limit + offset: + description: Number of items to skip + location: $message.header#/offset + 'items_{itemId}': + address: '/items/{itemId}' + messages: + getItemResponse200: + name: getItemResponse200 + title: GET response 200 + contentType: application/json + payload: + $ref: '#/components/schemas/Item' + summary: Successful response + updateItemRequest: + name: updateItemRequest + title: PUT request + contentType: application/json + payload: + $ref: '#/components/schemas/ItemInput' + updateItemResponse200: + name: updateItemResponse200 + title: PUT response 200 + contentType: application/json + payload: + $ref: '#/components/schemas/Item' + summary: Successful response + deleteItemResponse204: + name: deleteItemResponse204 + title: DELETE response 204 + summary: Successful response + parameters: + itemId: + description: Path parameter itemId +operations: + listItems: + action: send + channel: + $ref: '#/channels/items' + summary: List items + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/items' + messages: + - $ref: '#/channels/items/messages/listItemsResponse200' + createItem: + action: send + channel: + $ref: '#/channels/items' + summary: Create an item + bindings: + http: + method: POST + messages: + - $ref: '#/channels/items/messages/createItemRequest' + reply: + channel: + $ref: '#/channels/items' + messages: + - $ref: '#/channels/items/messages/createItemResponse201' + getItem: + action: send + channel: + $ref: '#/channels/items_{itemId}' + summary: Get an item + bindings: + http: + method: GET + reply: + channel: + $ref: '#/channels/items_{itemId}' + messages: + - $ref: '#/channels/items_{itemId}/messages/getItemResponse200' + updateItem: + action: send + channel: + $ref: '#/channels/items_{itemId}' + summary: Update an item + bindings: + http: + method: PUT + messages: + - $ref: '#/channels/items_{itemId}/messages/updateItemRequest' + reply: + channel: + $ref: '#/channels/items_{itemId}' + messages: + - $ref: '#/channels/items_{itemId}/messages/updateItemResponse200' + deleteItem: + action: send + channel: + $ref: '#/channels/items_{itemId}' + summary: Delete an item + bindings: + http: + method: DELETE + reply: + channel: + $ref: '#/channels/items_{itemId}' + messages: + - $ref: '#/channels/items_{itemId}/messages/deleteItemResponse204' +components: + schemas: + Item: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + ItemInput: + schemaFormat: application/vnd.oai.openapi;version=3.0.0 + schema: + type: object + properties: + name: + type: string + description: + type: string \ No newline at end of file