From a5b742aa03b242c70551d7c83477fec75ce54455 Mon Sep 17 00:00:00 2001 From: "takaoka.daisuke" Date: Tue, 10 Jun 2025 14:47:06 +0900 Subject: [PATCH 1/8] support schema tab in request body --- demo/examples/tests/examples.yaml | 453 ++++++++++++++++++ .../src/theme/RequestSchema/index.tsx | 194 ++++---- 2 files changed, 567 insertions(+), 80 deletions(-) create mode 100644 demo/examples/tests/examples.yaml diff --git a/demo/examples/tests/examples.yaml b/demo/examples/tests/examples.yaml new file mode 100644 index 000000000..80e2287bf --- /dev/null +++ b/demo/examples/tests/examples.yaml @@ -0,0 +1,453 @@ +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 + 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 Doe" + 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 + 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 + 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: false + example2: + value: + name: "Jane Smith" + age: 30 + isStudent: true + 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: false + - name: "Jane Smith" + age: 30 + isStudent: true + 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" + 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" + 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: false + example2: + value: + name: "Jane Smith" + age: 30 + isStudent: true + + /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: false + - name: "Jane Smith" + age: 30 + isStudent: true + + /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 diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx index 9eeceb4bd..cb9118571 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx @@ -11,7 +11,13 @@ import BrowserOnly from "@docusaurus/BrowserOnly"; import Details from "@theme/Details"; import Markdown from "@theme/Markdown"; import MimeTabs from "@theme/MimeTabs"; // Assume these components exist +import { + ExampleFromSchema, + ResponseExample, + ResponseExamples, +} from "@theme/ResponseExamples"; import SchemaNode from "@theme/Schema"; +import SchemaTabs from "@theme/SchemaTabs"; import SkeletonLoader from "@theme/SkeletonLoader"; import TabItem from "@theme/TabItem"; import { MediaTypeObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types"; @@ -38,12 +44,18 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => { return null; } + const SchemaTitle = "Schema"; const mimeTypes = Object.keys(body.content); - - if (mimeTypes.length > 1) { + if (mimeTypes && mimeTypes.length) { return ( {mimeTypes.map((mimeType) => { + const responseExamples = + body.content![mimeType].schema?.examples || + body.content![mimeType].examples; + const responseExample = + body.content![mimeType].schema?.example || + body.content![mimeType].example; const firstBody = body.content![mimeType].schema; if ( firstBody === undefined || @@ -55,93 +67,115 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => { return ( // @ts-ignore -
- -

- {title} - {body.required === true && ( - - required - - )} -

-
- - } - > -
- {body.description && ( -
- {body.description} + + {/* @ts-ignore */} + +
+ +

+ {title} + {body.required === true && ( + + required + + )} +

+
+ + } + > +
+ {body.description && ( +
+ {body.description} +
+ )}
- )} -
-
    - -
-
+
    + +
+ +
+ {firstBody && + ExampleFromSchema({ + schema: firstBody, + mimeType: mimeType, + })} + + {responseExamples && + ResponseExamples({ responseExamples, mimeType })} + + {responseExample && + ResponseExample({ responseExample, mimeType })} + ); })}
); } + return null; + // const randomFirstKey = mimeTypes[0]; + // const firstBody = + // body.content[randomFirstKey].schema ?? body.content![randomFirstKey]; - const randomFirstKey = mimeTypes[0]; - const firstBody = - body.content[randomFirstKey].schema ?? body.content![randomFirstKey]; + // if (firstBody === undefined) { + // return null; + // } - if (firstBody === undefined) { - return null; - } - - return ( - - {/* @ts-ignore */} - -
- -

- {title} - {firstBody.type === "array" && ( - array - )} - {body.required && ( - - required - - )} -

-
- - } - > -
- {body.description && ( -
- {body.description} -
- )} -
-
    - -
-
-
-
- ); + // return ( + // + // {/* @ts-ignore */} + // + //
+ // + //

+ // {title} + // {firstBody.type === "array" && ( + // array + // )} + // {body.required && ( + // + // required + // + // )} + //

+ //
+ // + // } + // > + //
+ // {body.description && ( + //
+ // {body.description} + //
+ // )} + //
+ //
    + // + //
+ //
+ //
+ //
+ // ); }; const RequestSchema: React.FC = (props) => { From b5442067998f764a45a71911e229de1d5a942ece Mon Sep 17 00:00:00 2001 From: "takaoka.daisuke" Date: Tue, 10 Jun 2025 18:42:55 +0900 Subject: [PATCH 2/8] add RequestExamples --- demo/examples/tests/examples.yaml | 2 + .../src/theme/RequestExamples/index.tsx | 249 ++++++++++++++++++ .../src/theme/RequestSchema/index.tsx | 101 +++---- 3 files changed, 286 insertions(+), 66 deletions(-) create mode 100644 packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx diff --git a/demo/examples/tests/examples.yaml b/demo/examples/tests/examples.yaml index 80e2287bf..b7556b555 100644 --- a/demo/examples/tests/examples.yaml +++ b/demo/examples/tests/examples.yaml @@ -238,6 +238,7 @@ paths: description: "description of requestBody schema properties example" requestBody: description: "description of requestBody" + required: true content: application/json: schema: @@ -264,6 +265,7 @@ paths: description: "description of requestBody schema properties examples" requestBody: description: "description of requestBody" + required: true content: application/json: schema: diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx new file mode 100644 index 000000000..87781f8f0 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx @@ -0,0 +1,249 @@ +/* ============================================================================ + * 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 Markdown from "@theme/Markdown"; +import ResponseSamples from "@theme/ResponseSamples"; +import TabItem from "@theme/TabItem"; +import { sampleResponseFromSchema } from "docusaurus-plugin-openapi-docs/lib/openapi/createResponseExample"; +import format from "xml-formatter"; + +export function json2xml(o: Record, tab: string): string { + const toXml = (v: any, name: string, ind: string): string => { + let xml = ""; + if (v instanceof Array) { + for (let i = 0, n = v.length; i < n; i++) { + xml += ind + toXml(v[i], name, ind + "\t") + "\n"; + } + } else if (typeof v === "object") { + let hasChild = false; + xml += ind + "<" + name; + for (const m in v) { + if (m.charAt(0) === "@") { + xml += " " + m.substr(1) + '="' + v[m].toString() + '"'; + } else { + hasChild = true; + } + } + xml += hasChild ? ">" : "/>"; + if (hasChild) { + for (const m2 in v) { + if (m2 === "#text") xml += v[m2]; + else if (m2 === "#cdata") xml += ""; + else if (m2.charAt(0) !== "@") xml += toXml(v[m2], m2, ind + "\t"); + } + xml += + (xml.charAt(xml.length - 1) === "\n" ? ind : "") + ""; + } + } else { + xml += ind + "<" + name + ">" + v.toString() + ""; + } + return xml; + }; + let xml = ""; + for (const m3 in o) xml += toXml(o[m3], m3, ""); + return tab ? xml.replace(/\t/g, tab) : xml.replace(/\t|\n/g, ""); +} + +interface RequestExamplesProps { + requestExamples: any; + mimeType: string; +} +export const RequestExamples: React.FC = ({ + requestExamples, + mimeType, +}): any => { + let language = "shell"; + if (mimeType.endsWith("json")) language = "json"; + if (mimeType.endsWith("xml")) language = "xml"; + + // Map response examples to an array of TabItem elements + const examplesArray = Object.entries(requestExamples).map( + ([exampleName, exampleValue]: any) => { + const isObject = typeof exampleValue.value === "object"; + const responseExample = isObject + ? JSON.stringify(exampleValue.value, null, 2) + : exampleValue.value; + + return ( + // @ts-ignore + + {exampleValue.summary && ( + + {exampleValue.summary} + + )} + + + ); + } + ); + + return examplesArray; +}; + +interface RequestExampleProps { + requestExample: any; + mimeType: string; +} + +export const RequestExample: React.FC = ({ + requestExample, + mimeType, +}) => { + let language = "shell"; + if (mimeType.endsWith("json")) { + language = "json"; + } + if (mimeType.endsWith("xml")) { + language = "xml"; + } + + const isObject = typeof requestExample === "object"; + const exampleContent = isObject + ? JSON.stringify(requestExample, null, 2) + : requestExample; + + return ( + // @ts-ignore + + {requestExample.summary && ( + + {requestExample.summary} + + )} + + + ); +}; + +interface ExampleFromSchemaProps { + schema: any; + mimeType: string; +} + +export const ExampleFromSchema: React.FC = ({ + schema, + mimeType, +}) => { + const example = sampleResponseFromSchema(schema); + + if (mimeType.endsWith("xml")) { + let responseExampleObject; + try { + responseExampleObject = JSON.parse(JSON.stringify(example)); + } catch { + return null; + } + + if (typeof responseExampleObject === "object") { + let xmlExample; + try { + xmlExample = format(json2xml(responseExampleObject, ""), { + indentation: " ", + lineSeparator: "\n", + collapseContent: true, + }); + } catch { + const xmlExampleWithRoot = { root: responseExampleObject }; + try { + xmlExample = format(json2xml(xmlExampleWithRoot, ""), { + indentation: " ", + lineSeparator: "\n", + collapseContent: true, + }); + } catch { + xmlExample = json2xml(responseExampleObject, ""); + } + } + return ( + // @ts-ignore + + + + ); + } + } + + if (typeof example === "object" || typeof example === "string") { + return ( + // @ts-ignore + + + + ); + } + + return null; +}; + +export const RequestSchemaExample: React.FC = ({ + requestExample, + mimeType, +}) => { + let language = "shell"; + if (mimeType.endsWith("json")) { + language = "json"; + } + if (mimeType.endsWith("xml")) { + language = "xml"; + } + + const isObject = typeof requestExample === "object"; + const exampleContent = isObject + ? JSON.stringify(requestExample, null, 2) + : requestExample; + + return ( + // @ts-ignore + + {requestExample.summary && ( + + {requestExample.summary} + + )} + + + ); +}; + +export const RequestSchemaExamples: React.FC = ({ + requestExamples, + mimeType, +}) => { + let language = "shell"; + if (mimeType.endsWith("json")) language = "json"; + if (mimeType.endsWith("xml")) language = "xml"; + + // Map response examples to an array of TabItem elements + const examplesArray = requestExamples.map((example: any, i: number) => { + const exampleName = `Example ${i + 1}`; + const isObject = typeof example === "object"; + const responseExample = isObject + ? JSON.stringify(example, null, 2) + : example; + + return ( + // @ts-ignore + + + + ); + }); + + return examplesArray; +}; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx index cb9118571..cb90cf4b5 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx @@ -13,9 +13,11 @@ import Markdown from "@theme/Markdown"; import MimeTabs from "@theme/MimeTabs"; // Assume these components exist import { ExampleFromSchema, - ResponseExample, - ResponseExamples, -} from "@theme/ResponseExamples"; + RequestExample, + RequestExamples, + RequestSchemaExample, + RequestSchemaExamples, +} from "@theme/RequestExamples"; import SchemaNode from "@theme/Schema"; import SchemaTabs from "@theme/SchemaTabs"; import SkeletonLoader from "@theme/SkeletonLoader"; @@ -50,12 +52,12 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => { return ( {mimeTypes.map((mimeType) => { - const responseExamples = - body.content![mimeType].schema?.examples || - body.content![mimeType].examples; - const responseExample = - body.content![mimeType].schema?.example || - body.content![mimeType].example; + const responseMimeExamples = body.content![mimeType].examples; + const responseMimeExample = body.content![mimeType].example; + const responseSchemaExamples = + body.content![mimeType].schema?.examples; + const responseSchemaExample = body.content![mimeType].schema?.example; + const firstBody = body.content![mimeType].schema; if ( firstBody === undefined || @@ -84,10 +86,10 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => {

