From 84078edb2035595169f2a36c306a71ec465f290a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Mon, 17 Mar 2025 17:12:33 +0100 Subject: [PATCH 1/4] Refactor OpenAPI examples code --- .../react-openapi/src/OpenAPICodeSample.tsx | 131 +++++++++++------- .../src/OpenAPIResponseExample.tsx | 1 + .../src/generateSchemaExample.ts | 31 +++-- packages/react-openapi/src/utils.ts | 4 +- 4 files changed, 106 insertions(+), 61 deletions(-) diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index bb6042e52a..ca5db29a0c 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -3,12 +3,14 @@ import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; import { ScalarApiButton } from './ScalarApiButton'; import { StaticSection } from './StaticSection'; import { type CodeSampleInput, codeSampleGenerators } from './code-samples'; -import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample'; +import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; import { getDefaultServerURL } from './util/server'; import { checkIsReference, createStateKey } from './utils'; +const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const; + /** * Display code samples to execute the operation. * It supports the Redocly custom syntax as well (https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples/) @@ -16,6 +18,67 @@ import { checkIsReference, createStateKey } from './utils'; export function OpenAPICodeSample(props: { data: OpenAPIOperationData; context: OpenAPIContextProps; +}) { + const { data } = props; + + // If code samples are disabled at operation level, we don't display the code samples. + if (data.operation['x-codeSamples'] === false) { + return null; + } + + const customCodeSamples = getCustomCodeSamples(props); + + // If code samples are disabled at the top-level and not custom code samples are defined, + // we don't display the code samples. + if (data['x-codeSamples'] === false && !customCodeSamples) { + return null; + } + + const samples = customCodeSamples ?? generateCodeSamples(props); + + if (samples.length === 0) { + return null; + } + + return ( + + } className="openapi-codesample"> + + + + ); +} + +function OpenAPICodeSampleFooter(props: { + data: OpenAPIOperationData; + context: OpenAPIContextProps; +}) { + const { data, context } = props; + const { method, path } = data; + const { specUrl } = context; + const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel']; + + if (hideTryItPanel) { + return null; + } + + if (!validateHttpMethod(method)) { + return null; + } + + return ( +
+ +
+ ); +} + +/** + * Generate code samples for the operation. + */ +function generateCodeSamples(props: { + data: OpenAPIOperationData; + context: OpenAPIContextProps; }) { const { data, context } = props; @@ -56,13 +119,17 @@ export function OpenAPICodeSample(props: { : undefined; const requestBodyContent = requestBodyContentEntries?.[0]; + const requestBodyExamples = requestBodyContent + ? generateMediaTypeExamples(requestBodyContent[1]) + : []; + const input: CodeSampleInput = { url: getDefaultServerURL(data.servers) + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, - body: requestBodyContent ? generateMediaTypeExample(requestBodyContent[1]) : undefined, + body: requestBodyExamples[0]?.value, headers: { ...getSecurityHeaders(data.securities), ...headersObject, @@ -74,7 +141,7 @@ export function OpenAPICodeSample(props: { }, }; - const autoCodeSamples = codeSampleGenerators.map((generator) => ({ + return codeSampleGenerators.map((generator) => ({ key: `default-${generator.id}`, label: generator.label, body: context.renderCodeBlock({ @@ -83,14 +150,24 @@ export function OpenAPICodeSample(props: { }), footer: , })); +} + +/** + * Get custom code samples for the operation. + */ +function getCustomCodeSamples(props: { + data: OpenAPIOperationData; + context: OpenAPIContextProps; +}) { + const { data, context } = props; - // Use custom samples if defined let customCodeSamples: null | Array<{ key: string; label: string; body: React.ReactNode; }> = null; - (['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const).forEach((key) => { + + CUSTOM_CODE_SAMPLES_KEYS.forEach((key) => { const customSamples = data.operation[key]; if (customSamples && Array.isArray(customSamples)) { customCodeSamples = customSamples @@ -102,7 +179,7 @@ export function OpenAPICodeSample(props: { ); }) .map((sample, index) => ({ - key: `redocly-${sample.lang}-${index}`, + key: `custom-sample-${sample.lang}-${index}`, label: sample.label, body: context.renderCodeBlock({ code: sample.source, @@ -113,47 +190,7 @@ export function OpenAPICodeSample(props: { } }); - // Code samples can be disabled at the top-level or at the operation level - // If code samples are defined at the operation level, it will override the top-level setting - const codeSamplesDisabled = - data['x-codeSamples'] === false || data.operation['x-codeSamples'] === false; - const samples = customCodeSamples ?? (!codeSamplesDisabled ? autoCodeSamples : []); - - if (samples.length === 0) { - return null; - } - - return ( - - } className="openapi-codesample"> - - - - ); -} - -function OpenAPICodeSampleFooter(props: { - data: OpenAPIOperationData; - context: OpenAPIContextProps; -}) { - const { data, context } = props; - const { method, path } = data; - const { specUrl } = context; - const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel']; - - if (hideTryItPanel) { - return null; - } - - if (!validateHttpMethod(method)) { - return null; - } - - return ( -
- -
- ); + return customCodeSamples; } function getSecurityHeaders(securities: OpenAPIOperationData['securities']): { diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx index fe7f2666e2..790956f264 100644 --- a/packages/react-openapi/src/OpenAPIResponseExample.tsx +++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx @@ -145,6 +145,7 @@ function OpenAPIResponseMediaType(props: { }) { const { mediaTypeObject, mediaType } = props; const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType }); + console.log(examples, mediaType); const syntax = getSyntaxFromMediaType(mediaType); const firstExample = examples[0]; diff --git a/packages/react-openapi/src/generateSchemaExample.ts b/packages/react-openapi/src/generateSchemaExample.ts index eda8b53450..313157f473 100644 --- a/packages/react-openapi/src/generateSchemaExample.ts +++ b/packages/react-openapi/src/generateSchemaExample.ts @@ -1,4 +1,5 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { checkIsReference } from './utils'; type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }; @@ -28,29 +29,37 @@ export function generateSchemaExample( /** * Generate an example for a media type. */ -export function generateMediaTypeExample( +export function generateMediaTypeExamples( mediaType: OpenAPIV3.MediaTypeObject, options?: GenerateSchemaExampleOptions -): JSONValue | undefined { +): OpenAPIV3.ExampleObject[] { if (mediaType.example) { - return mediaType.example; + return [{ summary: 'default', value: mediaType.example }]; } if (mediaType.examples) { - const key = Object.keys(mediaType.examples)[0]; - if (key) { - const example = mediaType.examples[key]; - if (example) { - return example.value; - } + const { examples } = mediaType; + const keys = Object.keys(examples); + if (keys.length > 0) { + return keys.reduce((result, key) => { + const example = examples[key]; + if (!example || checkIsReference(example)) { + return result; + } + result.push({ + summary: example.summary || key, + value: example.value, + }); + return result; + }, []); } } if (mediaType.schema) { - return generateSchemaExample(mediaType.schema, options); + return [{ summary: 'default', value: generateSchemaExample(mediaType.schema, options) }]; } - return undefined; + return []; } /** Hard limit for rendering circular references */ diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index 0b53baf1d7..b882804fdf 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -1,9 +1,7 @@ import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser'; import { stringifyOpenAPI } from './stringifyOpenAPI'; -export function checkIsReference( - input: unknown -): input is OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject { +export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject { return typeof input === 'object' && !!input && '$ref' in input; } From 34307f4736790b708a728eeff1c5bd273ebb1fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Fri, 21 Mar 2025 13:07:19 +0100 Subject: [PATCH 2/4] Support body examples --- .changeset/green-lizards-change.md | 6 + .../components/DocumentView/OpenAPI/style.css | 6 +- .../react-openapi/src/OpenAPICodeSample.tsx | 179 ++++++++++++------ .../src/OpenAPICodeSampleInteractive.tsx | 114 +++++++++++ .../src/OpenAPIResponseExample.tsx | 1 - packages/react-openapi/src/code-samples.ts | 6 +- .../src/generateSchemaExample.ts | 2 + 7 files changed, 249 insertions(+), 65 deletions(-) create mode 100644 .changeset/green-lizards-change.md create mode 100644 packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx diff --git a/.changeset/green-lizards-change.md b/.changeset/green-lizards-change.md new file mode 100644 index 0000000000..cecc815a25 --- /dev/null +++ b/.changeset/green-lizards-change.md @@ -0,0 +1,6 @@ +--- +"@gitbook/react-openapi": patch +"gitbook": patch +--- + +Support body examples diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index dd0ea9ebca..bfce72cb83 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -392,7 +392,11 @@ } .openapi-codesample-footer { - @apply flex w-full justify-end; + @apply flex gap-4 w-full justify-between; +} + +.openapi-codesample-selectors { + @apply flex flex-row items-center gap-2.5; } /* Path */ diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index ca5db29a0c..10410da57d 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -1,8 +1,12 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { + OpenAPIMediaTypeExamplesBody, + OpenAPIMediaTypeExamplesSelector, +} from './OpenAPICodeSampleInteractive'; import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; import { ScalarApiButton } from './ScalarApiButton'; import { StaticSection } from './StaticSection'; -import { type CodeSampleInput, codeSampleGenerators } from './code-samples'; +import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples'; import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; @@ -49,30 +53,6 @@ export function OpenAPICodeSample(props: { ); } -function OpenAPICodeSampleFooter(props: { - data: OpenAPIOperationData; - context: OpenAPIContextProps; -}) { - const { data, context } = props; - const { method, path } = data; - const { specUrl } = context; - const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel']; - - if (hideTryItPanel) { - return null; - } - - if (!validateHttpMethod(method)) { - return null; - } - - return ( -
- -
- ); -} - /** * Generate code samples for the operation. */ @@ -114,42 +94,119 @@ function generateCodeSamples(props: { const requestBody = !checkIsReference(data.operation.requestBody) ? data.operation.requestBody : undefined; - const requestBodyContentEntries = requestBody?.content - ? Object.entries(requestBody.content) - : undefined; - const requestBodyContent = requestBodyContentEntries?.[0]; - - const requestBodyExamples = requestBodyContent - ? generateMediaTypeExamples(requestBodyContent[1]) - : []; - - const input: CodeSampleInput = { - url: - getDefaultServerURL(data.servers) + - data.path + - (searchParams.size ? `?${searchParams.toString()}` : ''), - method: data.method, - body: requestBodyExamples[0]?.value, - headers: { - ...getSecurityHeaders(data.securities), - ...headersObject, - ...(requestBodyContent - ? { - 'Content-Type': requestBodyContent[0], - } - : undefined), - }, + + const url = + getDefaultServerURL(data.servers) + + data.path + + (searchParams.size ? `?${searchParams.toString()}` : ''); + + const genericHeaders = { + ...getSecurityHeaders(data.securities), + ...headersObject, }; - return codeSampleGenerators.map((generator) => ({ - key: `default-${generator.id}`, - label: generator.label, - body: context.renderCodeBlock({ - code: generator.generate(input), - syntax: generator.syntax, - }), - footer: , - })); + const mediaTypeRendererFactories = Object.entries(requestBody?.content ?? {}).map( + ([mediaType, mediaTypeObject]) => { + return (generator: CodeSampleGenerator) => { + const mediaTypeHeaders = { + ...genericHeaders, + 'Content-Type': mediaType, + }; + return { + mediaType, + element: context.renderCodeBlock({ + code: generator.generate({ + url, + method: data.method, + body: undefined, + headers: mediaTypeHeaders, + }), + syntax: generator.syntax, + }), + examples: generateMediaTypeExamples(mediaTypeObject).map((example) => ({ + example, + element: context.renderCodeBlock({ + code: generator.generate({ + url, + method: data.method, + body: example.value, + headers: mediaTypeHeaders, + }), + syntax: generator.syntax, + }), + })), + } satisfies MediaTypeRenderer; + }; + } + ); + + return codeSampleGenerators.map((generator) => { + if (mediaTypeRendererFactories.length > 0) { + const renderers = mediaTypeRendererFactories.map((generate) => generate(generator)); + return { + key: `default-${generator.id}`, + label: generator.label, + body: , + footer: ( + + ), + }; + } + return { + key: `default-${generator.id}`, + label: generator.label, + body: context.renderCodeBlock({ + code: generator.generate({ + url, + method: data.method, + body: undefined, + headers: genericHeaders, + }), + syntax: generator.syntax, + }), + footer: , + }; + }); +} + +export interface MediaTypeRenderer { + mediaType: string; + element: React.ReactNode; + examples: Array<{ + example: OpenAPIV3.ExampleObject; + element: React.ReactNode; + }>; +} + +function OpenAPICodeSampleFooter(props: { + data: OpenAPIOperationData; + renderers: MediaTypeRenderer[]; + context: OpenAPIContextProps; +}) { + const { data, context, renderers } = props; + const { method, path } = data; + const { specUrl } = context; + const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel']; + const hasMediaTypes = renderers.length > 0; + + if (hideTryItPanel && !hasMediaTypes) { + return null; + } + + if (!validateHttpMethod(method)) { + return null; + } + + return ( +
+ {hasMediaTypes ? ( + + ) : ( + + )} + {!hideTryItPanel && } +
+ ); } /** @@ -185,7 +242,9 @@ function getCustomCodeSamples(props: { code: sample.source, syntax: sample.lang, }), - footer: , + footer: ( + + ), })); } }); diff --git a/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx b/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx new file mode 100644 index 0000000000..095a57b2df --- /dev/null +++ b/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx @@ -0,0 +1,114 @@ +'use client'; +import clsx from 'clsx'; +import { useCallback } from 'react'; +import { useStore } from 'zustand'; +import type { MediaTypeRenderer } from './OpenAPICodeSample'; +import type { OpenAPIOperationData } from './types'; +import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState'; + +function useMediaTypeState(data: OpenAPIOperationData, defaultKey: string) { + const { method, path } = data; + const store = useStore(getOrCreateTabStoreByKey(`media-type-${method}-${path}`, defaultKey)); + if (typeof store.tabKey !== 'string') { + throw new Error('Media type key is not a string'); + } + return { + mediaType: store.tabKey, + setMediaType: useCallback((index: string) => store.setTabKey(index), [store.setTabKey]), + }; +} + +function useMediaTypeSampleIndexState(data: OpenAPIOperationData, mediaType: string) { + const { method, path } = data; + const store = useStore( + getOrCreateTabStoreByKey(`media-type-sample-${mediaType}-${method}-${path}`, 0) + ); + if (typeof store.tabKey !== 'number') { + throw new Error('Example key is not a number'); + } + return { + index: store.tabKey, + setIndex: useCallback((index: number) => store.setTabKey(index), [store.setTabKey]), + }; +} + +export function OpenAPIMediaTypeExamplesSelector(props: { + data: OpenAPIOperationData; + renderers: MediaTypeRenderer[]; +}) { + const { data, renderers } = props; + if (!renderers[0]) { + throw new Error('No renderers provided'); + } + const state = useMediaTypeState(data, renderers[0].mediaType); + const selected = renderers.find((r) => r.mediaType === state.mediaType) || renderers[0]; + + return ( +
+ + +
+ ); +} + +function ExamplesSelector(props: { + data: OpenAPIOperationData; + renderer: MediaTypeRenderer; +}) { + const { data, renderer } = props; + const state = useMediaTypeSampleIndexState(data, renderer.mediaType); + if (renderer.examples.length === 0) { + return null; + } + + return ( + + ); +} + +export function OpenAPIMediaTypeExamplesBody(props: { + data: OpenAPIOperationData; + renderers: MediaTypeRenderer[]; +}) { + const { renderers, data } = props; + if (!renderers[0]) { + throw new Error('No renderers provided'); + } + const mediaTypeState = useMediaTypeState(data, renderers[0].mediaType); + const selected = + renderers.find((r) => r.mediaType === mediaTypeState.mediaType) ?? renderers[0]; + if (selected.examples.length === 0) { + return selected.element; + } + return ; +} + +function ExamplesBody(props: { data: OpenAPIOperationData; renderer: MediaTypeRenderer }) { + const { data, renderer } = props; + const exampleState = useMediaTypeSampleIndexState(data, renderer.mediaType); + const example = renderer.examples[exampleState.index] ?? renderer.examples[0]; + if (!example) { + throw new Error(`No example found for index ${exampleState.index}`); + } + return example.element; +} diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx index 790956f264..fe7f2666e2 100644 --- a/packages/react-openapi/src/OpenAPIResponseExample.tsx +++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx @@ -145,7 +145,6 @@ function OpenAPIResponseMediaType(props: { }) { const { mediaTypeObject, mediaType } = props; const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType }); - console.log(examples, mediaType); const syntax = getSyntaxFromMediaType(mediaType); const firstExample = examples[0]; diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index bb07f1f20c..24bd1ca72c 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -17,7 +17,7 @@ export interface CodeSampleInput { body?: any; } -interface CodeSampleGenerator { +export interface CodeSampleGenerator { id: string; label: string; syntax: string; @@ -240,7 +240,7 @@ const BodyGenerators = { body = `--data '${String(body).replace(/"/g, '')}'`; } else if (isXML(contentType) || isCSV(contentType)) { // We use --data-binary to avoid cURL converting newlines to \r\n - body = `--data-binary $'${stringifyOpenAPI(body).replace(/"/g, '')}'`; + body = `--data-binary $'${stringifyOpenAPI(body).replace(/"/g, '').replace(/\\n/g, '\n')}'`; } else if (isGraphQL(contentType)) { body = `--data '${stringifyOpenAPI(body)}'`; // Set Content-Type to application/json for GraphQL, recommended by GraphQL spec @@ -249,7 +249,7 @@ const BodyGenerators = { // We use --data-binary to avoid cURL converting newlines to \r\n body = `--data-binary '@${String(body)}'`; } else { - body = `--data '${stringifyOpenAPI(body, null, 2)}'`; + body = `--data '${stringifyOpenAPI(body, null, 2).replace(/\\n/g, '\n')}'`; } return { diff --git a/packages/react-openapi/src/generateSchemaExample.ts b/packages/react-openapi/src/generateSchemaExample.ts index 313157f473..01d06565cd 100644 --- a/packages/react-openapi/src/generateSchemaExample.ts +++ b/packages/react-openapi/src/generateSchemaExample.ts @@ -49,6 +49,8 @@ export function generateMediaTypeExamples( result.push({ summary: example.summary || key, value: example.value, + description: example.description, + externalValue: example.externalValue, }); return result; }, []); From eb235a76d543c1a16f1f7ac0dfd563cdf5afd72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Fri, 21 Mar 2025 13:21:36 +0100 Subject: [PATCH 3/4] Fix display in mobile --- .../gitbook/src/components/DocumentView/OpenAPI/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index bfce72cb83..1c2592c3bc 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -392,11 +392,11 @@ } .openapi-codesample-footer { - @apply flex gap-4 w-full justify-between; + @apply flex gap-3 w-full justify-between flex-wrap; } .openapi-codesample-selectors { - @apply flex flex-row items-center gap-2.5; + @apply flex flex-row items-center gap-3 flex-wrap; } /* Path */ From 92ae11434466b8750148e85cd4d404d65326a0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Fri, 21 Mar 2025 14:00:43 +0100 Subject: [PATCH 4/4] Fix display of selector --- packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx b/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx index 095a57b2df..df96330fea 100644 --- a/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx +++ b/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx @@ -67,7 +67,7 @@ function ExamplesSelector(props: { }) { const { data, renderer } = props; const state = useMediaTypeSampleIndexState(data, renderer.mediaType); - if (renderer.examples.length === 0) { + if (renderer.examples.length < 2) { return null; }