Skip to content

Commit

Permalink
fix: redoc examples as snippets and small example parsing simplificat…
Browse files Browse the repository at this point in the history
…ion (#2033)
  • Loading branch information
RohinBhargava authored Jan 17, 2025
1 parent 6562820 commit de7e150
Show file tree
Hide file tree
Showing 23 changed files with 8,960 additions and 8,342 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { extendType } from "../../utils/extendType";
import { isExampleCodeSampleSchemaLanguage } from "../guards/isExampleCodeSampleSchemaLanguage";
import { isExampleCodeSampleSchemaSdk } from "../guards/isExampleCodeSampleSchemaSdk";
import { isExampleResponseBody } from "../guards/isExampleResponseBody";
import { isExampleStreamResponse } from "../guards/isExampleResponseStream";
import { isExampleSseResponseBody } from "../guards/isExampleSseResponseBody";
import { isFileWithData } from "../guards/isFileWithData";
import { isRecord } from "../guards/isRecord";
Expand Down Expand Up @@ -232,6 +233,7 @@ export class XFernEndpointExampleConverterNode extends BaseOpenApiV3_1ConverterN
let responseBody:
| FernRegistry.api.latest.ExampleEndpointResponse
| undefined;

switch (responseBodyNode.contentType) {
case "application/json": {
if (isExampleResponseBody(example.response)) {
Expand All @@ -255,12 +257,15 @@ export class XFernEndpointExampleConverterNode extends BaseOpenApiV3_1ConverterN
break;
}
case "application/octet-stream":
if (typeof example.response === "string") {
if (
!isExampleSseResponseBody(example.response) &&
isExampleStreamResponse(example.response)
) {
responseBody = {
type: "filename",
// TODO: example response should be a filename for now, but we should support different types of file patterns,
// e.g. an S3 link with an audio stream
value: example.response,
type: "stream",
// TODO: example response should be a stream for now, but we should support different types of file patterns,
// e.g. an S3 link with a filename
value: example.response.stream,
};
}
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,26 +107,24 @@ describe("RedocExampleConverterNode", () => {
const result = node.convert();

expect(result).toEqual({
snippets: {
typescript: [
{
name: "TS Example",
language: "TypeScript",
code: "console.log('hello')",
install: undefined,
generated: false,
description: undefined,
},
{
name: undefined,
language: "TypeScript",
code: "console.log('world')",
install: undefined,
generated: false,
description: undefined,
},
],
},
typescript: [
{
name: "TS Example",
language: "TypeScript",
code: "console.log('hello')",
install: undefined,
generated: false,
description: undefined,
},
{
name: undefined,
language: "TypeScript",
code: "console.log('world')",
install: undefined,
generated: false,
description: undefined,
},
],
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,11 @@ export declare namespace RedocExampleConverterNode {

export class RedocExampleConverterNode extends BaseOpenApiV3_1ConverterNode<
unknown,
FernRegistry.api.latest.ExampleEndpointCall
Record<string, FernRegistry.api.latest.CodeSnippet[]>
> {
codeSamples: RedocExampleConverterNode.RedocCodeSample[] | undefined;

constructor(
args: BaseOpenApiV3_1ConverterNodeConstructorArgs<unknown>,
protected path: string,
protected responseStatusCode: number,
protected name: string | undefined
) {
constructor(args: BaseOpenApiV3_1ConverterNodeConstructorArgs<unknown>) {
super(args);
this.safeParse();
}
Expand Down Expand Up @@ -67,7 +62,7 @@ export class RedocExampleConverterNode extends BaseOpenApiV3_1ConverterNode<
});
}

convert(): FernRegistry.api.latest.ExampleEndpointCall | undefined {
convert(): Record<string, FernRegistry.api.latest.CodeSnippet[]> | undefined {
const convertedCodeSamples: Record<
string,
FernRegistry.api.latest.CodeSnippet[]
Expand All @@ -86,17 +81,6 @@ export class RedocExampleConverterNode extends BaseOpenApiV3_1ConverterNode<
if (Object.keys(convertedCodeSamples).length === 0) {
return undefined;
}
return {
path: this.path,
responseStatusCode: this.responseStatusCode,
name: this.name,
description: undefined,
pathParameters: undefined,
queryParameters: undefined,
headers: undefined,
requestBody: undefined,
responseBody: undefined,
snippets: convertedCodeSamples,
};
return convertedCodeSamples;
}
}
12 changes: 12 additions & 0 deletions packages/parsers/src/openapi/3.1/guards/isExampleResponseStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FernDefinition } from "@fern-fern/docs-parsers-fern-definition";

export function isExampleStreamResponse(
input: unknown
): input is FernDefinition.ExampleStreamResponseSchema {
return (
typeof input === "object" &&
input != null &&
"stream" in input &&
Array.isArray(input.stream)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { ParameterBaseObjectConverterNode } from "./parameters";
import { RequestMediaTypeObjectConverterNode } from "./request/RequestMediaTypeObjectConverter.node";
import { ResponseMediaTypeObjectConverterNode } from "./response/ResponseMediaTypeObjectConverter.node";

export const GLOBAL_EXAMPLE_NAME = "";

export declare namespace ExampleObjectConverterNode {
export type Input =
| {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
import { coalesceServers } from "../../utils/3.1/coalesceServers";
import { resolveParameterReference } from "../../utils/3.1/resolveParameterReference";
import { getEndpointId } from "../../utils/getEndpointId";
import { mergeSnippets } from "../../utils/mergeSnippets";
import { mergeXFernAndResponseExamples } from "../../utils/mergeXFernAndResponsesExamples";
import { SecurityRequirementObjectConverterNode } from "../auth/SecurityRequirementObjectConverter.node";
import { AvailabilityConverterNode } from "../extensions/AvailabilityConverter.node";
import { XFernBasePathConverterNode } from "../extensions/XFernBasePathConverter.node";
Expand Down Expand Up @@ -154,19 +156,12 @@ export class OperationObjectConverterNode extends BaseOpenApiV3_1ConverterNode<
}
}

this.redocExamplesNode = new RedocExampleConverterNode(
{
input: this.input,
context: this.context,
accessPath: this.accessPath,
pathId: "x-code-samples",
},
this.path,
Object.keys(this.responses?.responsesByStatusCode ?? {})
.map(Number)
.sort()[0] ?? 200,
undefined
);
this.redocExamplesNode = new RedocExampleConverterNode({
input: this.input,
context: this.context,
accessPath: this.accessPath,
pathId: "x-code-samples",
});

this.requests =
this.input.requestBody != null
Expand Down Expand Up @@ -329,11 +324,18 @@ export class OperationObjectConverterNode extends BaseOpenApiV3_1ConverterNode<
errors: undefined,
};

const examples = [
this.redocExamplesNode?.convert(),
...(this.xFernExamplesNode?.convert() ?? []),
...(responses?.flatMap((response) => response.examples) ?? []),
].filter(isNonNullish);
const examples = mergeXFernAndResponseExamples(
this.xFernExamplesNode?.convert(),
responses?.flatMap((response) => response.examples)
)?.map((example) => {
return {
...example,
snippets: mergeSnippets(
example.snippets,
this.redocExamplesNode?.convert()
),
};
});

if (this.isWebhook) {
if (this.method !== "POST" && this.method !== "GET") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ describe("OperationObjectConverterNode", () => {
},
},
],
examples: [],
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,11 @@ export class RequestBodyObjectConverterNode extends BaseOpenApiV3_1ConverterNode
}

webhookExample(): FernRegistry.api.v1.read.ExampleWebhookPayload | undefined {
return this.requestBodiesByContentType?.[
"application/json"
]?.schema?.example() as FernRegistry.api.v1.read.ExampleWebhookPayload;
return {
payload:
this.requestBodiesByContentType?.[
"application/json"
]?.schema?.example(),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { isObjectSchema } from "../../guards/isObjectSchema";
import { isReferenceObject } from "../../guards/isReferenceObject";
import { ObjectConverterNode } from "../../schemas/ObjectConverter.node";
import { ReferenceConverterNode } from "../../schemas/ReferenceConverter.node";
import { GLOBAL_EXAMPLE_NAME } from "../ExampleObjectConverter.node";
import { MultipartFormDataPropertySchemaConverterNode } from "./MultipartFormDataPropertySchemaConverter.node";

export type RequestContentType = ConstArrayToType<
Expand Down Expand Up @@ -60,6 +61,22 @@ export class RequestMediaTypeObjectConverterNode extends BaseOpenApiV3_1Converte
}

parse(contentType: string | undefined): void {
// This sets examples derived from OpenAPI examples.
// this.input.example is typed as any
// this.input.examples is typed as Record<string, OpenAPIV3_1.ReferenceObject | OpenAPIV3.ExampleObject> | undefined
// In order to create a consistent shape, we add a default string key for an example, which should be treated as a global example
// If there is no global example, we try to generate an example from underlying schemas, which may have examples, or defaults or fallback values
this.examples = {
...(this.input.example != null || this.schema?.example() != null
? {
[GLOBAL_EXAMPLE_NAME]: {
value: this.input.example ?? this.schema?.example(),
},
}
: {}),
...this.input.examples,
};

if (this.input.schema != null) {
if (isReferenceObject(this.input.schema)) {
this.resolvedSchema = resolveReference(
Expand Down Expand Up @@ -130,14 +147,6 @@ export class RequestMediaTypeObjectConverterNode extends BaseOpenApiV3_1Converte
path: this.accessPath,
});
}

this.examples = {
"":
this.input.example != null
? { value: this.input.example }
: this.input.example,
...this.input.examples,
};
}

convert():
Expand Down
Loading

0 comments on commit de7e150

Please sign in to comment.