diff --git a/demo/examples/tests/examples.yaml b/demo/examples/tests/examples.yaml new file mode 100644 index 000000000..308786427 --- /dev/null +++ b/demo/examples/tests/examples.yaml @@ -0,0 +1,551 @@ +openapi: 3.1.0 +info: + title: Examples Demo API + description: Demonstrates various examples schema combinations. + version: 1.0.0 + license: + name: MIT + url: https://opensource.org/licenses/MIT +security: [] +servers: + - url: http://test.local:8080 + description: Local server +tags: + - name: examples + description: examples +paths: + /requestParameters/example: + get: + tags: + - examples + summary: example in request parameters + description: "description of request parameters example" + parameters: + - name: name + description: name example + in: query + schema: + type: string + example: "John Doe" + - name: age + description: age example + in: query + schema: + type: number + example: 25 + - name: isStudent + description: isStudent example + in: query + schema: + type: [boolean, "null"] + example: false + responses: + "204": + description: no content + + /requestParameters/examples: + get: + tags: + - examples + summary: examples in request parameters + description: "description of request parameters examples" + parameters: + - name: name + description: name example + in: query + schema: + type: string + examples: + example1: + summary: "name example 1" + description: "name example 1 description" + value: "John Doe" + example2: + summary: "name example 2" + description: "name example 2 description" + value: "Jane Smith" + - name: age + description: age example + in: query + schema: + type: number + examples: + example1: + summary: "age example 1" + description: "age example 1 description" + value: 25 + example2: + summary: "age example 2" + description: "age example 2 description" + value: 30 + - name: isStudent + description: isStudent example + in: query + schema: + type: boolean + examples: + example1: + summary: "isStudent example 1" + description: "isStudent example 1 description" + value: true + example2: + summary: "isStudent example 2" + description: "isStudent example 2 description" + value: false + responses: + "204": + description: no content + + /requestParameters/schema/example: + get: + tags: + - examples + summary: example in request parameters schema + description: "description of request parameters schema example" + parameters: + - name: name + description: name example + in: query + schema: + type: string + example: "John Doe" + - name: age + description: age example + in: query + schema: + type: number + example: 25 + - name: isStudent + description: isStudent example + in: query + schema: + type: boolean + example: false + responses: + "204": + description: no content + + /requestParameters/schema/examples: + get: + tags: + - examples + summary: examples in request parameters schema + description: "description of request parameters schema examples" + parameters: + - name: name + description: name example + in: query + schema: + type: string + examples: + - "John Doe" + - "Jane Smith" + - name: age + description: age example + in: query + schema: + type: number + examples: + - 25 + - 30 + - name: isStudent + description: isStudent example + in: query + schema: + type: boolean + examples: + - true + - false + responses: + "204": + description: no content + + /requestBody/mediaTypeObject/example: + post: + tags: + - examples + summary: example of media type object in requestBody + description: "description of requestBody media type object example" + requestBody: + description: "description of requestBody" + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + example: + name: "John Doe" + age: 25 + isStudent: false + responses: + "204": + description: no content + + /requestBody/mediaTypeObject/examples: + post: + tags: + - examples + summary: examples of media type object in requestBody + description: "description of requestBody media type object examples" + requestBody: + description: "description of requestBody" + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + examples: + example1: + value: + name: "John Doe" + age: 25 + isStudent: true + example2: + value: + name: "Jane Smith" + age: 30 + isStudent: false + responses: + "204": + description: no content + + /requestBody/schema/example: + post: + tags: + - examples + summary: example of schema in requestBody + description: "description of requestBody schema example" + requestBody: + description: "description of requestBody" + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + example: + name: "John Doe" + age: 25 + isStudent: false + responses: + "204": + description: no content + + /requestBody/schema/examples: + post: + tags: + - examples + summary: examples of schema in requestBody + description: "description of requestBody schema examples" + requestBody: + description: "description of requestBody" + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + examples: + - name: "John Doe" + age: 25 + isStudent: true + - name: "Jane Smith" + age: 30 + isStudent: false + responses: + "204": + description: no content + + /requestBody/schema/properties/example: + post: + tags: + - examples + summary: example of properties in requestBody schema + description: "description of requestBody schema properties example" + requestBody: + description: "description of requestBody" + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "John Doe" + age: + type: number + example: 25 + isStudent: + type: boolean + example: false + responses: + "204": + description: no content + + /requestBody/schema/properties/examples: + post: + tags: + - examples + summary: examples of properties in requestBody schema + description: "description of requestBody schema properties examples" + requestBody: + description: "description of requestBody" + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + examples: + - "John Doe" + - "Jane Smith" + age: + type: number + examples: + - 25 + - 30 + isStudent: + type: boolean + examples: + - true + - false + responses: + "204": + description: no content + + /response/mediaTypeObject/example: + get: + tags: + - examples + summary: example of media type object in response + description: "description of response media type object example" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + example: + name: "John Doe" + age: 25 + isStudent: false + + /response/mediaTypeObject/examples: + get: + tags: + - examples + summary: examples of media type object in response + description: "description of response media type object examples" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + examples: + example1: + value: + name: "John Doe" + age: 25 + isStudent: true + example2: + value: + name: "Jane Smith" + age: 30 + isStudent: false + + /response/mediaTypeObject/noSchema: + get: + tags: + - examples + summary: no schema in response + description: "description of response media type object no schema" + responses: + "200": + description: successful response + content: + application/json: + + /response/schema/example: + get: + tags: + - examples + summary: example of schema in response + description: "description of response schema example" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + example: + name: "John Doe" + age: 25 + isStudent: false + + /response/schema/examples: + get: + tags: + - examples + summary: examples of schema in response + description: "description of response schema examples" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + examples: + - name: "John Doe" + age: 25 + isStudent: true + - name: "Jane Smith" + age: 30 + isStudent: false + + /response/schema/properties/example: + get: + tags: + - examples + summary: example of schema properties in response + description: "description of response schema properties example" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "John Doe" + age: + type: number + example: 25 + isStudent: + type: boolean + example: false + + /response/schema/properties/examples: + get: + tags: + - examples + summary: examples of schema properties in response + description: "description of response schema properties examples" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + examples: + - "John Doe" + - "Jane Smith" + age: + type: number + examples: + - 25 + - 30 + isStudent: + type: boolean + examples: + - true + - false + + /response/schema/properties/multipleTypes/examples: + get: + tags: + - examples + summary: examples of schema properties with multiple types in response + description: "description of response schema properties with multiple types examples" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: + - string + - "null" + examples: + - "John Doe" + - "Jane Smith" + age: + type: + - number + - "null" + examples: + - 25 + - 30 + isStudent: + type: + - boolean + - "null" + examples: + - true + - false diff --git a/packages/docusaurus-theme-openapi-docs/src/markdown/schema.ts b/packages/docusaurus-theme-openapi-docs/src/markdown/schema.ts index b14bea631..74989da99 100644 --- a/packages/docusaurus-theme-openapi-docs/src/markdown/schema.ts +++ b/packages/docusaurus-theme-openapi-docs/src/markdown/schema.ts @@ -6,6 +6,7 @@ * ========================================================================== */ import { translate } from "@docusaurus/Translate"; + import { OPENAPI_SCHEMA_ITEM } from "../theme/translationIds"; import { SchemaObject } from "../types"; @@ -42,6 +43,10 @@ function prettyName(schema: SchemaObject, circular?: boolean) { // return schema.type; } + if (Array.isArray(schema.type)) { + return schema.type.join(" | "); + } + return schema.title ?? schema.type; } diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Example/_Example.scss b/packages/docusaurus-theme-openapi-docs/src/theme/Example/_Example.scss new file mode 100644 index 000000000..0aef0191b --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Example/_Example.scss @@ -0,0 +1,11 @@ +.openapi-examples { + > .openapi-tabs__schema-container { + margin: 0.4rem; + + @layer docusaurus.infima { + > .margin-top--md { + margin: 0.2rem !important; + } + } + } +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Example/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/Example/index.tsx new file mode 100644 index 000000000..c84f6a644 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Example/index.tsx @@ -0,0 +1,168 @@ +/* ============================================================================ + * Copyright (c) Palo Alto Networks + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import React from "react"; + +import { translate } from "@docusaurus/Translate"; +import { ExampleObject } from "@theme/ParamsItem"; +import SchemaTabs from "@theme/SchemaTabs"; +import TabItem from "@theme/TabItem"; +import { OPENAPI_SCHEMA_ITEM } from "@theme/translationIds"; + +const EXAMPLE_CLASS_NAME = "openapi-example"; +const EXAMPLES_CLASS_NAME = "openapi-examples"; + +type ExampleType = string; +type ExamplesType = Record | string[]; + +/** + * Example Component Props + */ +type ExampleProps = { + example?: ExampleType; + examples?: ExamplesType; +}; + +/** + * Example Component + */ +export const Example = ({ example, examples }: ExampleProps) => { + if (example !== undefined) { + return renderExample(example); + } + if (examples !== undefined) { + return renderExamples(examples); + } + return undefined; +}; + +/** + * Format example value + * + * @param example + * @returns + */ +const formatExample = (example: any) => { + if (typeof example === "object" && example !== null) { + return JSON.stringify(example); + } + return String(example); +}; + +const renderExample = (example: ExampleType) => { + return ( +
+ + {translate({ + id: OPENAPI_SCHEMA_ITEM.EXAMPLE, + message: "Example:", + })}{" "} + + + {formatExample(example)} + +
+ ); +}; + +const renderExamples = (examples: ExamplesType) => { + if (Array.isArray(examples)) { + return renderStringArrayExamples(examples); + } + return renderExamplesRecord(examples); +}; + +/** + * Render string examples + * + * @param examples + * @returns + */ +export function renderStringArrayExamples(examples: string[]) { + if (examples.length === 0) { + return undefined; + } + // If there's only one example, display it without tabs + if (examples.length === 1) { + return renderExample(examples[0]); + } + + // Multiple examples - use tabs + const exampleEntries = examples.reduce( + (acc, example, index) => ({ + ...acc, + [`Example ${index + 1}`]: { + value: example, + }, + }), + {} as Record + ); + return renderExamplesRecord(exampleEntries); +} + +export const renderExamplesRecord = ( + examples: Record +) => { + const exampleEntries = Object.entries(examples); + // If there's only one example, display it without tabs + if (exampleEntries.length === 1) { + const firstExample = exampleEntries[0][1]; + if (!firstExample) { + return undefined; + } + return renderExample(firstExample.value); + } + + return ( +
+ + {translate({ + id: OPENAPI_SCHEMA_ITEM.EXAMPLES, + message: "Examples:", + })} + + + {exampleEntries.map(([exampleName, exampleProperties]) => + renderExampleObject(exampleName, exampleProperties) + )} + +
+ ); +}; + +/** + * Render example object + * + * @param exampleName + * @param exampleProperties + * @returns + */ +const renderExampleObject = ( + exampleName: string, + exampleProperties: ExampleObject +) => { + return ( + // @ts-ignore + + {exampleProperties.summary &&

{exampleProperties.summary}

} + {exampleProperties.description && ( +

+ + {translate({ + id: OPENAPI_SCHEMA_ITEM.DESCRIPTION, + message: "Description:", + })}{" "} + + {exampleProperties.description} +

+ )} + {exampleProperties.value !== undefined + ? renderExample(exampleProperties.value) + : undefined} +
+ ); +}; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ParamsItem/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ParamsItem/index.tsx index 48eac03f7..f588ee9df 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ParamsItem/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ParamsItem/index.tsx @@ -8,16 +8,14 @@ import React from "react"; import { translate } from "@docusaurus/Translate"; - +import { Example } from "@theme/Example"; import Markdown from "@theme/Markdown"; -import SchemaTabs from "@theme/SchemaTabs"; -import TabItem from "@theme/TabItem"; /* eslint-disable import/no-extraneous-dependencies*/ -import clsx from "clsx"; import { OPENAPI_SCHEMA_ITEM } from "@theme/translationIds"; +import clsx from "clsx"; import { getQualifierMessage, getSchemaName } from "../../markdown/schema"; -import { guard, toString } from "../../markdown/utils"; +import { guard } from "../../markdown/utils"; export interface ExampleObject { summary?: string; @@ -31,7 +29,7 @@ export interface Props { param: { description: string; example: any; - examples: Record; + examples: Record | undefined; name: string; required: boolean; deprecated: boolean; @@ -64,19 +62,14 @@ ${enumDescriptions }; function ParamsItem({ param, ...rest }: Props) { - const { - description, - example, - examples, - name, - required, - deprecated, - enumDescriptions, - } = param; + const { description, name, required, deprecated, enumDescriptions } = param; let schema = param.schema; let defaultValue: string | undefined; + let examples = param.examples ?? (schema?.examples as any[] | undefined); + let example = param.example ?? schema?.example; + if (!schema) { schema = { type: "any" }; } @@ -162,60 +155,6 @@ function ParamsItem({ param, ...rest }: Props) { return undefined; } - const renderExample = guard(toString(example), (example) => ( -
- - {translate({ - id: OPENAPI_SCHEMA_ITEM.EXAMPLE, - message: "Example:", - })}{" "} - - {example} -
- )); - - const renderExamples = guard(examples, (examples) => { - const exampleEntries = Object.entries(examples); - return ( - <> - - {translate({ - id: OPENAPI_SCHEMA_ITEM.EXAMPLES, - message: "Examples:", - })} - - - {exampleEntries.map(([exampleName, exampleProperties]) => ( - // @ts-ignore - - {exampleProperties.summary &&

{exampleProperties.summary}

} - {exampleProperties.description && ( -

- - {translate({ - id: OPENAPI_SCHEMA_ITEM.DESCRIPTION, - message: "Description:", - })}{" "} - - {exampleProperties.description} -

- )} -

- - {translate({ - id: OPENAPI_SCHEMA_ITEM.EXAMPLE, - message: "Example:", - })}{" "} - - {exampleProperties.value} -

-
- ))} -
- - ); - }); - return (
@@ -237,8 +176,8 @@ function ParamsItem({ param, ...rest }: Props) { {renderDescription} {renderEnumDescriptions} {renderDefaultValue()} - {renderExample} - {renderExamples} + +
); } diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx index 95a920169..3a24b096c 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx @@ -7,10 +7,8 @@ import React from "react"; -import { translate } from "@docusaurus/Translate"; -import { OPENAPI_SCHEMA_ITEM } from "@theme/translationIds"; - import BrowserOnly from "@docusaurus/BrowserOnly"; +import { translate } from "@docusaurus/Translate"; import Details from "@theme/Details"; import Markdown from "@theme/Markdown"; import MimeTabs from "@theme/MimeTabs"; // Assume these components exist @@ -23,6 +21,7 @@ import SchemaNode from "@theme/Schema"; import SchemaTabs from "@theme/SchemaTabs"; import SkeletonLoader from "@theme/SkeletonLoader"; import TabItem from "@theme/TabItem"; +import { OPENAPI_SCHEMA, OPENAPI_SCHEMA_ITEM } from "@theme/translationIds"; import { MediaTypeObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types"; interface Props { @@ -57,79 +56,86 @@ const ResponseSchemaComponent: React.FC = ({ return ( {mimeTypes.map((mimeType: any) => { - const responseExamples = body.content![mimeType].examples; - const responseExample = body.content![mimeType].example; - const firstBody: any = - body.content![mimeType].schema ?? body.content![mimeType]; + const mediaTypeObject = body.content?.[mimeType]; + const responseExamples = mediaTypeObject?.examples; + const responseExample = mediaTypeObject?.example; + const firstBody = mediaTypeObject?.schema; if ( - firstBody === undefined && - responseExample === undefined && - responseExamples === undefined + !firstBody || + (firstBody.properties && + Object.keys(firstBody.properties).length === 0) ) { - return undefined; - } - - if (firstBody) { return ( // @ts-ignore - - {/* @ts-ignore */} - -
- - - {title} - {body.required === true && ( - - {translate({ - id: OPENAPI_SCHEMA_ITEM.REQUIRED, - message: "required", - })} - - )} - - - - } - > -
- {body.description && ( -
- {body.description} -
- )} -
-
    - -
-
-
- {firstBody && - ExampleFromSchema({ - schema: firstBody, - mimeType: mimeType, - })} - - {responseExamples && - ResponseExamples({ responseExamples, mimeType })} - - {responseExample && - ResponseExample({ responseExample, mimeType })} -
+
+ {translate({ + id: OPENAPI_SCHEMA.NO_SCHEMA, + message: "No schema", + })} +
); } - return undefined; + + return ( + // @ts-ignore + + + {/* @ts-ignore */} + +
+ + + {title} + {body.required === true && ( + + {translate({ + id: OPENAPI_SCHEMA_ITEM.REQUIRED, + message: "required", + })} + + )} + + + + } + > +
+ {body.description && ( +
+ {body.description} +
+ )} +
+
    + +
