Skip to content

Commit

Permalink
(feature): support asyncapi examples via x-fern-examples (#3042)
Browse files Browse the repository at this point in the history
support asyncapi examples via extension
  • Loading branch information
dsinghvi authored Feb 23, 2024
1 parent eb1f59f commit a06289f
Show file tree
Hide file tree
Showing 32 changed files with 235 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,18 @@ exports[`open api parser assembly parse open api 1`] = `
},
{
"description": null,
"messageType": "subscribe",
"messageType": "publish",
"payload": {
"properties": {
"expires_at": {
"type": "primitive",
"value": {
"type": "datetime",
"value": "2024-01-15T09:30:00.000Z",
},
},
"message_type": {
"type": "literal",
"value": {
"type": "string",
"value": "SessionBegins",
},
"type": "enum",
"value": "SessionBegins",
},
"session_id": {
"terminate_session": {
"type": "primitive",
"value": {
"type": "string",
"value": "session_id",
"type": "boolean",
"value": true,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ channels:
- $ref: "#/components/messages/FinalTranscript"
- $ref: "#/components/messages/SessionTerminated"
- $ref: "#/components/messages/RealtimeError"

x-fern-examples:
- messages:
- messageId: SendAudio
type: publish
payload:
audio_data: "base64-encoded-audio-data"
- messageId: TerminateSession
type: publish
payload:
terminate_session: true

components:
messages:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import {
QueryParameterExample,
SchemaWithExample,
WebsocketHandshakeWithExample,
WebsocketMessageExample,
WebsocketSessionExample
} from "@fern-api/openapi-ir-sdk";
import { isExamplePrimitive } from "../openapi/v3/converters/ExampleEndpointFactory";
import { convertSchema } from "../schema/convertSchemas";
import { ExampleTypeFactory } from "../schema/examples/ExampleTypeFactory";
import { isReferenceObject } from "../schema/utils/isReferenceObject";
import { isSchemaRequired } from "../schema/utils/isSchemaRequired";
import { AsyncAPIV2ParserContext } from "./AsyncAPIParserContext";
import { WebsocketSessionExampleExtension } from "./getFernExamples";

export class ExampleWebsocketSessionFactory {
private exampleTypeFactory: ExampleTypeFactory;
Expand All @@ -19,6 +24,110 @@ export class ExampleWebsocketSessionFactory {
this.schemas = schemas;
}

public buildWebsocketSessionExamplesForExtension({
context,
extensionExamples,
publish,
subscribe,
handshake
}: {
context: AsyncAPIV2ParserContext;
extensionExamples: WebsocketSessionExampleExtension[];
handshake: WebsocketHandshakeWithExample;
publish: SchemaWithExample | undefined;
subscribe: SchemaWithExample | undefined;
}): WebsocketSessionExample[] {
const result: WebsocketSessionExample[] = [];

for (const extensionExample of extensionExamples) {
const queryParameters: QueryParameterExample[] = [];

for (const queryParameter of handshake.queryParameters) {
const required = this.isSchemaRequired(queryParameter.schema);
let example = this.exampleTypeFactory.buildExample({
schema: queryParameter.schema,
example: extensionExample.queryParameters?.[queryParameter.name],
options: {
name: queryParameter.name,
isParameter: true,
ignoreOptionals: true
}
});
if (example != null && !isExamplePrimitive(example)) {
example = undefined;
}
if (required && example == null) {
continue;
} else if (example != null) {
queryParameters.push({
name: queryParameter.name,
value: example
});
}
}

const headers: HeaderExample[] = [];
for (const header of handshake.headers) {
const required = this.isSchemaRequired(header.schema);
let example = this.exampleTypeFactory.buildExample({
schema: header.schema,
example: extensionExample.headers?.[header.name],
options: {
name: header.name,
isParameter: true,
ignoreOptionals: true
}
});
if (example != null && !isExamplePrimitive(example)) {
example = undefined;
}
if (required && example == null) {
continue;
} else if (example != null) {
headers.push({
name: header.name,
value: example
});
}
}

const messages: WebsocketMessageExample[] = [];
for (const messageExample of extensionExample.messages) {
const messageSchema = context.resolveMessageReference({
$ref: `#/components/messages/${messageExample.messageId}`
});
const resolvedSchema = isReferenceObject(messageSchema.payload)
? context.resolveSchemaReference(messageSchema.payload)
: messageSchema.payload;
const example = this.exampleTypeFactory.buildExample({
schema: convertSchema(resolvedSchema, false, context, [messageExample.messageId]),
example: messageExample.value,
options: {
isParameter: false,
ignoreOptionals: true
}
});
if (example != null) {
messages.push({
messageType: messageExample.type,
payload: example,
description: undefined
});
}
}

