diff --git a/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts b/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts index 2660802865b..ea7f53171a1 100644 --- a/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts +++ b/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts @@ -27,6 +27,7 @@ export interface SingleNamespaceAPIDefinition { export interface APIDefinitionSettings { shouldUseTitleAsName: boolean | undefined; shouldUseUndiscriminatedUnionsWithLiterals: boolean | undefined; + asyncApiMessageNaming: "v1" | "v2" | undefined; } export interface APIDefinitionLocation { diff --git a/packages/cli/configuration/src/generators-yml/convertGeneratorsConfiguration.ts b/packages/cli/configuration/src/generators-yml/convertGeneratorsConfiguration.ts index cfd7c0c6428..0098c1179e3 100644 --- a/packages/cli/configuration/src/generators-yml/convertGeneratorsConfiguration.ts +++ b/packages/cli/configuration/src/generators-yml/convertGeneratorsConfiguration.ts @@ -87,7 +87,11 @@ async function parseAPIConfiguration( origin: undefined, overrides: undefined, audiences: [], - settings: { shouldUseTitleAsName: undefined, shouldUseUndiscriminatedUnionsWithLiterals: undefined } + settings: { + shouldUseTitleAsName: undefined, + shouldUseUndiscriminatedUnionsWithLiterals: undefined, + asyncApiMessageNaming: undefined + } }); } else if (isRawProtobufAPIDefinitionSchema(apiConfiguration)) { apiDefinitions.push({ @@ -100,7 +104,11 @@ async function parseAPIConfiguration( origin: undefined, overrides: apiConfiguration.proto.overrides, audiences: [], - settings: { shouldUseTitleAsName: undefined, shouldUseUndiscriminatedUnionsWithLiterals: undefined } + settings: { + shouldUseTitleAsName: undefined, + shouldUseUndiscriminatedUnionsWithLiterals: undefined, + asyncApiMessageNaming: undefined + } }); } else if (Array.isArray(apiConfiguration)) { for (const definition of apiConfiguration) { @@ -115,7 +123,8 @@ async function parseAPIConfiguration( audiences: [], settings: { shouldUseTitleAsName: undefined, - shouldUseUndiscriminatedUnionsWithLiterals: undefined + shouldUseUndiscriminatedUnionsWithLiterals: undefined, + asyncApiMessageNaming: undefined } }); } else if (isRawProtobufAPIDefinitionSchema(definition)) { @@ -131,7 +140,8 @@ async function parseAPIConfiguration( audiences: [], settings: { shouldUseTitleAsName: undefined, - shouldUseUndiscriminatedUnionsWithLiterals: undefined + shouldUseUndiscriminatedUnionsWithLiterals: undefined, + asyncApiMessageNaming: undefined } }); } else { @@ -145,7 +155,8 @@ async function parseAPIConfiguration( audiences: definition.audiences, settings: { shouldUseTitleAsName: definition.settings?.["use-title"], - shouldUseUndiscriminatedUnionsWithLiterals: definition.settings?.unions === "v1" + shouldUseUndiscriminatedUnionsWithLiterals: definition.settings?.unions === "v1", + asyncApiMessageNaming: definition.settings?.["message-naming"] } }); } @@ -161,7 +172,8 @@ async function parseAPIConfiguration( audiences: apiConfiguration.audiences, settings: { shouldUseTitleAsName: apiConfiguration.settings?.["use-title"], - shouldUseUndiscriminatedUnionsWithLiterals: apiConfiguration.settings?.unions === "v1" + shouldUseUndiscriminatedUnionsWithLiterals: apiConfiguration.settings?.unions === "v1", + asyncApiMessageNaming: apiConfiguration.settings?.["message-naming"] } }); } @@ -183,7 +195,8 @@ async function parseAPIConfiguration( audiences: [], settings: { shouldUseTitleAsName: settings?.["use-title"], - shouldUseUndiscriminatedUnionsWithLiterals: settings?.unions === "v1" + shouldUseUndiscriminatedUnionsWithLiterals: settings?.unions === "v1", + asyncApiMessageNaming: undefined } }); } else if (openapi != null) { @@ -197,7 +210,8 @@ async function parseAPIConfiguration( audiences: [], settings: { shouldUseTitleAsName: openapi.settings?.["use-title"], - shouldUseUndiscriminatedUnionsWithLiterals: openapi.settings?.unions === "v1" + shouldUseUndiscriminatedUnionsWithLiterals: openapi.settings?.unions === "v1", + asyncApiMessageNaming: undefined } }); } @@ -213,7 +227,8 @@ async function parseAPIConfiguration( audiences: [], settings: { shouldUseTitleAsName: settings?.["use-title"], - shouldUseUndiscriminatedUnionsWithLiterals: settings?.unions === "v1" + shouldUseUndiscriminatedUnionsWithLiterals: settings?.unions === "v1", + asyncApiMessageNaming: settings?.["message-naming"] } }); } diff --git a/packages/cli/configuration/src/generators-yml/schemas/APIConfigurationSchema.ts b/packages/cli/configuration/src/generators-yml/schemas/APIConfigurationSchema.ts index 76fa21dff6d..9bb885310cd 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/APIConfigurationSchema.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/APIConfigurationSchema.ts @@ -17,7 +17,12 @@ export const APIDefinitionSettingsSchema = z.object({ ), unions: z .optional(z.enum(["v1"])) - .describe("What version of union generation to use, this will grow over time. Defaults to v0.") + .describe("What version of union generation to use, this will grow over time. Defaults to v0."), + "message-naming": z + .optional(z.enum(["v1", "v2"])) + .describe( + "What version of message naming to use for AsyncAPI messages, this will grow over time. Defaults to v1." + ) }); export type APIDefinitionSettingsSchema = z.infer; diff --git a/packages/cli/openapi-parser/src/asyncapi/options.ts b/packages/cli/openapi-parser/src/asyncapi/options.ts new file mode 100644 index 00000000000..c5c4c2a3fa6 --- /dev/null +++ b/packages/cli/openapi-parser/src/asyncapi/options.ts @@ -0,0 +1,7 @@ +export interface ParseAsyncAPIOptions { + naming: "v1" | "v2"; +} + +export const DEFAULT_PARSE_ASYNCAPI_SETTINGS: ParseAsyncAPIOptions = { + naming: "v1" +}; diff --git a/packages/cli/openapi-parser/src/asyncapi/parse.ts b/packages/cli/openapi-parser/src/asyncapi/parse.ts index 3942e058148..363849bd852 100644 --- a/packages/cli/openapi-parser/src/asyncapi/parse.ts +++ b/packages/cli/openapi-parser/src/asyncapi/parse.ts @@ -15,13 +15,14 @@ import { getExtension } from "../getExtension"; import { ParseOpenAPIOptions } from "../options"; import { convertAvailability } from "../schema/convertAvailability"; import { convertSchema } from "../schema/convertSchemas"; -import { convertUndiscriminatedOneOf } from "../schema/convertUndiscriminatedOneOf"; +import { convertUndiscriminatedOneOf, UndiscriminatedOneOfPrefix } from "../schema/convertUndiscriminatedOneOf"; import { convertSchemaWithExampleToSchema } from "../schema/utils/convertSchemaWithExampleToSchema"; import { isReferenceObject } from "../schema/utils/isReferenceObject"; import { AsyncAPIV2ParserContext } from "./AsyncAPIParserContext"; import { ExampleWebsocketSessionFactory } from "./ExampleWebsocketSessionFactory"; import { FernAsyncAPIExtension } from "./fernExtensions"; import { getFernExamples, WebsocketSessionExampleExtension } from "./getFernExamples"; +import { ParseAsyncAPIOptions } from "./options"; import { AsyncAPIV2 } from "./v2"; export interface AsyncAPIIntermediateRepresentation { @@ -33,16 +34,21 @@ export interface AsyncAPIIntermediateRepresentation { export function parseAsyncAPI({ document, taskContext, - options + options, + asyncApiOptions }: { document: AsyncAPIV2.Document; taskContext: TaskContext; options: ParseOpenAPIOptions; + asyncApiOptions: ParseAsyncAPIOptions; }): AsyncAPIIntermediateRepresentation { const breadcrumbs: string[] = []; if (document.tags?.[0] != null) { breadcrumbs.push(document.tags[0].name); - } else { + } else if (asyncApiOptions.naming !== "v2") { + // In improved naming, we allow you to not have any prefixes here at all + // by not specifying tags. Without useImprovedMessageNaming, and no tags, + // we do still prefix with "Websocket". breadcrumbs.push("websocket"); } @@ -137,7 +143,9 @@ export function parseAsyncAPI({ generatedName: channel.publish.operationId ?? "PublishEvent", event: channel.publish, breadcrumbs, - context + context, + options, + asyncApiOptions }); } @@ -147,7 +155,9 @@ export function parseAsyncAPI({ generatedName: channel.subscribe.operationId ?? "SubscribeEvent", event: channel.subscribe, breadcrumbs, - context + context, + options, + asyncApiOptions }); } @@ -229,22 +239,39 @@ function convertMessageToSchema({ generatedName, event, context, - breadcrumbs + breadcrumbs, + options, + asyncApiOptions }: { breadcrumbs: string[]; generatedName: string; event: AsyncAPIV2.PublishEvent | AsyncAPIV2.SubscribeEvent; context: AsyncAPIV2ParserContext; + options: ParseOpenAPIOptions; + asyncApiOptions: ParseAsyncAPIOptions; }): SchemaWithExample | undefined { if (event.message.oneOf != null) { const subtypes: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[] = []; + const prefixes: UndiscriminatedOneOfPrefix[] = []; for (const schema of event.message.oneOf) { let resolvedSchema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject; + let namePrefix: UndiscriminatedOneOfPrefix = { type: "notFound" }; if (isReferenceObject(schema)) { - resolvedSchema = context.resolveMessageReference(schema).payload; + const resolvedMessage = context.resolveMessageReference(schema); + if (!isReferenceObject(resolvedMessage.payload) && asyncApiOptions.naming === "v2") { + namePrefix = resolvedMessage.name ? { type: "name", name: resolvedMessage.name } : namePrefix; + resolvedSchema = { + ...resolvedMessage.payload, + title: resolvedMessage.name ?? resolvedMessage.payload.title, + description: resolvedMessage.name ?? resolvedMessage.payload.description + }; + } else { + resolvedSchema = resolvedMessage.payload; + } } else { resolvedSchema = schema; } + prefixes.push(namePrefix); subtypes.push(resolvedSchema); } return convertUndiscriminatedOneOf({ @@ -256,7 +283,8 @@ function convertMessageToSchema({ groupName: undefined, wrapAsNullable: false, breadcrumbs, - context + context, + subtypePrefixOverrides: asyncApiOptions.naming === "v2" ? prefixes : [] }); } return undefined; diff --git a/packages/cli/openapi-parser/src/asyncapi/v2/asyncapi.ts b/packages/cli/openapi-parser/src/asyncapi/v2/asyncapi.ts index 41270fe6263..944939db964 100644 --- a/packages/cli/openapi-parser/src/asyncapi/v2/asyncapi.ts +++ b/packages/cli/openapi-parser/src/asyncapi/v2/asyncapi.ts @@ -13,6 +13,7 @@ export interface Document { export interface Message { messageId: string; + name?: string; summary?: string; payload: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject; } diff --git a/packages/cli/openapi-parser/src/parse.ts b/packages/cli/openapi-parser/src/parse.ts index 9df9aa30cbc..38ad5c42f42 100644 --- a/packages/cli/openapi-parser/src/parse.ts +++ b/packages/cli/openapi-parser/src/parse.ts @@ -4,6 +4,7 @@ import { TaskContext } from "@fern-api/task-context"; import { readFile } from "fs/promises"; import yaml from "js-yaml"; import { OpenAPI, OpenAPIV2, OpenAPIV3 } from "openapi-types"; +import { DEFAULT_PARSE_ASYNCAPI_SETTINGS, ParseAsyncAPIOptions } from "./asyncapi/options"; import { parseAsyncAPI } from "./asyncapi/parse"; import { AsyncAPIV2 } from "./asyncapi/v2"; import { loadOpenAPI } from "./loadOpenAPI"; @@ -22,8 +23,8 @@ export interface SpecImportSettings { audiences: string[]; shouldUseTitleAsName: boolean; shouldUseUndiscriminatedUnionsWithLiterals: boolean; + asyncApiNaming?: "v1" | "v2"; } - export interface RawOpenAPIFile { absoluteFilepath: AbsoluteFilePath; contents: string; @@ -99,7 +100,8 @@ export async function parse({ const parsedAsyncAPI = parseAsyncAPI({ document: asyncAPI, taskContext, - options: getParseOptions({ specSettings: spec.settings }) + options: getParseOptions({ specSettings: spec.settings }), + asyncApiOptions: getParseAsyncOptions({ specSettings: spec.settings }) }); if (parsedAsyncAPI.channel != null) { ir.channel.push(parsedAsyncAPI.channel); @@ -142,6 +144,18 @@ function getParseOptions({ }; } +function getParseAsyncOptions({ + specSettings, + overrides +}: { + specSettings?: SpecImportSettings; + overrides?: Partial; +}): ParseAsyncAPIOptions { + return { + naming: overrides?.naming ?? specSettings?.asyncApiNaming ?? DEFAULT_PARSE_ASYNCAPI_SETTINGS.naming + }; +} + function merge( ir1: OpenApiIntermediateRepresentation, ir2: OpenApiIntermediateRepresentation diff --git a/packages/cli/openapi-parser/src/schema/convertUndiscriminatedOneOf.ts b/packages/cli/openapi-parser/src/schema/convertUndiscriminatedOneOf.ts index 6b0637a8971..97fdcf60843 100644 --- a/packages/cli/openapi-parser/src/schema/convertUndiscriminatedOneOf.ts +++ b/packages/cli/openapi-parser/src/schema/convertUndiscriminatedOneOf.ts @@ -15,6 +15,17 @@ import { isReferenceObject } from "./utils/isReferenceObject"; import { isSchemaEqual } from "./utils/isSchemaEqual"; import { convertNumberToSnakeCase } from "./utils/replaceStartingNumber"; +export interface UndiscriminatedOneOfPrefixNotFound { + type: "notFound"; +} + +export interface UndiscriminatedOneOfPrefixName { + type: "name"; + name: string; +} + +export type UndiscriminatedOneOfPrefix = UndiscriminatedOneOfPrefixName | UndiscriminatedOneOfPrefixNotFound; + export function convertUndiscriminatedOneOf({ nameOverride, generatedName, @@ -24,7 +35,8 @@ export function convertUndiscriminatedOneOf({ wrapAsNullable, context, subtypes, - groupName + groupName, + subtypePrefixOverrides }: { nameOverride: string | undefined; generatedName: string; @@ -35,8 +47,9 @@ export function convertUndiscriminatedOneOf({ context: SchemaParserContext; subtypes: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[]; groupName: SdkGroupName | undefined; + subtypePrefixOverrides?: UndiscriminatedOneOfPrefix[]; }): SchemaWithExample { - const subtypePrefixes = getUniqueSubTypeNames({ schemas: subtypes }); + const derivedSubtypePrefixes = getUniqueSubTypeNames({ schemas: subtypes }); const convertedSubtypes = subtypes.flatMap((schema, index) => { if (!isReferenceObject(schema) && schema.enum != null) { @@ -51,7 +64,14 @@ export function convertUndiscriminatedOneOf({ }); }); } - return [convertSchema(schema, false, context, [...breadcrumbs, subtypePrefixes[index] ?? `${index}`])]; + let subtypePrefix = derivedSubtypePrefixes[index]; + if (subtypePrefixOverrides != null) { + const override = subtypePrefixOverrides[index]; + if (override != null && "name" in override) { + subtypePrefix = override.name; + } + } + return [convertSchema(schema, false, context, [...breadcrumbs, subtypePrefix ?? `${index}`])]; }); const uniqueSubtypes: SchemaWithExample[] = []; @@ -279,6 +299,7 @@ function getUniqueSubTypeNames({ } ++i; } + return prefixes; } diff --git a/packages/cli/workspace-loader/src/loadAPIWorkspace.ts b/packages/cli/workspace-loader/src/loadAPIWorkspace.ts index d3d943f47ad..123c5c60bf8 100644 --- a/packages/cli/workspace-loader/src/loadAPIWorkspace.ts +++ b/packages/cli/workspace-loader/src/loadAPIWorkspace.ts @@ -120,7 +120,8 @@ export async function loadAPIWorkspace({ audiences: definition.audiences ?? [], shouldUseTitleAsName: definition.settings?.shouldUseTitleAsName ?? true, shouldUseUndiscriminatedUnionsWithLiterals: - definition.settings?.shouldUseUndiscriminatedUnionsWithLiterals ?? false + definition.settings?.shouldUseUndiscriminatedUnionsWithLiterals ?? false, + asyncApiNaming: definition.settings?.asyncApiMessageNaming } }); } diff --git a/packages/cli/workspace-loader/src/types/Workspace.ts b/packages/cli/workspace-loader/src/types/Workspace.ts index 50597991870..97e366fae61 100644 --- a/packages/cli/workspace-loader/src/types/Workspace.ts +++ b/packages/cli/workspace-loader/src/types/Workspace.ts @@ -37,6 +37,7 @@ export interface SpecImportSettings { audiences: string[]; shouldUseTitleAsName: boolean; shouldUseUndiscriminatedUnionsWithLiterals: boolean; + asyncApiNaming?: "v1" | "v2"; } export interface APIChangelog { files: ChangelogFile[];