Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: allow naming for asyncapi messages to pull message name #4228

Merged
merged 7 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface SingleNamespaceAPIDefinition {
export interface APIDefinitionSettings {
shouldUseTitleAsName: boolean | undefined;
shouldUseUndiscriminatedUnionsWithLiterals: boolean | undefined;
asyncApiMessageNaming: "v1" | "v2" | undefined;
}

export interface APIDefinitionLocation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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) {
Expand All @@ -115,7 +123,8 @@ async function parseAPIConfiguration(
audiences: [],
settings: {
shouldUseTitleAsName: undefined,
shouldUseUndiscriminatedUnionsWithLiterals: undefined
shouldUseUndiscriminatedUnionsWithLiterals: undefined,
asyncApiMessageNaming: undefined
}
});
} else if (isRawProtobufAPIDefinitionSchema(definition)) {
Expand All @@ -131,7 +140,8 @@ async function parseAPIConfiguration(
audiences: [],
settings: {
shouldUseTitleAsName: undefined,
shouldUseUndiscriminatedUnionsWithLiterals: undefined
shouldUseUndiscriminatedUnionsWithLiterals: undefined,
asyncApiMessageNaming: undefined
}
});
} else {
Expand All @@ -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"]
}
});
}
Expand All @@ -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"]
}
});
}
Expand All @@ -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) {
Expand All @@ -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
}
});
}
Expand All @@ -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"]
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof APIDefinitionSettingsSchema>;
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/openapi-parser/src/asyncapi/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface ParseAsyncAPIOptions {
naming: "v1" | "v2";
}

export const DEFAULT_PARSE_ASYNCAPI_SETTINGS: ParseAsyncAPIOptions = {
naming: "v1"
};
44 changes: 36 additions & 8 deletions packages/cli/openapi-parser/src/asyncapi/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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");
}

Expand Down Expand Up @@ -137,7 +143,9 @@ export function parseAsyncAPI({
generatedName: channel.publish.operationId ?? "PublishEvent",
event: channel.publish,
breadcrumbs,
context
context,
options,
asyncApiOptions
});
}

Expand All @@ -147,7 +155,9 @@ export function parseAsyncAPI({
generatedName: channel.subscribe.operationId ?? "SubscribeEvent",
event: channel.subscribe,
breadcrumbs,
context
context,
options,
asyncApiOptions
});
}

Expand Down Expand Up @@ -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({
Expand All @@ -256,7 +283,8 @@ function convertMessageToSchema({
groupName: undefined,
wrapAsNullable: false,
breadcrumbs,
context
context,
subtypePrefixOverrides: asyncApiOptions.naming === "v2" ? prefixes : []
});
}
return undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/cli/openapi-parser/src/asyncapi/v2/asyncapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface Document {

export interface Message {
messageId: string;
name?: string;
summary?: string;
payload: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject;
}
Expand Down
18 changes: 16 additions & 2 deletions packages/cli/openapi-parser/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -22,8 +23,8 @@ export interface SpecImportSettings {
audiences: string[];
shouldUseTitleAsName: boolean;
shouldUseUndiscriminatedUnionsWithLiterals: boolean;
asyncApiNaming?: "v1" | "v2";
}

export interface RawOpenAPIFile {
absoluteFilepath: AbsoluteFilePath;
contents: string;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -142,6 +144,18 @@ function getParseOptions({
};
}

function getParseAsyncOptions({
specSettings,
overrides
}: {
specSettings?: SpecImportSettings;
overrides?: Partial<ParseAsyncAPIOptions>;
}): ParseAsyncAPIOptions {
return {
naming: overrides?.naming ?? specSettings?.asyncApiNaming ?? DEFAULT_PARSE_ASYNCAPI_SETTINGS.naming
};
}

function merge(
ir1: OpenApiIntermediateRepresentation,
ir2: OpenApiIntermediateRepresentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,7 +35,8 @@ export function convertUndiscriminatedOneOf({
wrapAsNullable,
context,
subtypes,
groupName
groupName,
subtypePrefixOverrides
}: {
nameOverride: string | undefined;
generatedName: string;
Expand All @@ -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) {
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -279,6 +299,7 @@ function getUniqueSubTypeNames({
}
++i;
}

return prefixes;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/workspace-loader/src/loadAPIWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli/workspace-loader/src/types/Workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface SpecImportSettings {
audiences: string[];
shouldUseTitleAsName: boolean;
shouldUseUndiscriminatedUnionsWithLiterals: boolean;
asyncApiNaming?: "v1" | "v2";
}
export interface APIChangelog {
files: ChangelogFile[];
Expand Down
Loading