result.push({
name: extensionExample.summary,
queryParameters,
headers,
description: extensionExample.description,
messages
});
}

return result;
}

public buildWebsocketSessionExample({
publish,
subscribe,
Expand Down
29 changes: 29 additions & 0 deletions packages/cli/openapi-parser/src/asyncapi/fernExtensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Values } from "@fern-api/core-utils";

export const FernAsyncAPIExtension = {
/**
* The x-fern-examples allows you to specify examples for the websocket session.
*
* channels:
* /my-channel:
* subscribe:
* ...
*
* x-fern-examples:
* - name: example-1
* summary: This is an example of a websocket session
* description: This is a description of the example
* messages:
* - type: publish
* messageId: SendMessage
* value:
* data: "1223233"
* - type: subscribe
* messageId: ReceiveMessage
* value:
* data: "12340213"
*/
FERN_EXAMPLES: "x-fern-examples"
} as const;

export type FernAsyncAPIExtension = Values<typeof FernAsyncAPIExtension>;
20 changes: 20 additions & 0 deletions packages/cli/openapi-parser/src/asyncapi/getFernExamples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getExtension } from "../getExtension";
import { FernAsyncAPIExtension } from "./fernExtensions";
import { AsyncAPIV2 } from "./v2";

export interface WebsocketSessionExampleExtension {
summary?: string;
description?: string;
queryParameters?: Record<string, string>;
headers?: Record<string, string>;
messages: {
type: "subscribe" | "publish";
messageId: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
}[];
}

export function getFernExamples(channel: AsyncAPIV2.Channel): WebsocketSessionExampleExtension[] {
return getExtension<WebsocketSessionExampleExtension[]>(channel, FernAsyncAPIExtension.FERN_EXAMPLES) ?? [];
}
41 changes: 31 additions & 10 deletions packages/cli/openapi-parser/src/asyncapi/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
Schema,
SchemaId,
SchemaWithExample,
WebsocketChannel
WebsocketChannel,
WebsocketSessionExample
} from "@fern-api/openapi-ir-sdk";
import { TaskContext } from "@fern-api/task-context";
import { OpenAPIV3 } from "openapi-types";
Expand All @@ -14,6 +15,7 @@ import { convertSchemaWithExampleToSchema } from "../schema/utils/convertSchemaW
import { isReferenceObject } from "../schema/utils/isReferenceObject";
import { AsyncAPIV2ParserContext } from "./AsyncAPIParserContext";
import { ExampleWebsocketSessionFactory } from "./ExampleWebsocketSessionFactory";
import { getFernExamples, WebsocketSessionExampleExtension } from "./getFernExamples";
import { AsyncAPIV2 } from "./v2";