+
+
+ {firstBody && + ExampleFromSchema({ + schema: firstBody, + mimeType: mimeType, + })} + + {responseExamples && + ResponseExamples({ responseExamples, mimeType })} + + {responseExample && + ResponseExample({ responseExample, mimeType })} +
+
+ ); })}
); diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx index 7b311fc86..aec98c7a1 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx @@ -7,10 +7,11 @@ import React, { ReactNode } from "react"; -import Markdown from "@theme/Markdown"; -import clsx from "clsx"; import { translate } from "@docusaurus/Translate"; +import { Example } from "@theme/Example"; +import Markdown from "@theme/Markdown"; import { OPENAPI_SCHEMA_ITEM } from "@theme/translationIds"; +import clsx from "clsx"; import { guard } from "../../markdown/utils"; @@ -73,6 +74,7 @@ export default function SchemaItem(props: Props) { let schemaDescription; let defaultValue: string | undefined; let example: string | undefined; + let examples: string[] | undefined; let nullable; let enumDescriptions: [string, string][] = []; let constValue: string | undefined; @@ -83,6 +85,7 @@ export default function SchemaItem(props: Props) { enumDescriptions = transformEnumDescriptions(schema["x-enumDescriptions"]); defaultValue = schema.default; example = schema.example; + examples = schema.examples; nullable = schema.nullable || (Array.isArray(schema.type) && schema.type.includes("null")); // support JSON Schema nullable @@ -167,40 +170,6 @@ export default function SchemaItem(props: Props) { return undefined; } - function renderExample() { - if (example !== undefined) { - if (typeof example === "string") { - return ( -
- - {translate({ - id: OPENAPI_SCHEMA_ITEM.EXAMPLE, - message: "Example:", - })}{" "} - - - {example} - -
- ); - } - return ( -
- - {translate({ - id: OPENAPI_SCHEMA_ITEM.EXAMPLE, - message: "Example:", - })}{" "} - - - {JSON.stringify(example)} - -
- ); - } - return undefined; - } - function renderConstValue() { if (constValue !== undefined) { if (typeof constValue === "string") { @@ -260,7 +229,8 @@ export default function SchemaItem(props: Props) { {renderQualifierMessage} {renderConstValue()} {renderDefaultValue()} - {renderExample()} + + {collapsibleSchemaContent ?? collapsibleSchemaContent} ); diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss b/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss index 5e0933346..6e8749ac3 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss +++ b/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss @@ -41,6 +41,8 @@ @use "./CodeSamples/CodeSamples"; /* Markdown Styling */ @use "./Markdown/Details/Details"; +/* Example Styling */ +@use "./Example/Example"; :root { --openapi-required: var(--ifm-color-danger); diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/translationIds.ts b/packages/docusaurus-theme-openapi-docs/src/theme/translationIds.ts index 46e47bab4..b3187c89e 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/translationIds.ts +++ b/packages/docusaurus-theme-openapi-docs/src/theme/translationIds.ts @@ -70,6 +70,10 @@ export const OPENAPI_STATUS_CODES = { SCHEMA_TITLE: "theme.openapi.statusCodes.schemaTitle", }; +export const OPENAPI_SCHEMA = { + NO_SCHEMA: "theme.openapi.schema.noSchema", +}; + export const OPENAPI_SCHEMA_ITEM = { REQUIRED: "theme.openapi.schemaItem.required", DEPRECATED: "theme.openapi.schemaItem.deprecated",