{title} - {body.required === true && ( - + {body.required && ( + required - + )}

@@ -114,11 +116,27 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => { mimeType: mimeType, })} - {responseExamples && - ResponseExamples({ responseExamples, mimeType })} - - {responseExample && - ResponseExample({ responseExample, mimeType })} + {responseMimeExamples + ? RequestExamples({ + requestExamples: responseMimeExamples, + mimeType, + }) + : responseMimeExample + ? RequestExample({ + requestExample: responseMimeExample, + mimeType, + }) + : responseSchemaExamples + ? RequestSchemaExamples({ + requestExamples: responseSchemaExamples, + mimeType, + }) + : responseSchemaExample + ? RequestSchemaExample({ + requestExample: responseSchemaExample, + mimeType, + }) + : null} ); @@ -127,55 +145,6 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => { ); } return null; - // const randomFirstKey = mimeTypes[0]; - // const firstBody = - // body.content[randomFirstKey].schema ?? body.content![randomFirstKey]; - - // if (firstBody === undefined) { - // return null; - // } - - // return ( - // - // {/* @ts-ignore */} - // - //
- // - //

- // {title} - // {firstBody.type === "array" && ( - // array - // )} - // {body.required && ( - // - // required - // - // )} - //

- //
- // - // } - // > - //
- // {body.description && ( - //
- // {body.description} - //
- // )} - //
- //
    - // - //
