From 647c8824881abd4a019b6762ec3d139ff93dfb9f Mon Sep 17 00:00:00 2001 From: Kaylee <65376239+KayleeWilliams@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:18:05 +0100 Subject: [PATCH] Use OpenAPI examples in snippets --- .changeset/tidy-openapi-examples.md | 5 + apps/fumadocs-example/lib/mdx-components.tsx | 76 +++++++++++-- apps/tanstack/src/components/docs-mdx/api.tsx | 76 +++++++++++-- docs/paths.lock.json | 4 +- docs/reference/openapi.mdx | 12 +- .../leadtype/src/markdown/plugins/openapi.ts | 48 +++++++- packages/leadtype/src/openapi/index.ts | 30 ++++- packages/leadtype/src/openapi/openapi.test.ts | 106 ++++++++++++++++++ 8 files changed, 321 insertions(+), 36 deletions(-) create mode 100644 .changeset/tidy-openapi-examples.md diff --git a/.changeset/tidy-openapi-examples.md b/.changeset/tidy-openapi-examples.md new file mode 100644 index 00000000..e3406587 --- /dev/null +++ b/.changeset/tidy-openapi-examples.md @@ -0,0 +1,5 @@ +--- +"leadtype": patch +--- + +Use named OpenAPI request body examples in generated code samples and avoid repeating them as standalone request examples before the snippets. diff --git a/apps/fumadocs-example/lib/mdx-components.tsx b/apps/fumadocs-example/lib/mdx-components.tsx index accfb82c..ce08a33e 100644 --- a/apps/fumadocs-example/lib/mdx-components.tsx +++ b/apps/fumadocs-example/lib/mdx-components.tsx @@ -299,17 +299,51 @@ function JsonExample({ value }: { value: unknown }) { ); } -function MediaTypeExamples({ media }: { media: ApiMediaType }) { +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isExternalOnlyExampleObject(value: unknown): boolean { + return ( + isRecord(value) && + Object.hasOwn(value, "externalValue") && + !Object.hasOwn(value, "value") + ); +} + +function preferredNamedExampleName(media: ApiMediaType): string | undefined { + const entries = Object.entries(media.examples ?? {}); + if (entries.length === 0) { + return; + } + if ( + Object.hasOwn(media.examples ?? {}, "default") && + !isExternalOnlyExampleObject(media.examples?.default) + ) { + return "default"; + } + return entries.find(([, value]) => !isExternalOnlyExampleObject(value))?.[0]; +} + +function MediaTypeExamples({ + media, + omittedExampleName, +}: { + media: ApiMediaType; + omittedExampleName?: string; +}) { const namedExamples = Object.entries(media.examples ?? {}); if (namedExamples.length > 0) { - return namedExamples.map(([name, value]) => ( -
-

- Example: {name} -

- -
- )); + return namedExamples + .filter(([name]) => name !== omittedExampleName) + .map(([name, value]) => ( +
+

+ Example: {name} +

+ +
+ )); } if (media.example === undefined) { return null; @@ -317,14 +351,27 @@ function MediaTypeExamples({ media }: { media: ApiMediaType }) { return ; } -function MediaType({ media }: { media: ApiMediaType }) { +function MediaType({ + includeSingleExample = true, + media, + omittedExampleName, +}: { + includeSingleExample?: boolean; + media: ApiMediaType; + omittedExampleName?: string; +}) { return (

Content type {media.mediaType}

- + {media.examples || includeSingleExample ? ( + + ) : null} {media.rawSchema === undefined ? null : (
JSON Schema @@ -344,7 +391,12 @@ function ApiRequestBody({ body }: ApiRequestBodyProps) { {body.description ? ` - ${body.description}` : ""}

{body.content.map((media) => ( - + ))}
); diff --git a/apps/tanstack/src/components/docs-mdx/api.tsx b/apps/tanstack/src/components/docs-mdx/api.tsx index c4fff75d..e57be5af 100644 --- a/apps/tanstack/src/components/docs-mdx/api.tsx +++ b/apps/tanstack/src/components/docs-mdx/api.tsx @@ -81,17 +81,51 @@ function JsonExample({ value }: { value: unknown }) { ); } -function MediaTypeExamples({ media }: { media: ApiMediaType }) { +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isExternalOnlyExampleObject(value: unknown): boolean { + return ( + isRecord(value) && + Object.hasOwn(value, "externalValue") && + !Object.hasOwn(value, "value") + ); +} + +function preferredNamedExampleName(media: ApiMediaType): string | undefined { + const entries = Object.entries(media.examples ?? {}); + if (entries.length === 0) { + return; + } + if ( + Object.hasOwn(media.examples ?? {}, "default") && + !isExternalOnlyExampleObject(media.examples?.default) + ) { + return "default"; + } + return entries.find(([, value]) => !isExternalOnlyExampleObject(value))?.[0]; +} + +function MediaTypeExamples({ + media, + omittedExampleName, +}: { + media: ApiMediaType; + omittedExampleName?: string; +}) { const namedExamples = Object.entries(media.examples ?? {}); if (namedExamples.length > 0) { - return namedExamples.map(([name, value]) => ( -
-

- Example: {name} -

- -
- )); + return namedExamples + .filter(([name]) => name !== omittedExampleName) + .map(([name, value]) => ( +
+

+ Example: {name} +

+ +
+ )); } if (media.example === undefined) { return null; @@ -99,14 +133,27 @@ function MediaTypeExamples({ media }: { media: ApiMediaType }) { return ; } -function MediaType({ media }: { media: ApiMediaType }) { +function MediaType({ + includeSingleExample = true, + media, + omittedExampleName, +}: { + includeSingleExample?: boolean; + media: ApiMediaType; + omittedExampleName?: string; +}) { return (

Content type {media.mediaType}

- + {media.examples || includeSingleExample ? ( + + ) : null} {media.rawSchema === undefined ? null : (
JSON Schema @@ -239,7 +286,12 @@ export function ApiRequestBody({ body }: ApiRequestBodyProps) { {body.description ? ` — ${body.description}` : ""}

{body.content.map((media) => ( - + ))} ); diff --git a/docs/paths.lock.json b/docs/paths.lock.json index 38e197c7..8bfec4da 100644 --- a/docs/paths.lock.json +++ b/docs/paths.lock.json @@ -155,7 +155,7 @@ }, { "path": "/docs/reference/openapi", - "hash": "9b7f5adbe50ebf5d" + "hash": "a90449172a503ec5" }, { "path": "/docs/reference/search", @@ -187,7 +187,7 @@ }, { "path": "/docs/rest-api/search/search-docs", - "hash": "0e8e40298bddb63e" + "hash": "e718978ae1e75a8b" }, { "path": "/docs/search/add-search", diff --git a/docs/reference/openapi.mdx b/docs/reference/openapi.mdx index 2e5671ea..5bbc1adb 100644 --- a/docs/reference/openapi.mdx +++ b/docs/reference/openapi.mdx @@ -99,11 +99,13 @@ Every operation page carries the full request/response contract: schemes those requirements reference. - **Parameters** — path/query/header/cookie tables. Nested object and array-item properties flatten into dotted rows such as `results[].title`. -- **Request body and responses** — property tables plus a JSON example. When - the spec provides `example`/`examples` they are used verbatim; otherwise a - representative payload is synthesized from the schema (defaults, enums, and - formats included). The dereferenced **JSON Schema** ships alongside as the - precise contract (disable with `includeSchemas: false`). +- **Request body and responses** — property tables plus response JSON examples. + Request body examples are folded into the generated cURL and `fetch` + snippets instead of repeated as a standalone block. When the spec provides + `example`/`examples` they are used verbatim; otherwise a representative + payload is synthesized from the schema (defaults, enums, and formats + included). The dereferenced **JSON Schema** ships alongside as the precise + contract (disable with `includeSchemas: false`). - **Code samples** — generated cURL and `fetch` snippets with real example bodies, required query parameters, and auth headers derived from the security scheme (`Authorization: Bearer `, API-key headers, and so diff --git a/packages/leadtype/src/markdown/plugins/openapi.ts b/packages/leadtype/src/markdown/plugins/openapi.ts index 398130fa..cc5fe1f1 100644 --- a/packages/leadtype/src/markdown/plugins/openapi.ts +++ b/packages/leadtype/src/markdown/plugins/openapi.ts @@ -77,7 +77,38 @@ function renderJsonExample(value: unknown): Code { return createCodeBlock(text, "json"); } -function renderMediaType(media: OpenApiMediaType): RootContent[] { +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isExternalOnlyExampleObject(value: unknown): boolean { + return ( + isRecord(value) && + Object.hasOwn(value, "externalValue") && + !Object.hasOwn(value, "value") + ); +} + +function preferredNamedExampleName( + media: OpenApiMediaType +): string | undefined { + const entries = Object.entries(media.examples ?? {}); + if (entries.length === 0) { + return; + } + if ( + Object.hasOwn(media.examples ?? {}, "default") && + !isExternalOnlyExampleObject(media.examples?.default) + ) { + return "default"; + } + return entries.find(([, value]) => !isExternalOnlyExampleObject(value))?.[0]; +} + +function renderMediaType( + media: OpenApiMediaType, + options: { includeSingleExample?: boolean; omitExampleName?: string } = {} +): RootContent[] { const nodes: RootContent[] = [ createParagraph(`Content type: ${media.mediaType}`), ]; @@ -96,10 +127,16 @@ function renderMediaType(media: OpenApiMediaType): RootContent[] { } if (media.examples) { for (const [name, value] of Object.entries(media.examples)) { + if (name === options.omitExampleName) { + continue; + } nodes.push(createParagraph(`Example: ${name}`)); nodes.push(renderJsonExample(value)); } - } else if (media.example !== undefined) { + } else if ( + (options.includeSingleExample ?? true) && + media.example !== undefined + ) { nodes.push(createParagraph("Example:")); nodes.push(renderJsonExample(media.example)); } @@ -242,7 +279,12 @@ export function apiRequestBodyToMarkdown(node: MdxNode): RootContent[] { ), ]; for (const media of body.content) { - nodes.push(...renderMediaType(media)); + nodes.push( + ...renderMediaType(media, { + includeSingleExample: false, + omitExampleName: preferredNamedExampleName(media), + }) + ); } return nodes; } diff --git a/packages/leadtype/src/openapi/index.ts b/packages/leadtype/src/openapi/index.ts index 4f1812c9..b4137a51 100644 --- a/packages/leadtype/src/openapi/index.ts +++ b/packages/leadtype/src/openapi/index.ts @@ -1165,13 +1165,39 @@ function sampleHeaders(operation: OpenApiOperation): Map { return headers; } +function preferredMediaExample(media: OpenApiMediaType): unknown { + if (media.examples) { + if ( + Object.hasOwn(media.examples, "default") && + !isExternalOnlyExampleObject(media.examples.default) + ) { + return media.examples.default; + } + for (const value of Object.values(media.examples)) { + if (!isExternalOnlyExampleObject(value)) { + return value; + } + } + } + return media.example; +} + +function isExternalOnlyExampleObject(value: unknown): boolean { + return ( + isRecord(value) && + Object.hasOwn(value, "externalValue") && + !Object.hasOwn(value, "value") + ); +} + function sampleRequestBody(operation: OpenApiOperation): unknown { const media = operation.requestBody?.content[0]; if (!media) { return; } - if (media.example !== undefined) { - return media.example; + const example = preferredMediaExample(media); + if (example !== undefined) { + return example; } if (media.schema) { return buildSchemaExample(media.schema); diff --git a/packages/leadtype/src/openapi/openapi.test.ts b/packages/leadtype/src/openapi/openapi.test.ts index 282c5fef..4aad09ba 100644 --- a/packages/leadtype/src/openapi/openapi.test.ts +++ b/packages/leadtype/src/openapi/openapi.test.ts @@ -673,6 +673,112 @@ paths: expect(curl).toContain('"query": "string"'); }); + it("uses named request body examples in generated snippets", async () => { + const spec = ` +openapi: 3.1.0 +info: { title: Examples, version: 1.0.0 } +servers: + - url: https://api.example.com +paths: + /sites: + post: + operationId: createSite + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [organizationId, name] + properties: + organizationId: + type: string + name: + type: string + region: + type: string + consent: + type: object + properties: + branding: + type: string + examples: + default: + value: + organizationId: org_123 + name: Website + region: us-east-1 + consent: + branding: inth + enterprise: + value: + organizationId: org_456 + name: Enterprise + region: eu-west-1 + consent: + branding: custom + responses: { "201": { description: created } } +`; + const { result } = await generateFixturePages(spec); + const samples = result.pages[0]?.operation.codeSamples ?? []; + const curl = samples.find((sample) => sample.label === "cURL")?.code ?? ""; + const fetch = + samples.find((sample) => sample.label === "JavaScript")?.code ?? ""; + + expect(curl).toContain('"organizationId": "org_123"'); + expect(curl).toContain('"branding": "inth"'); + expect(curl).not.toContain('"organizationId": "string"'); + expect(fetch).toContain('"organizationId": "org_123"'); + expect(fetch).toContain('"branding": "inth"'); + + const converted = await convertMdxToMarkdown( + result.pages[0]?.filePath ?? "", + defaultMarkdownTransforms + ); + expect(converted.markdown).not.toContain("Example: default"); + expect(converted.markdown).toContain("Example: enterprise"); + expect(converted.markdown).toContain('"organizationId": "org_456"'); + expect(converted.markdown).toContain('"organizationId": "org_123"'); + }); + + it("ignores external-only request body examples in generated snippets", async () => { + const spec = ` +openapi: 3.1.0 +info: { title: Examples, version: 1.0.0 } +servers: + - url: https://api.example.com +paths: + /search: + post: + operationId: search + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [query] + properties: + query: + type: string + examples: + default: + summary: External payload + externalValue: https://example.com/search-request.json + responses: { "200": { description: ok } } +`; + const { result } = await generateFixturePages(spec); + const samples = result.pages[0]?.operation.codeSamples ?? []; + const curl = samples.find((sample) => sample.label === "cURL")?.code ?? ""; + const fetch = + samples.find((sample) => sample.label === "JavaScript")?.code ?? ""; + + expect(curl).toContain('"query": "string"'); + expect(curl).not.toContain("externalValue"); + expect(fetch).toContain('"query": "string"'); + expect(fetch).not.toContain("externalValue"); + }); + it("substitutes path parameter examples in generated code sample URLs", async () => { const spec = ` openapi: 3.1.0