export interface AsyncAPIIntermediateRepresentation {
Expand Down Expand Up @@ -104,14 +106,33 @@ export function parseAsyncAPI({
}

if (headers.length > 0 || queryParameters.length > 0 || publishSchema != null || subscribeSchema != null) {
const example = exampleFactory.buildWebsocketSessionExample({
handshake: {
headers,
queryParameters
},
publish: publishSchema,
subscribe: subscribeSchema
});
// Reads the `x-fern-examples` extension from the channel
const fernExamples: WebsocketSessionExampleExtension[] = getFernExamples(channel);
let examples: WebsocketSessionExample[] = [];
if (fernExamples.length > 0) {
examples = exampleFactory.buildWebsocketSessionExamplesForExtension({
context,
extensionExamples: fernExamples,
handshake: {
headers,
queryParameters
},
publish: publishSchema,
subscribe: subscribeSchema
});
} else {
const autogenExample = exampleFactory.buildWebsocketSessionExample({
handshake: {
headers,
queryParameters
},
publish: publishSchema,
subscribe: subscribeSchema
});
if (autogenExample != null) {
examples.push(autogenExample);
}
}

const tag = document.tags?.[0];
parsedChannel = {
Expand All @@ -136,7 +157,7 @@ export function parseAsyncAPI({
summary: undefined,
path: channelPath,
description: undefined,
examples: example != null ? [example] : []
examples
};
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OpenAPIExtension } from "./extensions";
import { FernOpenAPIExtension } from "./fernExtensions";
import { ReadmeOpenAPIExtension } from "./readmeExtensions";
import { OpenAPIExtension } from "./openapi/v3/extensions/extensions";
import { FernOpenAPIExtension } from "./openapi/v3/extensions/fernExtensions";
import { ReadmeOpenAPIExtension } from "./openapi/v3/extensions/readmeExtensions";

export function getExtension<T>(
object: object,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { EnumSchema, SecurityScheme } from "@fern-api/openapi-ir-sdk";
import { OpenAPIV3 } from "openapi-types";
import { getExtension } from "../../../getExtension";
import { convertEnum } from "../../../schema/convertEnum";
import { convertSchemaWithExampleToSchema } from "../../../schema/utils/convertSchemaWithExampleToSchema";
import { isReferenceObject } from "../../../schema/utils/isReferenceObject";
import { OpenAPIExtension } from "../extensions/extensions";
import { FernOpenAPIExtension } from "../extensions/fernExtensions";
import { getBasicSecuritySchemeNames } from "../extensions/getBasicSecuritySchemeNames";
import { getExtension } from "../extensions/getExtension";
import {
getBasicSecuritySchemeNameAndEnvvar,
SecuritySchemeNames
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Server } from "@fern-api/openapi-ir-sdk";
import { OpenAPIV3 } from "openapi-types";
import { getExtension } from "../../../getExtension";
import { FernOpenAPIExtension } from "../extensions/fernExtensions";
import { getExtension } from "../extensions/getExtension";

export function convertServer(server: OpenAPIV3.ServerObject): Server {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { MultipartSchema, RequestWithExample } from "@fern-api/openapi-ir-sdk";
import { OpenAPIV3 } from "openapi-types";
import { getExtension } from "../../../../getExtension";
import { convertSchema, getSchemaIdFromReference, SCHEMA_REFERENCE_PREFIX } from "../../../../schema/convertSchemas";
import { convertSchemaWithExampleToSchema } from "../../../../schema/utils/convertSchemaWithExampleToSchema";
import { isReferenceObject } from "../../../../schema/utils/isReferenceObject";
import { AbstractOpenAPIV3ParserContext } from "../../AbstractOpenAPIV3ParserContext";
import { FernOpenAPIExtension } from "../../extensions/fernExtensions";
import { getExtension } from "../../extensions/getExtension";
import { getApplicationJsonSchemaMediaObject } from "./getApplicationJsonSchema";

export const APPLICATION_JSON_CONTENT = "application/json";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { assertNever } from "@fern-api/core-utils";
import { ResponseWithExample, StatusCode } from "@fern-api/openapi-ir-sdk";
import { OpenAPIV3 } from "openapi-types";
import { getExtension } from "../../../../getExtension";
import { convertSchema } from "../../../../schema/convertSchemas";
import { convertSchemaWithExampleToSchema } from "../../../../schema/utils/convertSchemaWithExampleToSchema";
import { isReferenceObject } from "../../../../schema/utils/isReferenceObject";
import { AbstractOpenAPIV3ParserContext } from "../../AbstractOpenAPIV3ParserContext";
import { FernOpenAPIExtension } from "../../extensions/fernExtensions";
import { getExtension } from "../../extensions/getExtension";
import { OperationContext } from "../contexts";
import { getApplicationJsonSchemaMediaObject } from "./getApplicationJsonSchema";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { NamedFullExample } from "@fern-api/openapi-ir-sdk";
import { OpenAPIV3 } from "openapi-types";
import { getExtension } from "../../../../getExtension";
import { isReferenceObject } from "../../../../schema/utils/isReferenceObject";
import { OpenAPIExtension } from "../../extensions/extensions";
import { getExtension } from "../../extensions/getExtension";

export interface ApplicationJsonMediaObject {
schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { EndpointWithExample } from "@fern-api/openapi-ir-sdk";
import { OpenAPIV3 } from "openapi-types";
import { getExtension } from "../../../../getExtension";
import { getGeneratedTypeName } from "../../../../schema/utils/getSchemaName";
import { AbstractOpenAPIV3ParserContext } from "../../AbstractOpenAPIV3ParserContext";
import { DummyOpenAPIV3ParserContext } from "../../DummyOpenAPIV3ParserContext";
import { OpenAPIExtension } from "../../extensions/extensions";
import { FernOpenAPIExtension } from "../../extensions/fernExtensions";
import { getExtension } from "../../extensions/getExtension";
import { getFernAvailability } from "../../extensions/getFernAvailability";
import { getFernExamples } from "../../extensions/getFernExamples";
import { OperationContext } from "../contexts";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { EndpointSdkName, EndpointWithExample, HttpMethod, Webhook } from "@fern-api/openapi-ir-sdk";
import { camelCase } from "lodash-es";
import { OpenAPIV3 } from "openapi-types";
import { getExtension } from "../../../../getExtension";
import { AbstractOpenAPIV3ParserContext } from "../../AbstractOpenAPIV3ParserContext";
import { FernOpenAPIExtension } from "../../extensions/fernExtensions";
import { getExtension } from "../../extensions/getExtension";
import { getFernAsyncExtension } from "../../extensions/getFernAsyncExtension";
import { getFernStreamingExtension } from "../../extensions/getFernStreamingExtension";
import { OperationContext, PathItemContext } from "../contexts";
Expand Down
Loading

0 comments on commit a06289f

Please sign in to comment.