- //
- //
- //
- // ); }; const RequestSchema: React.FC = (props) => { From 808e9b11b1160b57f57f5890d379d223a74fc370 Mon Sep 17 00:00:00 2001 From: "takaoka.daisuke" Date: Wed, 11 Jun 2025 09:45:14 +0900 Subject: [PATCH 3/8] refactor Examples --- .../src/theme/Examples/index.tsx | 244 +++++++++++++++++ .../src/theme/RequestExamples/index.tsx | 251 ++---------------- .../src/theme/RequestSchema/index.tsx | 50 ++-- .../src/theme/ResponseExamples/index.tsx | 195 ++------------ .../src/theme/ResponseSchema/index.tsx | 9 +- 5 files changed, 310 insertions(+), 439 deletions(-) create mode 100644 packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx new file mode 100644 index 000000000..172bedff4 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx @@ -0,0 +1,244 @@ +/* ============================================================================ + * 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 Markdown from "@theme/Markdown"; +import ResponseSamples from "@theme/ResponseSamples"; +import TabItem from "@theme/TabItem"; +import { sampleResponseFromSchema } from "docusaurus-plugin-openapi-docs/lib/openapi/createResponseExample"; +import format from "xml-formatter"; + +export function json2xml(o: Record, tab: string): string { + const toXml = (v: any, name: string, ind: string): string => { + let xml = ""; + if (v instanceof Array) { + for (let i = 0, n = v.length; i < n; i++) { + xml += ind + toXml(v[i], name, ind + "\t") + "\n"; + } + } else if (typeof v === "object") { + let hasChild = false; + xml += ind + "<" + name; + for (const m in v) { + if (m.charAt(0) === "@") { + xml += " " + m.substr(1) + '="' + v[m].toString() + '"'; + } else { + hasChild = true; + } + } + xml += hasChild ? ">" : "/>"; + if (hasChild) { + for (const m2 in v) { + if (m2 === "#text") xml += v[m2]; + else if (m2 === "#cdata") xml += ""; + else if (m2.charAt(0) !== "@") xml += toXml(v[m2], m2, ind + "\t"); + } + xml += + (xml.charAt(xml.length - 1) === "\n" ? ind : "") + ""; + } + } else { + xml += ind + "<" + name + ">" + v.toString() + ""; + } + return xml; + }; + let xml = ""; + for (const m3 in o) xml += toXml(o[m3], m3, ""); + return tab ? xml.replace(/\t/g, tab) : xml.replace(/\t|\n/g, ""); +} + +export function getLanguageFromMimeType(mimeType: string): string { + let language = "shell"; + if (mimeType.endsWith("json")) language = "json"; + if (mimeType.endsWith("xml")) language = "xml"; + return language; +} + +export interface MimeExampleProps { + example: any; + mimeType: string; +} + +export const MimeExample: React.FC = ({ + example, + mimeType, +}) => { + const language = getLanguageFromMimeType(mimeType); + + const isObject = typeof example === "object"; + const exampleContent = isObject ? JSON.stringify(example, null, 2) : example; + + return ( + // @ts-ignore + + {example.summary && ( + + {example.summary} + + )} + + + ); +}; + +export interface MimeExamplesProps { + examples: any; + mimeType: string; +} + +export const MimeExamples: React.FC = ({ + examples, + mimeType, +}): any => { + const language = getLanguageFromMimeType(mimeType); + + // Map examples to an array of TabItem elements + const examplesArray = Object.entries(examples).map( + ([exampleName, exampleValue]: any) => { + const isObject = typeof exampleValue.value === "object"; + const exampleContent = isObject + ? JSON.stringify(exampleValue.value, null, 2) + : exampleValue.value; + + return ( + // @ts-ignore + + {exampleValue.summary && ( + + {exampleValue.summary} + + )} + + + ); + } + ); + + return examplesArray; +}; + +export interface SchemaExampleProps { + example: any; + mimeType: string; +} + +export const SchemaExample: React.FC = ({ + example, + mimeType, +}) => { + const language = getLanguageFromMimeType(mimeType); + + const isObject = typeof example === "object"; + const exampleContent = isObject ? JSON.stringify(example, null, 2) : example; + + return ( + // @ts-ignore + + {example.summary && ( + + {example.summary} + + )} + + + ); +}; + +export interface SchemaExamplesProps { + examples: any[]; + mimeType: string; +} + +export const SchemaExamples: React.FC = ({ + examples, + mimeType, +}) => { + const language = getLanguageFromMimeType(mimeType); + + // Map examples to an array of TabItem elements + const examplesArray = examples.map((example: any, i: number) => { + const exampleName = `Example ${i + 1}`; + const isObject = typeof example === "object"; + const exampleContent = isObject + ? JSON.stringify(example, null, 2) + : example; + + return ( + // @ts-ignore + + + + ); + }); + + return examplesArray; +}; + +export interface ExampleFromSchemaProps { + schema: any; + mimeType: string; +} + +export const ExampleFromSchema: React.FC = ({ + schema, + mimeType, +}) => { + const example = sampleResponseFromSchema(schema); + + if (mimeType.endsWith("xml")) { + let exampleObject; + try { + exampleObject = JSON.parse(JSON.stringify(example)); + } catch { + return null; + } + + if (typeof exampleObject === "object") { + let xmlExample; + try { + xmlExample = format(json2xml(exampleObject, ""), { + indentation: " ", + lineSeparator: "\n", + collapseContent: true, + }); + } catch { + const xmlExampleWithRoot = { root: exampleObject }; + try { + xmlExample = format(json2xml(xmlExampleWithRoot, ""), { + indentation: " ", + lineSeparator: "\n", + collapseContent: true, + }); + } catch { + xmlExample = json2xml(exampleObject, ""); + } + } + return ( + // @ts-ignore + + + + ); + } + } + + if (typeof example === "object" || typeof example === "string") { + return ( + // @ts-ignore + + + + ); + } + + return null; +}; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx index 87781f8f0..8ef6da848 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx @@ -7,243 +7,44 @@ import React from "react"; -import Markdown from "@theme/Markdown"; -import ResponseSamples from "@theme/ResponseSamples"; -import TabItem from "@theme/TabItem"; -import { sampleResponseFromSchema } from "docusaurus-plugin-openapi-docs/lib/openapi/createResponseExample"; -import format from "xml-formatter"; - -export function json2xml(o: Record, tab: string): string { - const toXml = (v: any, name: string, ind: string): string => { - let xml = ""; - if (v instanceof Array) { - for (let i = 0, n = v.length; i < n; i++) { - xml += ind + toXml(v[i], name, ind + "\t") + "\n"; - } - } else if (typeof v === "object") { - let hasChild = false; - xml += ind + "<" + name; - for (const m in v) { - if (m.charAt(0) === "@") { - xml += " " + m.substr(1) + '="' + v[m].toString() + '"'; - } else { - hasChild = true; - } - } - xml += hasChild ? ">" : "/>"; - if (hasChild) { - for (const m2 in v) { - if (m2 === "#text") xml += v[m2]; - else if (m2 === "#cdata") xml += ""; - else if (m2.charAt(0) !== "@") xml += toXml(v[m2], m2, ind + "\t"); - } - xml += - (xml.charAt(xml.length - 1) === "\n" ? ind : "") + ""; - } - } else { - xml += ind + "<" + name + ">" + v.toString() + ""; - } - return xml; - }; - let xml = ""; - for (const m3 in o) xml += toXml(o[m3], m3, ""); - return tab ? xml.replace(/\t/g, tab) : xml.replace(/\t|\n/g, ""); -} - -interface RequestExamplesProps { - requestExamples: any; - mimeType: string; -} -export const RequestExamples: React.FC = ({ - requestExamples, - mimeType, -}): any => { - let language = "shell"; - if (mimeType.endsWith("json")) language = "json"; - if (mimeType.endsWith("xml")) language = "xml"; - - // Map response examples to an array of TabItem elements - const examplesArray = Object.entries(requestExamples).map( - ([exampleName, exampleValue]: any) => { - const isObject = typeof exampleValue.value === "object"; - const responseExample = isObject - ? JSON.stringify(exampleValue.value, null, 2) - : exampleValue.value; - - return ( - // @ts-ignore - - {exampleValue.summary && ( - - {exampleValue.summary} - - )} - - - ); - } - ); - - return examplesArray; -}; - -interface RequestExampleProps { - requestExample: any; - mimeType: string; -} - -export const RequestExample: React.FC = ({ - requestExample, +import { + MimeExamples, + MimeExample, + SchemaExample, + SchemaExamples, + MimeExampleProps, + MimeExamplesProps, + SchemaExampleProps, + SchemaExamplesProps, + ExampleFromSchema, +} from "@theme/Examples"; + +export const RequestMimeExample: React.FC = ({ + example, mimeType, }) => { - let language = "shell"; - if (mimeType.endsWith("json")) { - language = "json"; - } - if (mimeType.endsWith("xml")) { - language = "xml"; - } - - const isObject = typeof requestExample === "object"; - const exampleContent = isObject - ? JSON.stringify(requestExample, null, 2) - : requestExample; - - return ( - // @ts-ignore - - {requestExample.summary && ( - - {requestExample.summary} - - )} - - - ); + return MimeExample({ example, mimeType }); }; -interface ExampleFromSchemaProps { - schema: any; - mimeType: string; -} - -export const ExampleFromSchema: React.FC = ({ - schema, +export const RequestMimeExamples: React.FC = ({ + examples, mimeType, }) => { - const example = sampleResponseFromSchema(schema); - - if (mimeType.endsWith("xml")) { - let responseExampleObject; - try { - responseExampleObject = JSON.parse(JSON.stringify(example)); - } catch { - return null; - } - - if (typeof responseExampleObject === "object") { - let xmlExample; - try { - xmlExample = format(json2xml(responseExampleObject, ""), { - indentation: " ", - lineSeparator: "\n", - collapseContent: true, - }); - } catch { - const xmlExampleWithRoot = { root: responseExampleObject }; - try { - xmlExample = format(json2xml(xmlExampleWithRoot, ""), { - indentation: " ", - lineSeparator: "\n", - collapseContent: true, - }); - } catch { - xmlExample = json2xml(responseExampleObject, ""); - } - } - return ( - // @ts-ignore - - - - ); - } - } - - if (typeof example === "object" || typeof example === "string") { - return ( - // @ts-ignore - - - - ); - } - - return null; + return MimeExamples({ examples, mimeType }); }; -export const RequestSchemaExample: React.FC = ({ - requestExample, +export const RequestSchemaExample: React.FC = ({ + example, mimeType, }) => { - let language = "shell"; - if (mimeType.endsWith("json")) { - language = "json"; - } - if (mimeType.endsWith("xml")) { - language = "xml"; - } - - const isObject = typeof requestExample === "object"; - const exampleContent = isObject - ? JSON.stringify(requestExample, null, 2) - : requestExample; - - return ( - // @ts-ignore - - {requestExample.summary && ( - - {requestExample.summary} - - )} - - - ); + return SchemaExample({ example, mimeType }); }; -export const RequestSchemaExamples: React.FC = ({ - requestExamples, +export const RequestSchemaExamples: React.FC = ({ + examples, mimeType, }) => { - let language = "shell"; - if (mimeType.endsWith("json")) language = "json"; - if (mimeType.endsWith("xml")) language = "xml"; - - // Map response examples to an array of TabItem elements - const examplesArray = requestExamples.map((example: any, i: number) => { - const exampleName = `Example ${i + 1}`; - const isObject = typeof example === "object"; - const responseExample = isObject - ? JSON.stringify(example, null, 2) - : example; - - return ( - // @ts-ignore - - - - ); - }); - - return examplesArray; + return SchemaExamples({ examples, mimeType }); }; + +export { ExampleFromSchema }; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx index cb90cf4b5..23db8d4e7 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx @@ -13,8 +13,8 @@ import Markdown from "@theme/Markdown"; import MimeTabs from "@theme/MimeTabs"; // Assume these components exist import { ExampleFromSchema, - RequestExample, - RequestExamples, + RequestMimeExample, + RequestMimeExamples, RequestSchemaExample, RequestSchemaExamples, } from "@theme/RequestExamples"; @@ -52,11 +52,10 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => { return ( {mimeTypes.map((mimeType) => { - const responseMimeExamples = body.content![mimeType].examples; - const responseMimeExample = body.content![mimeType].example; - const responseSchemaExamples = - body.content![mimeType].schema?.examples; - const responseSchemaExample = body.content![mimeType].schema?.example; + const mimeExamples = body.content![mimeType].examples; + const mimeExample = body.content![mimeType].example; + const schemaExamples = body.content![mimeType].schema?.examples; + const schemaExample = body.content![mimeType].schema?.example; const firstBody = body.content![mimeType].schema; if ( @@ -111,32 +110,19 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => { {firstBody && - ExampleFromSchema({ - schema: firstBody, - mimeType: mimeType, - })} + ExampleFromSchema({ schema: firstBody, mimeType })} - {responseMimeExamples - ? RequestExamples({ - requestExamples: responseMimeExamples, - mimeType, - }) - : responseMimeExample - ? RequestExample({ - requestExample: responseMimeExample, - mimeType, - }) - : responseSchemaExamples - ? RequestSchemaExamples({ - requestExamples: responseSchemaExamples, - mimeType, - }) - : responseSchemaExample - ? RequestSchemaExample({ - requestExample: responseSchemaExample, - mimeType, - }) - : null} + {mimeExamples && + RequestMimeExamples({ examples: mimeExamples, mimeType })} + + {mimeExample && + RequestMimeExample({ example: mimeExample, mimeType })} + + {schemaExamples && + RequestSchemaExamples({ examples: schemaExamples, mimeType })} + + {schemaExample && + RequestSchemaExample({ example: schemaExample, mimeType })} ); diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx index 67eb5361e..6850fcf28 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx @@ -7,186 +7,29 @@ import React from "react"; -import Markdown from "@theme/Markdown"; -import ResponseSamples from "@theme/ResponseSamples"; -import TabItem from "@theme/TabItem"; -import { sampleResponseFromSchema } from "docusaurus-plugin-openapi-docs/lib/openapi/createResponseExample"; -import format from "xml-formatter"; - -export function json2xml(o: Record, tab: string): string { - const toXml = (v: any, name: string, ind: string): string => { - let xml = ""; - if (v instanceof Array) { - for (let i = 0, n = v.length; i < n; i++) { - xml += ind + toXml(v[i], name, ind + "\t") + "\n"; - } - } else if (typeof v === "object") { - let hasChild = false; - xml += ind + "<" + name; - for (const m in v) { - if (m.charAt(0) === "@") { - xml += " " + m.substr(1) + '="' + v[m].toString() + '"'; - } else { - hasChild = true; - } - } - xml += hasChild ? ">" : "/>"; - if (hasChild) { - for (const m2 in v) { - if (m2 === "#text") xml += v[m2]; - else if (m2 === "#cdata") xml += ""; - else if (m2.charAt(0) !== "@") xml += toXml(v[m2], m2, ind + "\t"); - } - xml += - (xml.charAt(xml.length - 1) === "\n" ? ind : "") + ""; - } - } else { - xml += ind + "<" + name + ">" + v.toString() + ""; - } - return xml; - }; - let xml = ""; - for (const m3 in o) xml += toXml(o[m3], m3, ""); - return tab ? xml.replace(/\t/g, tab) : xml.replace(/\t|\n/g, ""); -} - -interface ResponseExamplesProps { - responseExamples: any; - mimeType: string; -} -export const ResponseExamples: React.FC = ({ - responseExamples, - mimeType, -}): any => { - let language = "shell"; - if (mimeType.endsWith("json")) language = "json"; - if (mimeType.endsWith("xml")) language = "xml"; - - // Map response examples to an array of TabItem elements - const examplesArray = Object.entries(responseExamples).map( - ([exampleName, exampleValue]: any) => { - const isObject = typeof exampleValue.value === "object"; - const responseExample = isObject - ? JSON.stringify(exampleValue.value, null, 2) - : exampleValue.value; - - return ( - // @ts-ignore - - {exampleValue.summary && ( - - {exampleValue.summary} - - )} - - - ); - } - ); - - return examplesArray; -}; - -interface ResponseExampleProps { - responseExample: any; - mimeType: string; -} - -export const ResponseExample: React.FC = ({ - responseExample, +import { + MimeExamples, + MimeExample, + MimeExampleProps, + MimeExamplesProps, + ExampleFromSchema, +} from "@theme/Examples"; + +export const ResponseExamples: React.FC = ({ + examples, mimeType, }) => { - let language = "shell"; - if (mimeType.endsWith("json")) { - language = "json"; - } - if (mimeType.endsWith("xml")) { - language = "xml"; - } - - const isObject = typeof responseExample === "object"; - const exampleContent = isObject - ? JSON.stringify(responseExample, null, 2) - : responseExample; - - return ( - // @ts-ignore - - {responseExample.summary && ( - - {responseExample.summary} - - )} - - - ); + return MimeExamples({ examples, mimeType }); + // return ; }; -interface ExampleFromSchemaProps { - schema: any; - mimeType: string; -} - -export const ExampleFromSchema: React.FC = ({ - schema, +export const ResponseExample: React.FC = ({ + example, mimeType, }) => { - const responseExample = sampleResponseFromSchema(schema); - - if (mimeType.endsWith("xml")) { - let responseExampleObject; - try { - responseExampleObject = JSON.parse(JSON.stringify(responseExample)); - } catch { - return null; - } - - if (typeof responseExampleObject === "object") { - let xmlExample; - try { - xmlExample = format(json2xml(responseExampleObject, ""), { - indentation: " ", - lineSeparator: "\n", - collapseContent: true, - }); - } catch { - const xmlExampleWithRoot = { root: responseExampleObject }; - try { - xmlExample = format(json2xml(xmlExampleWithRoot, ""), { - indentation: " ", - lineSeparator: "\n", - collapseContent: true, - }); - } catch { - xmlExample = json2xml(responseExampleObject, ""); - } - } - return ( - // @ts-ignore - - - - ); - } - } - - if ( - typeof responseExample === "object" || - typeof responseExample === "string" - ) { - return ( - // @ts-ignore - - - - ); - } - - return null; + return MimeExample({ example, mimeType }); + // return ; }; + +// Re-export ExampleFromSchema with original name +export { ExampleFromSchema }; 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 ac4a64954..82b5bf359 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx @@ -109,16 +109,13 @@ const ResponseSchemaComponent: React.FC = ({ {firstBody && - ExampleFromSchema({ - schema: firstBody, - mimeType: mimeType, - })} + ExampleFromSchema({ schema: firstBody, mimeType })} {responseExamples && - ResponseExamples({ responseExamples, mimeType })} + ResponseExamples({ examples: responseExamples, mimeType })} {responseExample && - ResponseExample({ responseExample, mimeType })} + ResponseExample({ example: responseExample, mimeType })} ); From afaa92a4dca0566cb5cdad4ddefb502936508d35 Mon Sep 17 00:00:00 2001 From: "takaoka.daisuke" Date: Wed, 11 Jun 2025 09:55:10 +0900 Subject: [PATCH 4/8] feat: refactor request and response example --- .../src/openapi/createRequestExample.ts | 253 +--------------- .../src/openapi/createResponseExample.ts | 256 +--------------- .../src/openapi/createSchemaExample.ts | 279 ++++++++++++++++++ 3 files changed, 283 insertions(+), 505 deletions(-) create mode 100644 packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts index 72e15d590..876b7cff0 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts @@ -5,258 +5,9 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import chalk from "chalk"; -import merge from "lodash/merge"; - +import { sampleFromSchema } from "./createSchemaExample"; import { SchemaObject } from "./types"; -import { mergeAllOf } from "../markdown/createSchema"; - -interface OASTypeToTypeMap { - string: string; - number: number; - integer: number; - boolean: boolean; - object: any; - array: any[]; - null: string | null; -} - -type Primitives = { - [OASType in keyof OASTypeToTypeMap]: { - [format: string]: (schema: SchemaObject) => OASTypeToTypeMap[OASType]; - }; -}; - -const primitives: Primitives = { - string: { - default: () => "string", - email: () => "user@example.com", - date: () => "2024-07-29", - "date-time": () => "2024-07-29T15:51:28.071Z", - uuid: () => "3fa85f64-5717-4562-b3fc-2c963f66afa6", - hostname: () => "example.com", - ipv4: () => "198.51.100.42", - ipv6: () => "2001:0db8:5b96:0000:0000:426f:8e17:642a", - }, - number: { - default: () => 0, - float: () => 0.0, - }, - integer: { - default: () => 0, - }, - boolean: { - default: (schema) => - typeof schema.default === "boolean" ? schema.default : true, - }, - object: {}, - array: {}, - null: { - default: () => "null", - }, -}; - -function sampleRequestFromProp(name: string, prop: any, obj: any): any { - // Handle resolved circular props - if (typeof prop === "object" && Object.keys(prop).length === 0) { - obj[name] = prop; - return obj; - } - - // TODO: handle discriminators - - if (prop.oneOf) { - obj[name] = sampleRequestFromSchema(prop.oneOf[0]); - } else if (prop.anyOf) { - obj[name] = sampleRequestFromSchema(prop.anyOf[0]); - } else if (prop.allOf) { - const mergedSchemas = mergeAllOf(prop) as SchemaObject; - sampleRequestFromProp(name, mergedSchemas, obj); - } else { - obj[name] = sampleRequestFromSchema(prop); - } - return obj; -} export const sampleRequestFromSchema = (schema: SchemaObject = {}): any => { - try { - // deep copy schema before processing - let schemaCopy = JSON.parse(JSON.stringify(schema)); - let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy; - - if (example !== undefined) { - return example; - } - - if (oneOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, oneOf[0]); - delete combinedSchemas.oneOf; - return sampleRequestFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleRequestFromSchema(oneOf[0]); - } - - if (anyOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, anyOf[0]); - delete combinedSchemas.anyOf; - return sampleRequestFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleRequestFromSchema(anyOf[0]); - } - - if (allOf) { - const mergedSchemas = mergeAllOf(schemaCopy) as SchemaObject; - if (mergedSchemas.properties) { - for (const [key, value] of Object.entries(mergedSchemas.properties)) { - if ((value.readOnly && value.readOnly === true) || value.deprecated) { - delete mergedSchemas.properties[key]; - } - } - } - if (properties) { - const combinedSchemas = merge(schemaCopy, mergedSchemas); - delete combinedSchemas.allOf; - return sampleRequestFromSchema(combinedSchemas); - } - return sampleRequestFromSchema(mergedSchemas); - } - - if (!type) { - if (properties) { - type = "object"; - } else if (items) { - type = "array"; - } else { - return; - } - } - - if (type === "object") { - let obj: any = {}; - for (let [name, prop] of Object.entries(properties ?? {}) as any) { - if (prop.properties) { - for (const [key, value] of Object.entries(prop.properties) as any) { - if ( - (value.readOnly && value.readOnly === true) || - value.deprecated - ) { - delete prop.properties[key]; - } - } - } - - if (prop.items && prop.items.properties) { - for (const [key, value] of Object.entries( - prop.items.properties - ) as any) { - if ( - (value.readOnly && value.readOnly === true) || - value.deprecated - ) { - delete prop.items.properties[key]; - } - } - } - - if (prop.readOnly && prop.readOnly === true) { - continue; - } - - if (prop.deprecated) { - continue; - } - - // Resolve schema from prop recursively - obj = sampleRequestFromProp(name, prop, obj); - } - return obj; - } - - if (type === "array") { - if (Array.isArray(items?.anyOf)) { - return processArrayItems(items, "anyOf"); - } - - if (Array.isArray(items?.oneOf)) { - return processArrayItems(items, "oneOf"); - } - - return normalizeArray(sampleRequestFromSchema(items)); - } - - if (schemaCopy.enum) { - if (schemaCopy.default) { - return schemaCopy.default; - } - return normalizeArray(schemaCopy.enum)[0]; - } - - if ( - (schema.readOnly && schema.readOnly === true) || - schemaCopy.deprecated - ) { - return undefined; - } - - return primitive(schemaCopy); - } catch (err) { - console.error( - chalk.yellow("WARNING: failed to create example from schema object:", err) - ); - return; - } + return sampleFromSchema(schema, { type: "request" }); }; - -function primitive(schema: SchemaObject = {}) { - let { type, format } = schema; - - if (type === undefined) { - return; - } - - let fn = schema.default ? () => schema.default : primitives[type].default; - - if (format !== undefined) { - fn = primitives[type][format] || fn; - } - - if (fn) { - return fn(schema); - } - - return "Unknown Type: " + schema.type; -} - -function normalizeArray(arr: any) { - if (Array.isArray(arr)) { - return arr; - } - return [arr]; -} - -function processArrayItems( - items: SchemaObject, - schemaType: "anyOf" | "oneOf" -): any[] { - const itemsArray = items[schemaType] as SchemaObject[]; - return itemsArray.map((item: SchemaObject) => { - // If items has properties, merge them with each item - if (items.properties) { - const combinedSchema = { - ...item, - properties: { - ...items.properties, // Common properties from parent - ...item.properties, // Specific properties from this anyOf/oneOf item - }, - }; - // Remove anyOf/oneOf to prevent infinite recursion when calling sampleRequestFromSchema - delete combinedSchema[schemaType]; - return sampleRequestFromSchema(combinedSchema); - } - return sampleRequestFromSchema(item); - }); -} diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts index 5526421d9..602f872b4 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts @@ -5,261 +5,9 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import chalk from "chalk"; -import merge from "lodash/merge"; - +import { sampleFromSchema } from "./createSchemaExample"; import { SchemaObject } from "./types"; -import { mergeAllOf } from "../markdown/createSchema"; - -interface OASTypeToTypeMap { - string: string; - number: number; - integer: number; - boolean: boolean; - object: any; - array: any[]; - null: string | null; -} - -type Primitives = { - [OASType in keyof OASTypeToTypeMap]: { - [format: string]: (schema: SchemaObject) => OASTypeToTypeMap[OASType]; - }; -}; - -const primitives: Primitives = { - string: { - default: () => "string", - email: () => "user@example.com", - date: () => "2024-07-29", - "date-time": () => "2024-07-29T15:51:28.071Z", - uuid: () => "3fa85f64-5717-4562-b3fc-2c963f66afa6", - hostname: () => "example.com", - ipv4: () => "198.51.100.42", - ipv6: () => "2001:0db8:5b96:0000:0000:426f:8e17:642a", - }, - number: { - default: () => 0, - float: () => 0.0, - }, - integer: { - default: () => 0, - }, - boolean: { - default: (schema) => - typeof schema.default === "boolean" ? schema.default : true, - }, - object: {}, - array: {}, - null: { - default: () => "null", - }, -}; - -function sampleResponseFromProp(name: string, prop: any, obj: any): any { - // Handle resolved circular props - if (typeof prop === "object" && Object.keys(prop).length === 0) { - obj[name] = prop; - return obj; - } - - // TODO: handle discriminators - - if (prop.oneOf) { - obj[name] = sampleResponseFromSchema(prop.oneOf[0]); - } else if (prop.anyOf) { - obj[name] = sampleResponseFromSchema(prop.anyOf[0]); - } else if (prop.allOf) { - const mergedSchemas = mergeAllOf(prop) as SchemaObject; - sampleResponseFromProp(name, mergedSchemas, obj); - } else { - obj[name] = sampleResponseFromSchema(prop); - } - return obj; -} export const sampleResponseFromSchema = (schema: SchemaObject = {}): any => { - try { - // deep copy schema before processing - let schemaCopy = JSON.parse(JSON.stringify(schema)); - let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy; - - if (example !== undefined) { - return example; - } - - if (allOf) { - const mergedSchemas = mergeAllOf(schemaCopy) as SchemaObject; - if (mergedSchemas.properties) { - for (const [key, value] of Object.entries(mergedSchemas.properties)) { - if ( - (value.writeOnly && value.writeOnly === true) || - value.deprecated - ) { - delete mergedSchemas.properties[key]; - } - } - } - if (properties) { - const combinedSchemas = merge(schemaCopy, mergedSchemas); - delete combinedSchemas.allOf; - return sampleResponseFromSchema(combinedSchemas); - } - return sampleResponseFromSchema(mergedSchemas); - } - - if (oneOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, oneOf[0]); - delete combinedSchemas.oneOf; - return sampleResponseFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleResponseFromSchema(oneOf[0]); - } - - if (anyOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, anyOf[0]); - delete combinedSchemas.anyOf; - return sampleResponseFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleResponseFromSchema(anyOf[0]); - } - - if (!type) { - if (properties) { - type = "object"; - } else if (items) { - type = "array"; - } else { - return; - } - } - - if (type === "object") { - let obj: any = {}; - for (let [name, prop] of Object.entries(properties ?? {}) as any) { - if (prop.properties) { - for (const [key, value] of Object.entries(prop.properties) as any) { - if ( - (value.writeOnly && value.writeOnly === true) || - value.deprecated - ) { - delete prop.properties[key]; - } - } - } - - if (prop.items && prop.items.properties) { - for (const [key, value] of Object.entries( - prop.items.properties - ) as any) { - if ( - (value.writeOnly && value.writeOnly === true) || - value.deprecated - ) { - delete prop.items.properties[key]; - } - } - } - - if (prop.writeOnly && prop.writeOnly === true) { - continue; - } - - if (prop.deprecated) { - continue; - } - - // Resolve schema from prop recursively - obj = sampleResponseFromProp(name, prop, obj); - } - return obj; - } - - if (type === "array") { - if (Array.isArray(items?.anyOf)) { - return processArrayItems(items, "anyOf"); - } - - if (Array.isArray(items?.oneOf)) { - return processArrayItems(items, "oneOf"); - } - - return [sampleResponseFromSchema(items)]; - } - - if (schemaCopy.enum) { - if (schemaCopy.default) { - return schemaCopy.default; - } - return normalizeArray(schemaCopy.enum)[0]; - } - - if ( - (schemaCopy.writeOnly && schemaCopy.writeOnly === true) || - schemaCopy.deprecated - ) { - return undefined; - } - - return primitive(schemaCopy); - } catch (err) { - console.error( - chalk.yellow("WARNING: failed to create example from schema object:", err) - ); - return; - } + return sampleFromSchema(schema, { type: "response" }); }; - -function primitive(schema: SchemaObject = {}) { - let { type, format } = schema; - - if (type === undefined) { - return; - } - - let fn = schema.default ? () => schema.default : primitives[type].default; - - if (format !== undefined) { - fn = primitives[type][format] || fn; - } - - if (fn) { - return fn(schema); - } - - return "Unknown Type: " + schema.type; -} - -function normalizeArray(arr: any) { - if (Array.isArray(arr)) { - return arr; - } - return [arr]; -} - -function processArrayItems( - items: SchemaObject, - schemaType: "anyOf" | "oneOf" -): any[] { - const itemsArray = items[schemaType] as SchemaObject[]; - return itemsArray.map((item: SchemaObject) => { - // If items has properties, merge them with each item - if (items.properties) { - const combinedSchema = { - ...item, - properties: { - ...items.properties, // Common properties from parent - ...item.properties, // Specific properties from this anyOf/oneOf item - }, - }; - // Remove anyOf/oneOf to prevent infinite recursion when calling sampleResponseFromSchema - delete combinedSchema[schemaType]; - return sampleResponseFromSchema(combinedSchema); - } - return sampleResponseFromSchema(item); - }); -} diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts new file mode 100644 index 000000000..de0abfc73 --- /dev/null +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts @@ -0,0 +1,279 @@ +/* ============================================================================ + * 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 chalk from "chalk"; +import merge from "lodash/merge"; + +import { SchemaObject } from "./types"; +import { mergeAllOf } from "../markdown/createSchema"; + +interface OASTypeToTypeMap { + string: string; + number: number; + integer: number; + boolean: boolean; + object: any; + array: any[]; + null: string | null; +} + +type Primitives = { + [OASType in keyof OASTypeToTypeMap]: { + [format: string]: (schema: SchemaObject) => OASTypeToTypeMap[OASType]; + }; +}; + +const primitives: Primitives = { + string: { + default: () => "string", + email: () => "user@example.com", + date: () => "2024-07-29", + "date-time": () => "2024-07-29T15:51:28.071Z", + uuid: () => "3fa85f64-5717-4562-b3fc-2c963f66afa6", + hostname: () => "example.com", + ipv4: () => "198.51.100.42", + ipv6: () => "2001:0db8:5b96:0000:0000:426f:8e17:642a", + }, + number: { + default: () => 0, + float: () => 0.0, + }, + integer: { + default: () => 0, + }, + boolean: { + default: (schema) => + typeof schema.default === "boolean" ? schema.default : true, + }, + object: {}, + array: {}, + null: { + default: () => "null", + }, +}; + +type ExampleType = "request" | "response"; + +interface ExampleContext { + type: ExampleType; +} + +function shouldExcludeProperty( + prop: SchemaObject, + context: ExampleContext +): boolean { + if (prop.deprecated) { + return true; + } + + if (context.type === "request") { + return prop.readOnly === true; + } else { + return prop.writeOnly === true; + } +} + +function sampleFromProp( + name: string, + prop: any, + obj: any, + context: ExampleContext +): any { + // Handle resolved circular props + if (typeof prop === "object" && Object.keys(prop).length === 0) { + obj[name] = prop; + return obj; + } + + // TODO: handle discriminators + + if (prop.oneOf) { + obj[name] = sampleFromSchema(prop.oneOf[0], context); + } else if (prop.anyOf) { + obj[name] = sampleFromSchema(prop.anyOf[0], context); + } else if (prop.allOf) { + const mergedSchemas = mergeAllOf(prop) as SchemaObject; + sampleFromProp(name, mergedSchemas, obj, context); + } else { + obj[name] = sampleFromSchema(prop, context); + } + return obj; +} + +export const sampleFromSchema = ( + schema: SchemaObject = {}, + context: ExampleContext +): any => { + try { + // deep copy schema before processing + let schemaCopy = JSON.parse(JSON.stringify(schema)); + let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy; + + if (example !== undefined) { + return example; + } + + if (oneOf) { + if (properties) { + const combinedSchemas = merge(schemaCopy, oneOf[0]); + delete combinedSchemas.oneOf; + return sampleFromSchema(combinedSchemas, context); + } + // Just go with first schema + return sampleFromSchema(oneOf[0], context); + } + + if (anyOf) { + if (properties) { + const combinedSchemas = merge(schemaCopy, anyOf[0]); + delete combinedSchemas.anyOf; + return sampleFromSchema(combinedSchemas, context); + } + // Just go with first schema + return sampleFromSchema(anyOf[0], context); + } + + if (allOf) { + const mergedSchemas = mergeAllOf(schemaCopy) as SchemaObject; + if (mergedSchemas.properties) { + for (const [key, value] of Object.entries(mergedSchemas.properties)) { + if (shouldExcludeProperty(value, context)) { + delete mergedSchemas.properties[key]; + } + } + } + if (properties) { + const combinedSchemas = merge(schemaCopy, mergedSchemas); + delete combinedSchemas.allOf; + return sampleFromSchema(combinedSchemas, context); + } + return sampleFromSchema(mergedSchemas, context); + } + + if (!type) { + if (properties) { + type = "object"; + } else if (items) { + type = "array"; + } else { + return; + } + } + + if (type === "object") { + let obj: any = {}; + for (let [name, prop] of Object.entries(properties ?? {}) as any) { + if (prop.properties) { + for (const [key, value] of Object.entries(prop.properties) as any) { + if (shouldExcludeProperty(value, context)) { + delete prop.properties[key]; + } + } + } + + if (prop.items && prop.items.properties) { + for (const [key, value] of Object.entries( + prop.items.properties + ) as any) { + if (shouldExcludeProperty(value, context)) { + delete prop.items.properties[key]; + } + } + } + + if (shouldExcludeProperty(prop, context)) { + continue; + } + + // Resolve schema from prop recursively + obj = sampleFromProp(name, prop, obj, context); + } + return obj; + } + + if (type === "array") { + if (Array.isArray(items?.anyOf)) { + return processArrayItems(items, "anyOf", context); + } + + if (Array.isArray(items?.oneOf)) { + return processArrayItems(items, "oneOf", context); + } + + return normalizeArray(sampleFromSchema(items, context)); + } + + if (schemaCopy.enum) { + if (schemaCopy.default) { + return schemaCopy.default; + } + return normalizeArray(schemaCopy.enum)[0]; + } + + if (shouldExcludeProperty(schemaCopy, context)) { + return undefined; + } + + return primitive(schemaCopy); + } catch (err) { + console.error( + chalk.yellow("WARNING: failed to create example from schema object:", err) + ); + return; + } +}; + +function primitive(schema: SchemaObject = {}) { + let { type, format } = schema; + + if (type === undefined) { + return; + } + + let fn = schema.default ? () => schema.default : primitives[type].default; + + if (format !== undefined) { + fn = primitives[type][format] || fn; + } + + if (fn) { + return fn(schema); + } + + return "Unknown Type: " + schema.type; +} + +function normalizeArray(arr: any) { + if (Array.isArray(arr)) { + return arr; + } + return [arr]; +} + +function processArrayItems( + items: SchemaObject, + schemaType: "anyOf" | "oneOf", + context: ExampleContext +): any[] { + const itemsArray = items[schemaType] as SchemaObject[]; + return itemsArray.map((item: SchemaObject) => { + // If items has properties, merge them with each item + if (items.properties) { + const combinedSchema = { + ...item, + properties: { + ...items.properties, // Common properties from parent + ...item.properties, // Specific properties from this anyOf/oneOf item + }, + }; + // Remove anyOf/oneOf to prevent infinite recursion when calling sampleFromSchema + delete combinedSchema[schemaType]; + return sampleFromSchema(combinedSchema, context); + } + return sampleFromSchema(item, context); + }); +} From 294609b248a027f2e507a2da939fff093320cdbf Mon Sep 17 00:00:00 2001 From: "takaoka.daisuke" Date: Wed, 11 Jun 2025 09:55:10 +0900 Subject: [PATCH 5/8] feat: refactor request and response example --- .../src/openapi/createRequestExample.ts | 253 +--------------- .../src/openapi/createResponseExample.ts | 256 +--------------- .../src/openapi/createSchemaExample.ts | 279 ++++++++++++++++++ 3 files changed, 283 insertions(+), 505 deletions(-) create mode 100644 packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts index 72e15d590..876b7cff0 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts @@ -5,258 +5,9 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import chalk from "chalk"; -import merge from "lodash/merge"; - +import { sampleFromSchema } from "./createSchemaExample"; import { SchemaObject } from "./types"; -import { mergeAllOf } from "../markdown/createSchema"; - -interface OASTypeToTypeMap { - string: string; - number: number; - integer: number; - boolean: boolean; - object: any; - array: any[]; - null: string | null; -} - -type Primitives = { - [OASType in keyof OASTypeToTypeMap]: { - [format: string]: (schema: SchemaObject) => OASTypeToTypeMap[OASType]; - }; -}; - -const primitives: Primitives = { - string: { - default: () => "string", - email: () => "user@example.com", - date: () => "2024-07-29", - "date-time": () => "2024-07-29T15:51:28.071Z", - uuid: () => "3fa85f64-5717-4562-b3fc-2c963f66afa6", - hostname: () => "example.com", - ipv4: () => "198.51.100.42", - ipv6: () => "2001:0db8:5b96:0000:0000:426f:8e17:642a", - }, - number: { - default: () => 0, - float: () => 0.0, - }, - integer: { - default: () => 0, - }, - boolean: { - default: (schema) => - typeof schema.default === "boolean" ? schema.default : true, - }, - object: {}, - array: {}, - null: { - default: () => "null", - }, -}; - -function sampleRequestFromProp(name: string, prop: any, obj: any): any { - // Handle resolved circular props - if (typeof prop === "object" && Object.keys(prop).length === 0) { - obj[name] = prop; - return obj; - } - - // TODO: handle discriminators - - if (prop.oneOf) { - obj[name] = sampleRequestFromSchema(prop.oneOf[0]); - } else if (prop.anyOf) { - obj[name] = sampleRequestFromSchema(prop.anyOf[0]); - } else if (prop.allOf) { - const mergedSchemas = mergeAllOf(prop) as SchemaObject; - sampleRequestFromProp(name, mergedSchemas, obj); - } else { - obj[name] = sampleRequestFromSchema(prop); - } - return obj; -} export const sampleRequestFromSchema = (schema: SchemaObject = {}): any => { - try { - // deep copy schema before processing - let schemaCopy = JSON.parse(JSON.stringify(schema)); - let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy; - - if (example !== undefined) { - return example; - } - - if (oneOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, oneOf[0]); - delete combinedSchemas.oneOf; - return sampleRequestFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleRequestFromSchema(oneOf[0]); - } - - if (anyOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, anyOf[0]); - delete combinedSchemas.anyOf; - return sampleRequestFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleRequestFromSchema(anyOf[0]); - } - - if (allOf) { - const mergedSchemas = mergeAllOf(schemaCopy) as SchemaObject; - if (mergedSchemas.properties) { - for (const [key, value] of Object.entries(mergedSchemas.properties)) { - if ((value.readOnly && value.readOnly === true) || value.deprecated) { - delete mergedSchemas.properties[key]; - } - } - } - if (properties) { - const combinedSchemas = merge(schemaCopy, mergedSchemas); - delete combinedSchemas.allOf; - return sampleRequestFromSchema(combinedSchemas); - } - return sampleRequestFromSchema(mergedSchemas); - } - - if (!type) { - if (properties) { - type = "object"; - } else if (items) { - type = "array"; - } else { - return; - } - } - - if (type === "object") { - let obj: any = {}; - for (let [name, prop] of Object.entries(properties ?? {}) as any) { - if (prop.properties) { - for (const [key, value] of Object.entries(prop.properties) as any) { - if ( - (value.readOnly && value.readOnly === true) || - value.deprecated - ) { - delete prop.properties[key]; - } - } - } - - if (prop.items && prop.items.properties) { - for (const [key, value] of Object.entries( - prop.items.properties - ) as any) { - if ( - (value.readOnly && value.readOnly === true) || - value.deprecated - ) { - delete prop.items.properties[key]; - } - } - } - - if (prop.readOnly && prop.readOnly === true) { - continue; - } - - if (prop.deprecated) { - continue; - } - - // Resolve schema from prop recursively - obj = sampleRequestFromProp(name, prop, obj); - } - return obj; - } - - if (type === "array") { - if (Array.isArray(items?.anyOf)) { - return processArrayItems(items, "anyOf"); - } - - if (Array.isArray(items?.oneOf)) { - return processArrayItems(items, "oneOf"); - } - - return normalizeArray(sampleRequestFromSchema(items)); - } - - if (schemaCopy.enum) { - if (schemaCopy.default) { - return schemaCopy.default; - } - return normalizeArray(schemaCopy.enum)[0]; - } - - if ( - (schema.readOnly && schema.readOnly === true) || - schemaCopy.deprecated - ) { - return undefined; - } - - return primitive(schemaCopy); - } catch (err) { - console.error( - chalk.yellow("WARNING: failed to create example from schema object:", err) - ); - return; - } + return sampleFromSchema(schema, { type: "request" }); }; - -function primitive(schema: SchemaObject = {}) { - let { type, format } = schema; - - if (type === undefined) { - return; - } - - let fn = schema.default ? () => schema.default : primitives[type].default; - - if (format !== undefined) { - fn = primitives[type][format] || fn; - } - - if (fn) { - return fn(schema); - } - - return "Unknown Type: " + schema.type; -} - -function normalizeArray(arr: any) { - if (Array.isArray(arr)) { - return arr; - } - return [arr]; -} - -function processArrayItems( - items: SchemaObject, - schemaType: "anyOf" | "oneOf" -): any[] { - const itemsArray = items[schemaType] as SchemaObject[]; - return itemsArray.map((item: SchemaObject) => { - // If items has properties, merge them with each item - if (items.properties) { - const combinedSchema = { - ...item, - properties: { - ...items.properties, // Common properties from parent - ...item.properties, // Specific properties from this anyOf/oneOf item - }, - }; - // Remove anyOf/oneOf to prevent infinite recursion when calling sampleRequestFromSchema - delete combinedSchema[schemaType]; - return sampleRequestFromSchema(combinedSchema); - } - return sampleRequestFromSchema(item); - }); -} diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts index 5526421d9..602f872b4 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts @@ -5,261 +5,9 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import chalk from "chalk"; -import merge from "lodash/merge"; - +import { sampleFromSchema } from "./createSchemaExample"; import { SchemaObject } from "./types"; -import { mergeAllOf } from "../markdown/createSchema"; - -interface OASTypeToTypeMap { - string: string; - number: number; - integer: number; - boolean: boolean; - object: any; - array: any[]; - null: string | null; -} - -type Primitives = { - [OASType in keyof OASTypeToTypeMap]: { - [format: string]: (schema: SchemaObject) => OASTypeToTypeMap[OASType]; - }; -}; - -const primitives: Primitives = { - string: { - default: () => "string", - email: () => "user@example.com", - date: () => "2024-07-29", - "date-time": () => "2024-07-29T15:51:28.071Z", - uuid: () => "3fa85f64-5717-4562-b3fc-2c963f66afa6", - hostname: () => "example.com", - ipv4: () => "198.51.100.42", - ipv6: () => "2001:0db8:5b96:0000:0000:426f:8e17:642a", - }, - number: { - default: () => 0, - float: () => 0.0, - }, - integer: { - default: () => 0, - }, - boolean: { - default: (schema) => - typeof schema.default === "boolean" ? schema.default : true, - }, - object: {}, - array: {}, - null: { - default: () => "null", - }, -}; - -function sampleResponseFromProp(name: string, prop: any, obj: any): any { - // Handle resolved circular props - if (typeof prop === "object" && Object.keys(prop).length === 0) { - obj[name] = prop; - return obj; - } - - // TODO: handle discriminators - - if (prop.oneOf) { - obj[name] = sampleResponseFromSchema(prop.oneOf[0]); - } else if (prop.anyOf) { - obj[name] = sampleResponseFromSchema(prop.anyOf[0]); - } else if (prop.allOf) { - const mergedSchemas = mergeAllOf(prop) as SchemaObject; - sampleResponseFromProp(name, mergedSchemas, obj); - } else { - obj[name] = sampleResponseFromSchema(prop); - } - return obj; -} export const sampleResponseFromSchema = (schema: SchemaObject = {}): any => { - try { - // deep copy schema before processing - let schemaCopy = JSON.parse(JSON.stringify(schema)); - let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy; - - if (example !== undefined) { - return example; - } - - if (allOf) { - const mergedSchemas = mergeAllOf(schemaCopy) as SchemaObject; - if (mergedSchemas.properties) { - for (const [key, value] of Object.entries(mergedSchemas.properties)) { - if ( - (value.writeOnly && value.writeOnly === true) || - value.deprecated - ) { - delete mergedSchemas.properties[key]; - } - } - } - if (properties) { - const combinedSchemas = merge(schemaCopy, mergedSchemas); - delete combinedSchemas.allOf; - return sampleResponseFromSchema(combinedSchemas); - } - return sampleResponseFromSchema(mergedSchemas); - } - - if (oneOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, oneOf[0]); - delete combinedSchemas.oneOf; - return sampleResponseFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleResponseFromSchema(oneOf[0]); - } - - if (anyOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, anyOf[0]); - delete combinedSchemas.anyOf; - return sampleResponseFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleResponseFromSchema(anyOf[0]); - } - - if (!type) { - if (properties) { - type = "object"; - } else if (items) { - type = "array"; - } else { - return; - } - } - - if (type === "object") { - let obj: any = {}; - for (let [name, prop] of Object.entries(properties ?? {}) as any) { - if (prop.properties) { - for (const [key, value] of Object.entries(prop.properties) as any) { - if ( - (value.writeOnly && value.writeOnly === true) || - value.deprecated - ) { - delete prop.properties[key]; - } - } - } - - if (prop.items && prop.items.properties) { - for (const [key, value] of Object.entries( - prop.items.properties - ) as any) { - if ( - (value.writeOnly && value.writeOnly === true) || - value.deprecated - ) { - delete prop.items.properties[key]; - } - } - } - - if (prop.writeOnly && prop.writeOnly === true) { - continue; - } - - if (prop.deprecated) { - continue; - } - - // Resolve schema from prop recursively - obj = sampleResponseFromProp(name, prop, obj); - } - return obj; - } - - if (type === "array") { - if (Array.isArray(items?.anyOf)) { - return processArrayItems(items, "anyOf"); - } - - if (Array.isArray(items?.oneOf)) { - return processArrayItems(items, "oneOf"); - } - - return [sampleResponseFromSchema(items)]; - } - - if (schemaCopy.enum) { - if (schemaCopy.default) { - return schemaCopy.default; - } - return normalizeArray(schemaCopy.enum)[0]; - } - - if ( - (schemaCopy.writeOnly && schemaCopy.writeOnly === true) || - schemaCopy.deprecated - ) { - return undefined; - } - - return primitive(schemaCopy); - } catch (err) { - console.error( - chalk.yellow("WARNING: failed to create example from schema object:", err) - ); - return; - } + return sampleFromSchema(schema, { type: "response" }); }; - -function primitive(schema: SchemaObject = {}) { - let { type, format } = schema; - - if (type === undefined) { - return; - } - - let fn = schema.default ? () => schema.default : primitives[type].default; - - if (format !== undefined) { - fn = primitives[type][format] || fn; - } - - if (fn) { - return fn(schema); - } - - return "Unknown Type: " + schema.type; -} - -function normalizeArray(arr: any) { - if (Array.isArray(arr)) { - return arr; - } - return [arr]; -} - -function processArrayItems( - items: SchemaObject, - schemaType: "anyOf" | "oneOf" -): any[] { - const itemsArray = items[schemaType] as SchemaObject[]; - return itemsArray.map((item: SchemaObject) => { - // If items has properties, merge them with each item - if (items.properties) { - const combinedSchema = { - ...item, - properties: { - ...items.properties, // Common properties from parent - ...item.properties, // Specific properties from this anyOf/oneOf item - }, - }; - // Remove anyOf/oneOf to prevent infinite recursion when calling sampleResponseFromSchema - delete combinedSchema[schemaType]; - return sampleResponseFromSchema(combinedSchema); - } - return sampleResponseFromSchema(item); - }); -} diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts new file mode 100644 index 000000000..de0abfc73 --- /dev/null +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts @@ -0,0 +1,279 @@ +/* ============================================================================ + * 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 chalk from "chalk"; +import merge from "lodash/merge"; + +import { SchemaObject } from "./types"; +import { mergeAllOf } from "../markdown/createSchema"; + +interface OASTypeToTypeMap { + string: string; + number: number; + integer: number; + boolean: boolean; + object: any; + array: any[]; + null: string | null; +} + +type Primitives = { + [OASType in keyof OASTypeToTypeMap]: { + [format: string]: (schema: SchemaObject) => OASTypeToTypeMap[OASType]; + }; +}; + +const primitives: Primitives = { + string: { + default: () => "string", + email: () => "user@example.com", + date: () => "2024-07-29", + "date-time": () => "2024-07-29T15:51:28.071Z", + uuid: () => "3fa85f64-5717-4562-b3fc-2c963f66afa6", + hostname: () => "example.com", + ipv4: () => "198.51.100.42", + ipv6: () => "2001:0db8:5b96:0000:0000:426f:8e17:642a", + }, + number: { + default: () => 0, + float: () => 0.0, + }, + integer: { + default: () => 0, + }, + boolean: { + default: (schema) => + typeof schema.default === "boolean" ? schema.default : true, + }, + object: {}, + array: {}, + null: { + default: () => "null", + }, +}; + +type ExampleType = "request" | "response"; + +interface ExampleContext { + type: ExampleType; +} + +function shouldExcludeProperty( + prop: SchemaObject, + context: ExampleContext +): boolean { + if (prop.deprecated) { + return true; + } + + if (context.type === "request") { + return prop.readOnly === true; + } else { + return prop.writeOnly === true; + } +} + +function sampleFromProp( + name: string, + prop: any, + obj: any, + context: ExampleContext +): any { + // Handle resolved circular props + if (typeof prop === "object" && Object.keys(prop).length === 0) { + obj[name] = prop; + return obj; + } + + // TODO: handle discriminators + + if (prop.oneOf) { + obj[name] = sampleFromSchema(prop.oneOf[0], context); + } else if (prop.anyOf) { + obj[name] = sampleFromSchema(prop.anyOf[0], context); + } else if (prop.allOf) { + const mergedSchemas = mergeAllOf(prop) as SchemaObject; + sampleFromProp(name, mergedSchemas, obj, context); + } else { + obj[name] = sampleFromSchema(prop, context); + } + return obj; +} + +export const sampleFromSchema = ( + schema: SchemaObject = {}, + context: ExampleContext +): any => { + try { + // deep copy schema before processing + let schemaCopy = JSON.parse(JSON.stringify(schema)); + let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy; + + if (example !== undefined) { + return example; + } + + if (oneOf) { + if (properties) { + const combinedSchemas = merge(schemaCopy, oneOf[0]); + delete combinedSchemas.oneOf; + return sampleFromSchema(combinedSchemas, context); + } + // Just go with first schema + return sampleFromSchema(oneOf[0], context); + } + + if (anyOf) { + if (properties) { + const combinedSchemas = merge(schemaCopy, anyOf[0]); + delete combinedSchemas.anyOf; + return sampleFromSchema(combinedSchemas, context); + } + // Just go with first schema + return sampleFromSchema(anyOf[0], context); + } + + if (allOf) { + const mergedSchemas = mergeAllOf(schemaCopy) as SchemaObject; + if (mergedSchemas.properties) { + for (const [key, value] of Object.entries(mergedSchemas.properties)) { + if (shouldExcludeProperty(value, context)) { + delete mergedSchemas.properties[key]; + } + } + } + if (properties) { + const combinedSchemas = merge(schemaCopy, mergedSchemas); + delete combinedSchemas.allOf; + return sampleFromSchema(combinedSchemas, context); + } + return sampleFromSchema(mergedSchemas, context); + } + + if (!type) { + if (properties) { + type = "object"; + } else if (items) { + type = "array"; + } else { + return; + } + } + + if (type === "object") { + let obj: any = {}; + for (let [name, prop] of Object.entries(properties ?? {}) as any) { + if (prop.properties) { + for (const [key, value] of Object.entries(prop.properties) as any) { + if (shouldExcludeProperty(value, context)) { + delete prop.properties[key]; + } + } + } + + if (prop.items && prop.items.properties) { + for (const [key, value] of Object.entries( + prop.items.properties + ) as any) { + if (shouldExcludeProperty(value, context)) { + delete prop.items.properties[key]; + } + } + } + + if (shouldExcludeProperty(prop, context)) { + continue; + } + + // Resolve schema from prop recursively + obj = sampleFromProp(name, prop, obj, context); + } + return obj; + } + + if (type === "array") { + if (Array.isArray(items?.anyOf)) { + return processArrayItems(items, "anyOf", context); + } + + if (Array.isArray(items?.oneOf)) { + return processArrayItems(items, "oneOf", context); + } + + return normalizeArray(sampleFromSchema(items, context)); + } + + if (schemaCopy.enum) { + if (schemaCopy.default) { + return schemaCopy.default; + } + return normalizeArray(schemaCopy.enum)[0]; + } + + if (shouldExcludeProperty(schemaCopy, context)) { + return undefined; + } + + return primitive(schemaCopy); + } catch (err) { + console.error( + chalk.yellow("WARNING: failed to create example from schema object:", err) + ); + return; + } +}; + +function primitive(schema: SchemaObject = {}) { + let { type, format } = schema; + + if (type === undefined) { + return; + } + + let fn = schema.default ? () => schema.default : primitives[type].default; + + if (format !== undefined) { + fn = primitives[type][format] || fn; + } + + if (fn) { + return fn(schema); + } + + return "Unknown Type: " + schema.type; +} + +function normalizeArray(arr: any) { + if (Array.isArray(arr)) { + return arr; + } + return [arr]; +} + +function processArrayItems( + items: SchemaObject, + schemaType: "anyOf" | "oneOf", + context: ExampleContext +): any[] { + const itemsArray = items[schemaType] as SchemaObject[]; + return itemsArray.map((item: SchemaObject) => { + // If items has properties, merge them with each item + if (items.properties) { + const combinedSchema = { + ...item, + properties: { + ...items.properties, // Common properties from parent + ...item.properties, // Specific properties from this anyOf/oneOf item + }, + }; + // Remove anyOf/oneOf to prevent infinite recursion when calling sampleFromSchema + delete combinedSchema[schemaType]; + return sampleFromSchema(combinedSchema, context); + } + return sampleFromSchema(item, context); + }); +} From cd478ed32b915c6c4b7570329ed17add08b32e37 Mon Sep 17 00:00:00 2001 From: "takaoka.daisuke" Date: Wed, 11 Jun 2025 11:22:43 +0900 Subject: [PATCH 6/8] refactor: change ResponseSamples to CodeSamples --- .../src/openapi/createSchemaExample.ts | 4 +- .../src/theme/CodeSamples/_CodeSamples.scss | 3 ++ .../index.tsx | 15 ++---- .../src/theme/Examples/index.tsx | 28 ++++++----- .../src/theme/RequestExamples/index.tsx | 50 ------------------- .../src/theme/RequestSchema/index.tsx | 29 ++++++----- .../src/theme/ResponseExamples/index.tsx | 35 ------------- .../ResponseSamples/_ResponseSamples.scss | 3 -- .../src/theme/ResponseSchema/index.tsx | 45 +++++++++++------ .../src/theme/styles.scss | 2 +- 10 files changed, 73 insertions(+), 141 deletions(-) create mode 100644 packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/_CodeSamples.scss rename packages/docusaurus-theme-openapi-docs/src/theme/{ResponseSamples => CodeSamples}/index.tsx (62%) delete mode 100644 packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx delete mode 100644 packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx delete mode 100644 packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/_ResponseSamples.scss diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts index de0abfc73..4090b6489 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts @@ -56,9 +56,9 @@ const primitives: Primitives = { }, }; -type ExampleType = "request" | "response"; +export type ExampleType = "request" | "response"; -interface ExampleContext { +export interface ExampleContext { type: ExampleType; } diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/_CodeSamples.scss b/packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/_CodeSamples.scss new file mode 100644 index 000000000..2b79a3633 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/_CodeSamples.scss @@ -0,0 +1,3 @@ +.openapi-code__code-samples-container { + margin-top: 2rem; +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/index.tsx similarity index 62% rename from packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/index.tsx rename to packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/index.tsx index dc0f759ab..9fbde2345 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/index.tsx @@ -11,21 +11,16 @@ import CodeBlock from "@theme/CodeBlock"; import { Language } from "prism-react-renderer"; export interface Props { - readonly responseExample: string; + readonly example: string; readonly language: Language; } -function ResponseSamples({ - responseExample, - language, -}: Props): React.JSX.Element { +function CodeSamples({ example, language }: Props): React.JSX.Element { return ( -
- - {responseExample} - +
+ {example}
); } -export default ResponseSamples; +export default CodeSamples; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx index 172bedff4..5d8d779b7 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx @@ -7,10 +7,13 @@ import React from "react"; +import CodeSamples from "@theme/CodeSamples"; import Markdown from "@theme/Markdown"; -import ResponseSamples from "@theme/ResponseSamples"; import TabItem from "@theme/TabItem"; -import { sampleResponseFromSchema } from "docusaurus-plugin-openapi-docs/lib/openapi/createResponseExample"; +import { + sampleFromSchema, + ExampleContext, +} from "docusaurus-plugin-openapi-docs/lib/openapi/createSchemaExample"; import format from "xml-formatter"; export function json2xml(o: Record, tab: string): string { @@ -79,7 +82,7 @@ export const MimeExample: React.FC = ({ {example.summary} )} - + ); }; @@ -111,10 +114,7 @@ export const MimeExamples: React.FC = ({ {exampleValue.summary} )} - + ); } @@ -145,7 +145,7 @@ export const SchemaExample: React.FC = ({ {example.summary} )} - + ); }; @@ -172,7 +172,7 @@ export const SchemaExamples: React.FC = ({ return ( // @ts-ignore - + ); }); @@ -183,13 +183,15 @@ export const SchemaExamples: React.FC = ({ export interface ExampleFromSchemaProps { schema: any; mimeType: string; + context: ExampleContext; } export const ExampleFromSchema: React.FC = ({ schema, mimeType, + context, }) => { - const example = sampleResponseFromSchema(schema); + const example = sampleFromSchema(schema, context); if (mimeType.endsWith("xml")) { let exampleObject; @@ -222,7 +224,7 @@ export const ExampleFromSchema: React.FC = ({ return ( // @ts-ignore - + ); } @@ -232,8 +234,8 @@ export const ExampleFromSchema: React.FC = ({ return ( // @ts-ignore - diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx deleted file mode 100644 index 8ef6da848..000000000 --- a/packages/docusaurus-theme-openapi-docs/src/theme/RequestExamples/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* ============================================================================ - * 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 { - MimeExamples, - MimeExample, - SchemaExample, - SchemaExamples, - MimeExampleProps, - MimeExamplesProps, - SchemaExampleProps, - SchemaExamplesProps, - ExampleFromSchema, -} from "@theme/Examples"; - -export const RequestMimeExample: React.FC = ({ - example, - mimeType, -}) => { - return MimeExample({ example, mimeType }); -}; - -export const RequestMimeExamples: React.FC = ({ - examples, - mimeType, -}) => { - return MimeExamples({ examples, mimeType }); -}; - -export const RequestSchemaExample: React.FC = ({ - example, - mimeType, -}) => { - return SchemaExample({ example, mimeType }); -}; - -export const RequestSchemaExamples: React.FC = ({ - examples, - mimeType, -}) => { - return SchemaExamples({ examples, mimeType }); -}; - -export { ExampleFromSchema }; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx index 23db8d4e7..c2e0c4d7c 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx @@ -9,15 +9,15 @@ import React from "react"; import BrowserOnly from "@docusaurus/BrowserOnly"; import Details from "@theme/Details"; -import Markdown from "@theme/Markdown"; -import MimeTabs from "@theme/MimeTabs"; // Assume these components exist import { ExampleFromSchema, - RequestMimeExample, - RequestMimeExamples, - RequestSchemaExample, - RequestSchemaExamples, -} from "@theme/RequestExamples"; + MimeExample, + MimeExamples, + SchemaExample, + SchemaExamples, +} from "@theme/Examples"; +import Markdown from "@theme/Markdown"; +import MimeTabs from "@theme/MimeTabs"; // Assume these components exist import SchemaNode from "@theme/Schema"; import SchemaTabs from "@theme/SchemaTabs"; import SkeletonLoader from "@theme/SkeletonLoader"; @@ -110,19 +110,22 @@ const RequestSchemaComponent: React.FC = ({ title, body, style }) => { {firstBody && - ExampleFromSchema({ schema: firstBody, mimeType })} + ExampleFromSchema({ + schema: firstBody, + mimeType, + context: { type: "request" }, + })} {mimeExamples && - RequestMimeExamples({ examples: mimeExamples, mimeType })} + MimeExamples({ examples: mimeExamples, mimeType })} - {mimeExample && - RequestMimeExample({ example: mimeExample, mimeType })} + {mimeExample && MimeExample({ example: mimeExample, mimeType })} {schemaExamples && - RequestSchemaExamples({ examples: schemaExamples, mimeType })} + SchemaExamples({ examples: schemaExamples, mimeType })} {schemaExample && - RequestSchemaExample({ example: schemaExample, mimeType })} + SchemaExample({ example: schemaExample, mimeType })} ); diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx deleted file mode 100644 index 6850fcf28..000000000 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* ============================================================================ - * 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 { - MimeExamples, - MimeExample, - MimeExampleProps, - MimeExamplesProps, - ExampleFromSchema, -} from "@theme/Examples"; - -export const ResponseExamples: React.FC = ({ - examples, - mimeType, -}) => { - return MimeExamples({ examples, mimeType }); - // return ; -}; - -export const ResponseExample: React.FC = ({ - example, - mimeType, -}) => { - return MimeExample({ example, mimeType }); - // return ; -}; - -// Re-export ExampleFromSchema with original name -export { ExampleFromSchema }; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/_ResponseSamples.scss b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/_ResponseSamples.scss deleted file mode 100644 index 798db36bd..000000000 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/_ResponseSamples.scss +++ /dev/null @@ -1,3 +0,0 @@ -.openapi-code__response-samples-container { - margin-top: 2rem; -} 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 82b5bf359..fedaccedb 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx @@ -9,13 +9,15 @@ import React from "react"; import BrowserOnly from "@docusaurus/BrowserOnly"; import Details from "@theme/Details"; -import Markdown from "@theme/Markdown"; -import MimeTabs from "@theme/MimeTabs"; // Assume these components exist import { ExampleFromSchema, - ResponseExample, - ResponseExamples, -} from "@theme/ResponseExamples"; + MimeExample, + MimeExamples, + SchemaExample, + SchemaExamples, +} from "@theme/Examples"; +import Markdown from "@theme/Markdown"; +import MimeTabs from "@theme/MimeTabs"; // Assume these components exist import SchemaNode from "@theme/Schema"; import SchemaTabs from "@theme/SchemaTabs"; import SkeletonLoader from "@theme/SkeletonLoader"; @@ -54,15 +56,20 @@ const ResponseSchemaComponent: React.FC = ({ return ( {mimeTypes.map((mimeType: any) => { - const responseExamples = body.content![mimeType].examples; - const responseExample = body.content![mimeType].example; + const mimeExamples = body.content![mimeType].examples; + const mimeExample = body.content![mimeType].example; + const schemaExamples = body.content![mimeType].schema?.examples; + const schemaExample = body.content![mimeType].schema?.example; + const firstBody: any = body.content![mimeType].schema ?? body.content![mimeType]; if ( firstBody === undefined && - responseExample === undefined && - responseExamples === undefined + mimeExample === undefined && + mimeExamples === undefined && + schemaExample === undefined && + schemaExamples === undefined ) { return undefined; } @@ -109,13 +116,23 @@ const ResponseSchemaComponent: React.FC = ({ {firstBody && - ExampleFromSchema({ schema: firstBody, mimeType })} + ExampleFromSchema({ + schema: firstBody, + mimeType, + context: { type: "response" }, + })} + + {mimeExamples && + MimeExamples({ examples: mimeExamples, mimeType })} + + {mimeExample && + MimeExample({ example: mimeExample, mimeType })} - {responseExamples && - ResponseExamples({ examples: responseExamples, mimeType })} + {schemaExamples && + SchemaExamples({ examples: schemaExamples, mimeType })} - {responseExample && - ResponseExample({ example: responseExample, mimeType })} + {schemaExample && + SchemaExample({ example: schemaExample, mimeType })} ); diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss b/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss index db8a648ee..da1b29937 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss +++ b/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss @@ -38,7 +38,7 @@ @use "./SchemaTabs/SchemaTabs"; @use "./OperationTabs/OperationTabs"; /* Code Samples */ -@use "./ResponseSamples/ResponseSamples"; +@use "./CodeSamples/CodeSamples"; /* Markdown Styling */ @use "./Markdown/Details/Details"; From bf1af5f7fa66d0176062a555565ace7e1eee1592 Mon Sep 17 00:00:00 2001 From: "takaoka.daisuke" Date: Wed, 11 Jun 2025 18:23:41 +0900 Subject: [PATCH 7/8] refactor: add BaseSchemaComponent --- demo/examples/tests/examples.yaml | 2 +- .../src/theme/BaseSchema/index.tsx | 159 ++++++++++++++++++ .../src/theme/Markdown/Details/_Details.scss | 6 +- .../src/theme/RequestSchema/index.tsx | 124 +------------- .../src/theme/ResponseSchema/index.tsx | 133 +-------------- 5 files changed, 165 insertions(+), 259 deletions(-) create mode 100644 packages/docusaurus-theme-openapi-docs/src/theme/BaseSchema/index.tsx diff --git a/demo/examples/tests/examples.yaml b/demo/examples/tests/examples.yaml index b7556b555..be999ac07 100644 --- a/demo/examples/tests/examples.yaml +++ b/demo/examples/tests/examples.yaml @@ -57,7 +57,7 @@ paths: example2: summary: "name example 2" description: "name example 2 description" - value: "Jane Doe" + value: "Jane Smith" responses: "204": description: no content diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/BaseSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/BaseSchema/index.tsx new file mode 100644 index 000000000..3758af6fc --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/BaseSchema/index.tsx @@ -0,0 +1,159 @@ +/* ============================================================================ + * 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 BrowserOnly from "@docusaurus/BrowserOnly"; +import Details from "@theme/Details"; +import { + ExampleFromSchema, + MimeExample, + MimeExamples, + SchemaExample, + SchemaExamples, +} from "@theme/Examples"; +import Markdown from "@theme/Markdown"; +import MimeTabs from "@theme/MimeTabs"; +import SchemaNode from "@theme/Schema"; +import SchemaTabs from "@theme/SchemaTabs"; +import SkeletonLoader from "@theme/SkeletonLoader"; +import TabItem from "@theme/TabItem"; +import { MediaTypeObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types"; + +interface Props { + style?: React.CSSProperties; + title: string; + body: { + content?: { + [key: string]: MediaTypeObject; + }; + description?: string; + required?: string[] | boolean; + }; + schemaType: "request" | "response"; +} + +const BaseSchemaComponent: React.FC = ({ + title, + body, + style, + schemaType, +}) => { + if ( + body === undefined || + body.content === undefined || + Object.keys(body).length === 0 || + Object.keys(body.content).length === 0 + ) { + return null; + } + + const mimeTypes = Object.keys(body.content); + if (mimeTypes && mimeTypes.length) { + return ( + + {mimeTypes.map((mimeType: any) => { + const mimeExamples = body.content![mimeType].examples; + const mimeExample = body.content![mimeType].example; + const schemaExamples = body.content![mimeType].schema?.examples; + const schemaExample = body.content![mimeType].schema?.example; + const firstBody = body.content![mimeType].schema; + + if ( + firstBody === undefined || + (firstBody.properties && + Object.keys(firstBody.properties).length === 0) + ) { + return null; + } + + if (firstBody) { + const tabTitle = "Schema"; + return ( + // @ts-ignore + + + {/* @ts-ignore */} + +
+ + + {title} + {body.required && ( + + required + + )} + + + + } + > +
+ {body.description && ( +
+ {body.description} +
+ )} +
+
    + +
+
+
+ {firstBody && + ExampleFromSchema({ + schema: firstBody, + mimeType, + context: { type: schemaType }, + })} + + {mimeExamples && + MimeExamples({ examples: mimeExamples, mimeType })} + + {mimeExample && + MimeExample({ example: mimeExample, mimeType })} + + {schemaExamples && + SchemaExamples({ examples: schemaExamples, mimeType })} + + {schemaExample && + SchemaExample({ example: schemaExample, mimeType })} +
+
+ ); + } + return null; + })} +
+ ); + } + return null; +}; + +const BaseSchema: React.FC = (props) => { + return ( + }> + {() => { + return ; + }} + + ); +}; + +export default BaseSchema; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/Details/_Details.scss b/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/Details/_Details.scss index 96e992073..41a8f8b92 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/Details/_Details.scss +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/Details/_Details.scss @@ -24,11 +24,7 @@ /* Top-Level Details Caret Styling */ .openapi-left-panel__container > .openapi-markdown__details > summary::before, .openapi-markdown__details.mime > summary::before { - top: 0.1rem; -} - -.openapi-markdown__details.response > summary::before { - top: 0.25rem; /* TODO: figure out why this is necessary */ + top: 0.25rem; } /* End of Top-Level Details Caret Styling */ diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx index c2e0c4d7c..00ec65e24 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx @@ -7,21 +7,7 @@ import React from "react"; -import BrowserOnly from "@docusaurus/BrowserOnly"; -import Details from "@theme/Details"; -import { - ExampleFromSchema, - MimeExample, - MimeExamples, - SchemaExample, - SchemaExamples, -} from "@theme/Examples"; -import Markdown from "@theme/Markdown"; -import MimeTabs from "@theme/MimeTabs"; // Assume these components exist -import SchemaNode from "@theme/Schema"; -import SchemaTabs from "@theme/SchemaTabs"; -import SkeletonLoader from "@theme/SkeletonLoader"; -import TabItem from "@theme/TabItem"; +import BaseSchema from "@theme/BaseSchema"; import { MediaTypeObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types"; interface Props { @@ -36,114 +22,8 @@ interface Props { }; } -const RequestSchemaComponent: React.FC = ({ title, body, style }) => { - if ( - body === undefined || - body.content === undefined || - Object.keys(body).length === 0 || - Object.keys(body.content).length === 0 - ) { - return null; - } - - const SchemaTitle = "Schema"; - const mimeTypes = Object.keys(body.content); - if (mimeTypes && mimeTypes.length) { - return ( - - {mimeTypes.map((mimeType) => { - const mimeExamples = body.content![mimeType].examples; - const mimeExample = body.content![mimeType].example; - const schemaExamples = body.content![mimeType].schema?.examples; - const schemaExample = body.content![mimeType].schema?.example; - - const firstBody = body.content![mimeType].schema; - if ( - firstBody === undefined || - (firstBody.properties && - Object.keys(firstBody.properties).length === 0) - ) { - return null; - } - return ( - // @ts-ignore - - - {/* @ts-ignore */} - -
- -

