import { ConstrainedArrayModel, ConstrainedMetaModel, ConstrainedObjectPropertyModel, ConstrainedUnionModel, FormatHelpers, } from '@asyncapi/modelina'; import { AsyncAPIDocumentInterface, ChannelInterface, MessageInterface, SpecTypesV2, } from '@asyncapi/parser'; import * as fs from 'fs'; import * as path from 'path'; export function getRelativeDir(from: string, to: string): string { return fs.statSync(to).isFile() ? path.dirname(path.relative(from, to)) : path.relative(from, to); } export function getMessageTitle(message: MessageInterface): string { const messageTitle = message.payload()?.title() ?? message.extensions().get('x-parser-message-name')?.value() ?? message.payload()?.extensions().get('x-parser-schema-id')?.value(); if (messageTitle === undefined) { throw new Error('Unable to determine message title.'); } return messageTitle; } // TODO: This is a hack to avoid reserved keywords in property names. // Currently many non-reserved keywords are also being replaced: https://github.com/asyncapi/modelina/issues/1053 export function createDummyReservedKeywordsChecker(): (name: string) => string { return (name: string): string => { return name; }; } /** * Retrieve Service Title from AsyncAPI document. * As Title returned value set in info.title property of document. * @param asyncAPIDocument - AsyncAPI document. */ export function getServiceTitle( asyncAPIDocument: AsyncAPIDocumentInterface, ): string { return asyncAPIDocument.info().title(); } /** * Retrieve Service Id from AsyncAPI document. * As Id returned value set in extension property `x-service-id`. * If extension property is not set - returned Service Title in lower-kebab-case. * @param asyncAPIDocument - AsyncAPI document. */ export function getServiceId( asyncAPIDocument: AsyncAPIDocumentInterface, ): string { const serviceId = asyncAPIDocument.extensions().get('x-service-id')?.value() ?? asyncAPIDocument.info().title(); return FormatHelpers.toParamCase(serviceId); } /** * Retrieves whether a channel is for subscribing or publishing. * Currently, only channels with a single operation are supported. * @param channel - Channel object. */ export function getChannelAction( channel: ChannelInterface, ): 'subscribe' | 'publish' { const operations = channel.operations(); if (operations.length === 0) { throw new Error('Channel has no operations'); } if (operations.length > 1) { throw new Error('Channel has more than one operation'); } const action = operations[0].action(); if (action !== 'subscribe' && action !== 'publish') { throw new Error(`Channel has unsupported action: ${action}`); } return action; } /** * Takes the aggregate type from the (single) channel message from a tag * starting with "aggregate-type:" * @param channel - Channel object. */ export function getChannelAggregateType(channel: ChannelInterface): string { const messages = channel.messages(); if (messages.length === 0) { throw new Error('Channel has no message'); } if (messages.length > 1) { throw new Error('Channel has more than one message'); } const tags = messages[0].tags(); let aggregateType: string | undefined = undefined; for (const key in tags) { const value = tags[key]; if ( typeof value === 'object' && value?.constructor?.name === 'Tag' && value.name().startsWith('aggregate-type:') ) { if (aggregateType !== undefined) { throw new Error('Channel message has more than one aggregate-type tag'); } aggregateType = value.name().replace('aggregate-type:', ''); } } if (aggregateType === undefined) { throw new Error('Channel has no message tag with an aggregate-type'); } return aggregateType; } /** * Takes the aggregate ID field name from the (single) channel message from a * tag starting with "aggregate-id-field:" * @param channel - Channel object. */ export function getChannelAggregateIdField(channel: ChannelInterface): string { const messages = channel.messages(); if (messages.length === 0) { throw new Error('Channel has no message'); } if (messages.length > 1) { throw new Error('Channel has more than one message'); } const tags = messages[0].tags(); let aggregateIdField: string | undefined = undefined; for (const key in tags) { const value = tags[key]; if ( typeof value === 'object' && value?.constructor?.name === 'Tag' && value.name().startsWith('aggregate-id-field:') ) { if (aggregateIdField !== undefined) { throw new Error( 'Channel message has more than one aggregate-id-field tag', ); } aggregateIdField = value.name().replace('aggregate-id-field:', ''); } } if (aggregateIdField === undefined) { throw new Error('Channel has no message tag with an aggregate-id-field'); } return aggregateIdField; } /** * Retrieves the payload model name for a channel. * Currently, only channels with a single message are supported. * @param channel - Channel object. */ export function getPayloadTitle(channel: ChannelInterface): string | undefined { const messages = channel.messages(); if (messages.length === 0) { throw new Error('Channel has no messages'); } if (messages.length > 1) { throw new Error('Channel has more than one message'); } return messages[0].payload()?.title(); } /** * Converts a path into a POSIX path by replacing current path separator with `/`. * @param p - Path to convert. */ export function toPosixPath(p: string): string { return path.posix.join(...p.split(path.sep)); } /** * Generating the path prefix for model. * model name ends with "command" - prefix "commands" * model name ends with "event" - prefix "events" * default prefix "types" * @param modelName - name of the model, defined in json schema, or AsyncAPI document */ export function getModelPathPrefix(modelName: string): string { return modelName ? modelName.toLowerCase().endsWith('command') ? 'commands' : modelName.toLowerCase().endsWith('event') ? 'events' : 'types' : 'types'; } export function removeXParserProperties( obj: SpecTypesV2.AsyncAPISchemaObject, ): SpecTypesV2.AsyncAPISchemaObject { if (Array.isArray(obj)) { return obj.map(removeXParserProperties); } else if (obj !== null && typeof obj === 'object') { const newObj = {}; for (const [key, value] of Object.entries(obj)) { if (!key.startsWith('x-parser-')) { newObj[key] = removeXParserProperties(value); } } return newObj; } else { return obj; } } /** * Replaces the `any` type in a type string. */ export function replaceAnyType(type: string, replacement: string): string { const pattern = /\b(any)\b/g; return type.replace(pattern, replacement); } export function indexOfNullPlain( model: ConstrainedObjectPropertyModel, ): number { if ( model.property instanceof ConstrainedMetaModel && model.property.originalInput?.nullable === true ) { return 1; // Append null to the end of the type. } return -1; } export function indexOfNullUnion( model: ConstrainedObjectPropertyModel, ): number { if (model.property instanceof ConstrainedUnionModel) { return model.property.originalInput?.type?.indexOf('null') ?? -1; } return -1; } export function indexOfNullOneOf( model: ConstrainedObjectPropertyModel, ): number { if (model.property instanceof ConstrainedUnionModel) { const types = model.property.originalInput?.oneOf?.map((m) => m.type); return types?.indexOf('null') ?? -1; } return -1; } export function indexOfNullArrayPlain( model: ConstrainedObjectPropertyModel, ): number { if ( model.property instanceof ConstrainedArrayModel && model.property.originalInput?.items?.nullable === true ) { return 1; // Append null to the end of the type. } return -1; } export function indexOfNullArrayUnion( model: ConstrainedObjectPropertyModel, ): number { if (model.property instanceof ConstrainedArrayModel) { return model.property.originalInput?.items?.type?.indexOf('null') ?? -1; } return -1; } export function indexOfNullArrayOneOf( model: ConstrainedObjectPropertyModel, ): number { if (model.property instanceof ConstrainedArrayModel) { const types = model.property.originalInput?.items?.oneOf?.map( (m) => m.type, ); return types?.indexOf('null') ?? -1; } return -1; } export function getArrayItemNonNullTypes( model: ConstrainedObjectPropertyModel, ): string[] | null { if (model.property instanceof ConstrainedArrayModel) { const itemsType = model.property.originalInput?.items?.type; if (Array.isArray(itemsType)) { const nonNullTypes = itemsType.filter((t: string) => t !== 'null'); return nonNullTypes.length > 0 ? nonNullTypes : null; } } return null; } export function indexOfNullObjectProperty( model: ConstrainedObjectPropertyModel, ): number { if (indexOfNullPlain(model) > -1) { return indexOfNullPlain(model); } if (indexOfNullUnion(model) > -1) { return indexOfNullUnion(model); } if (indexOfNullOneOf(model) > -1) { return indexOfNullOneOf(model); } return -1; } export function indexOfNullArrayProperty( model: ConstrainedObjectPropertyModel, ): number { if (indexOfNullArrayPlain(model) > -1) { return indexOfNullArrayPlain(model); } if (indexOfNullArrayUnion(model) > -1) { return indexOfNullArrayUnion(model); } if (indexOfNullArrayOneOf(model) > -1) { return indexOfNullArrayOneOf(model); } return -1; } export function ensureNullable(type: string, index: number): string { const types = type.split('|').map((t) => t.trim()); if (types[index] === 'any') { types[index] = replaceAnyType(types[index], 'null'); } else if (!types.includes('null')) { types.splice(index, 0, 'null'); } return types.join(' | '); }