- {title} - {body.required && ( - - required - - )} -

-
- - } - > -
- {body.description && ( -
- {body.description} -
- )} -
-
    - -
-
-
- {firstBody && - ExampleFromSchema({ - schema: firstBody, - mimeType, - context: { type: "request" }, - })} - - {mimeExamples && - MimeExamples({ examples: mimeExamples, mimeType })} - - {mimeExample && MimeExample({ example: mimeExample, mimeType })} - - {schemaExamples && - SchemaExamples({ examples: schemaExamples, mimeType })} - - {schemaExample && - SchemaExample({ example: schemaExample, mimeType })} -
-
- ); - })} -
- ); - } - return null; -}; - const RequestSchema: React.FC = (props) => { - return ( - }> - {() => { - return ; - }} - - ); + return ; }; export default RequestSchema; 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 fedaccedb..023385765 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx @@ -7,21 +7,7 @@ import React from "react"; -import BrowserOnly from "@docusaurus/BrowserOnly"; -import Details from "@theme/Details"; -import { - ExampleFromSchema, - MimeExample, - MimeExamples, - SchemaExample, - SchemaExamples, -} from "@theme/Examples"; -import Markdown from "@theme/Markdown"; -import MimeTabs from "@theme/MimeTabs"; // Assume these components exist -import SchemaNode from "@theme/Schema"; -import SchemaTabs from "@theme/SchemaTabs"; -import SkeletonLoader from "@theme/SkeletonLoader"; -import TabItem from "@theme/TabItem"; +import BaseSchema from "@theme/BaseSchema"; import { MediaTypeObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types"; interface Props { @@ -36,123 +22,8 @@ interface Props { }; } -const ResponseSchemaComponent: React.FC = ({ - title, - body, - style, -}): any => { - if ( - body === undefined || - body.content === undefined || - Object.keys(body).length === 0 || - Object.keys(body.content).length === 0 - ) { - return null; - } - - // Get all MIME types, including vendor-specific - const mimeTypes = Object.keys(body.content); - if (mimeTypes && mimeTypes.length) { - return ( - - {mimeTypes.map((mimeType: any) => { - const mimeExamples = body.content![mimeType].examples; - const mimeExample = body.content![mimeType].example; - const schemaExamples = body.content![mimeType].schema?.examples; - const schemaExample = body.content![mimeType].schema?.example; - - const firstBody: any = - body.content![mimeType].schema ?? body.content![mimeType]; - - if ( - firstBody === undefined && - mimeExample === undefined && - mimeExamples === undefined && - schemaExample === undefined && - schemaExamples === undefined - ) { - return undefined; - } - - if (firstBody) { - return ( - // @ts-ignore - - - {/* @ts-ignore */} - -
- - - {title} - {body.required === true && ( - - required - - )} - - - - } - > -
- {body.description && ( -
- {body.description} -
- )} -
-
    - -
-
-
- {firstBody && - ExampleFromSchema({ - schema: firstBody, - mimeType, - context: { type: "response" }, - })} - - {mimeExamples && - MimeExamples({ examples: mimeExamples, mimeType })} - - {mimeExample && - MimeExample({ example: mimeExample, mimeType })} - - {schemaExamples && - SchemaExamples({ examples: schemaExamples, mimeType })} - - {schemaExample && - SchemaExample({ example: schemaExample, mimeType })} -
-
- ); - } - return undefined; - })} -
- ); - } - return undefined; -}; - const ResponseSchema: React.FC = (props) => { - return ( - }> - {() => { - return ; - }} - - ); + return ; }; export default ResponseSchema; From 084cf87f4ccf2f122dd31bed0ea3a66abd5fb3af Mon Sep 17 00:00:00 2001 From: "takaoka.daisuke" Date: Fri, 13 Jun 2025 14:02:34 +0900 Subject: [PATCH 8/8] fix: primitive --- .../src/openapi/createSchemaExample.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts index de0abfc73..ea0ddcf0c 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts @@ -234,14 +234,27 @@ function primitive(schema: SchemaObject = {}) { return; } - let fn = schema.default ? () => schema.default : primitives[type].default; + // If type is an array, use the first type + if (Array.isArray(type)) { + type = type[0]; + if (type === undefined) { + return; + } + } - if (format !== undefined) { - fn = primitives[type][format] || fn; + // Use schema default if available, otherwise use type default + if (schema.default !== undefined) { + return schema.default; } - if (fn) { - return fn(schema); + const typeConfig = primitives[type]; + if (typeConfig) { + if (format !== undefined && typeConfig[format] !== undefined) { + return typeConfig[format](schema); + } + if (typeConfig.default !== undefined) { + return typeConfig.default(schema); + } } return "Unknown Type: " + schema.type;