From 8de461606f488addfe64daac9db4f6ae87801812 Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:34:41 -0500 Subject: [PATCH 01/15] Improve circular reference detection --- .../src/openapi/utils/loadAndResolveSpec.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts index ee59054b2..79629a113 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts @@ -22,11 +22,19 @@ function serializer(replacer: any, cycleReplacer: any) { if (cycleReplacer === undefined) cycleReplacer = function (key: any, value: any) { + if (value?.["x-circular-ref"]) { + return value.title ? `circular(${value.title})` : "circular()"; + } if (stack[0] === value) return "circular()"; return value.title ? `circular(${value.title})` : "circular()"; }; return function (key: any, value: any) { + // Convert objects flagged as circular + if (value?.["x-circular-ref"]) { + return value.title ? `circular(${value.title})` : "circular()"; + } + // Resolve discriminator ref pointers if (value?.discriminator !== undefined) { const parser = new OpenAPIParser(stack[0]); @@ -115,6 +123,49 @@ async function resolveJsonRefs(specUrlOrObject: object | string) { } } +function markCircularRefs(obj: any, stack: any[] = []): boolean { + if (!obj || typeof obj !== "object") { + return false; + } + + const pos = stack.indexOf(obj); + if (pos !== -1) { + for (let i = pos + 1; i < stack.length; i++) { + const cyc = stack[i]; + if (cyc && typeof cyc === "object") { + cyc["x-circular-ref"] = true; + } + } + return true; + } + + stack.push(obj); + let foundCircular = false; + + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + const val = obj[i]; + if (typeof val === "object" && val !== null) { + if (markCircularRefs(val, stack)) { + foundCircular = true; + } + } + } + } else { + for (const key of Object.keys(obj)) { + const val = obj[key]; + if (typeof val === "object" && val !== null) { + if (markCircularRefs(val, stack)) { + foundCircular = true; + } + } + } + } + + stack.pop(); + return foundCircular; +} + export async function loadAndResolveSpec(specUrlOrObject: object | string) { const config = new Config({} as ResolvedConfig); const bundleOpts = { @@ -154,6 +205,9 @@ export async function loadAndResolveSpec(specUrlOrObject: object | string) { const resolved = await resolveJsonRefs(parsed); + // Mark any remaining circular references + markCircularRefs(resolved); + // Force serialization and replace circular $ref pointers // @ts-ignore const serialized = JSON.stringify(resolved, serializer()); From ffa796881b84b149cf68370ca17d6e4567cbf1fe Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:09:32 -0500 Subject: [PATCH 02/15] Add self-referencing schema detection --- .../__fixtures__/examples/self-ref.yaml | 15 +++++++++++ .../src/openapi/circular.test.ts | 25 +++++++++++++++++++ .../src/openapi/utils/loadAndResolveSpec.ts | 25 ++++++++++--------- 3 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/self-ref.yaml create mode 100644 packages/docusaurus-plugin-openapi-docs/src/openapi/circular.test.ts diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/self-ref.yaml b/packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/self-ref.yaml new file mode 100644 index 000000000..6c5024783 --- /dev/null +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/self-ref.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.3 +info: + title: Self Ref Example + version: 1.0.0 +paths: {} +components: + schemas: + Node: + title: Node + type: object + properties: + name: + type: string + child: + $ref: "#/components/schemas/Node" diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/circular.test.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/circular.test.ts new file mode 100644 index 000000000..b4fa93098 --- /dev/null +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/circular.test.ts @@ -0,0 +1,25 @@ +/* ============================================================================ + * 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 path from "path"; + +// eslint-disable-next-line import/no-extraneous-dependencies +import { posixPath } from "@docusaurus/utils"; + +import { loadAndResolveSpec } from "./utils/loadAndResolveSpec"; + +describe("circular references", () => { + it("flags self referencing schemas", async () => { + const file = posixPath( + path.join(__dirname, "__fixtures__/examples/self-ref.yaml") + ); + const spec: any = await loadAndResolveSpec(file); + expect(spec.components.schemas.Node.properties.child).toBe( + "circular(Node)" + ); + }); +}); diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts index 79629a113..af2163c5f 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts @@ -123,20 +123,13 @@ async function resolveJsonRefs(specUrlOrObject: object | string) { } } -function markCircularRefs(obj: any, stack: any[] = []): boolean { +function markCircularRefs(obj: any, stack: any[] = []): boolean | "circular" { if (!obj || typeof obj !== "object") { return false; } - const pos = stack.indexOf(obj); - if (pos !== -1) { - for (let i = pos + 1; i < stack.length; i++) { - const cyc = stack[i]; - if (cyc && typeof cyc === "object") { - cyc["x-circular-ref"] = true; - } - } - return true; + if (stack.includes(obj)) { + return "circular"; } stack.push(obj); @@ -146,7 +139,11 @@ function markCircularRefs(obj: any, stack: any[] = []): boolean { for (let i = 0; i < obj.length; i++) { const val = obj[i]; if (typeof val === "object" && val !== null) { - if (markCircularRefs(val, stack)) { + const res = markCircularRefs(val, stack); + if (res) { + if (res === "circular") { + obj[i] = { ...val, "x-circular-ref": true }; + } foundCircular = true; } } @@ -155,7 +152,11 @@ function markCircularRefs(obj: any, stack: any[] = []): boolean { for (const key of Object.keys(obj)) { const val = obj[key]; if (typeof val === "object" && val !== null) { - if (markCircularRefs(val, stack)) { + const res = markCircularRefs(val, stack); + if (res) { + if (res === "circular") { + obj[key] = { ...val, "x-circular-ref": true }; + } foundCircular = true; } } From f950f1e77d15ef87c1ef059bd132cac10b29932f Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:57:30 -0500 Subject: [PATCH 03/15] Fix circular ref marking to avoid infinite recursion --- .../src/openapi/utils/loadAndResolveSpec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts index af2163c5f..3a4525c7f 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts @@ -142,7 +142,7 @@ function markCircularRefs(obj: any, stack: any[] = []): boolean | "circular" { const res = markCircularRefs(val, stack); if (res) { if (res === "circular") { - obj[i] = { ...val, "x-circular-ref": true }; + obj[i] = { title: val.title, "x-circular-ref": true }; } foundCircular = true; } @@ -155,7 +155,7 @@ function markCircularRefs(obj: any, stack: any[] = []): boolean | "circular" { const res = markCircularRefs(val, stack); if (res) { if (res === "circular") { - obj[key] = { ...val, "x-circular-ref": true }; + obj[key] = { title: val.title, "x-circular-ref": true }; } foundCircular = true; } From a58b774d7c401b620beb5cb3e5de823cdda2a2da Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:12:49 -0500 Subject: [PATCH 04/15] docs: document x-circular-ref support --- demo/docs/intro.mdx | 2 +- demo/docs/vendor-extensions.mdx | 2 +- packages/docusaurus-plugin-openapi-docs/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/docs/intro.mdx b/demo/docs/intro.mdx index d4779c3f7..5de200071 100644 --- a/demo/docs/intro.mdx +++ b/demo/docs/intro.mdx @@ -69,7 +69,7 @@ The plugin extracts a number of vendor extensions from the OpenAPI spec to enric | `x-displayName` | Overrides tag display names. | | `x-enumDescription` / `x-enumDescriptions` | Documents enum values. | -Other ReDoc specific extensions such as `x-circular-ref`, `x-code-samples` (deprecated), `x-examples`, `x-ignoredHeaderParameters`, `x-nullable`, `x-servers`, `x-traitTag`, `x-additionalPropertiesName`, and `x-explicitMappingOnly` are ignored when extracting custom data. +Circular references flagged with `x-circular-ref` or detected automatically are serialized as `circular()`. Other ReDoc specific extensions such as `x-code-samples` (deprecated), `x-examples`, `x-ignoredHeaderParameters`, `x-nullable`, `x-servers`, `x-traitTag`, `x-additionalPropertiesName`, and `x-explicitMappingOnly` are ignored when extracting custom data. --- diff --git a/demo/docs/vendor-extensions.mdx b/demo/docs/vendor-extensions.mdx index f0aede3e8..761e12606 100644 --- a/demo/docs/vendor-extensions.mdx +++ b/demo/docs/vendor-extensions.mdx @@ -21,4 +21,4 @@ The OpenAPI plugin and theme recognize several [vendor extensions](https://swagg | `x-displayName` | Override tag names used for grouping. | | `x-enumDescription` / `x-enumDescriptions` | Document individual enum values. | -Other ReDoc extensions such as `x-circular-ref`, `x-code-samples` (deprecated), `x-examples`, `x-ignoredHeaderParameters`, `x-nullable`, `x-servers`, `x-traitTag`, `x-additionalPropertiesName`, and `x-explicitMappingOnly` are detected but ignored when extracting custom extensions. +Circular references flagged with `x-circular-ref` or detected automatically are serialized as `circular(<title>)`. Other ReDoc extensions such as `x-code-samples` (deprecated), `x-examples`, `x-ignoredHeaderParameters`, `x-nullable`, `x-servers`, `x-traitTag`, `x-additionalPropertiesName`, and `x-explicitMappingOnly` are detected but ignored when extracting custom extensions. diff --git a/packages/docusaurus-plugin-openapi-docs/README.md b/packages/docusaurus-plugin-openapi-docs/README.md index 73121243d..1d1928390 100644 --- a/packages/docusaurus-plugin-openapi-docs/README.md +++ b/packages/docusaurus-plugin-openapi-docs/README.md @@ -238,7 +238,7 @@ The plugin extracts a number of vendor extensions from the OpenAPI spec to enric | `x-displayName` | Overrides tag display names. | | `x-enumDescription` / `x-enumDescriptions` | Documents enum values. | -Other ReDoc specific extensions such as `x-circular-ref`, `x-code-samples` (deprecated), `x-examples`, `x-ignoredHeaderParameters`, `x-nullable`, `x-servers`, `x-traitTag`, `x-additionalPropertiesName`, and `x-explicitMappingOnly` are ignored when extracting custom data. +Circular references flagged with `x-circular-ref` or detected automatically are serialized as `circular(<title>)`. Other ReDoc specific extensions such as `x-code-samples` (deprecated), `x-examples`, `x-ignoredHeaderParameters`, `x-nullable`, `x-servers`, `x-traitTag`, `x-additionalPropertiesName`, and `x-explicitMappingOnly` are ignored when extracting custom data. ## CLI Usage From bebd1439bd153363ee16f7b28cfc324fedd7e280 Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Mon, 14 Jul 2025 11:36:31 -0500 Subject: [PATCH 05/15] Add circular label styling --- .../src/theme/SchemaItem/_SchemaItem.scss | 11 +++++++++++ .../src/theme/SchemaItem/index.tsx | 12 ++++++++++++ .../src/theme/styles.scss | 1 + 3 files changed, 24 insertions(+) diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/_SchemaItem.scss b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/_SchemaItem.scss index e5bd389ea..6aedc8605 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/_SchemaItem.scss +++ b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/_SchemaItem.scss @@ -67,6 +67,17 @@ background-color: transparent; } +.openapi-schema__circular { + display: inline-flex; + align-items: center; + font-size: 10.5px; + font-weight: bold; + text-transform: uppercase; + color: var(--openapi-circular); + margin-left: 1%; + background-color: transparent; +} + .openapi-schema__strikethrough { text-decoration: line-through; } diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx index a77d1f3ee..288223466 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx @@ -92,6 +92,17 @@ export default function SchemaItem(props: Props) { <span className="openapi-schema__nullable">nullable</span> )); + const circularMatch = + typeof schemaName === "string" && schemaName.startsWith("circular(") + ? schemaName.match(/^circular\\(([^)]*)\\)/) + : null; + + const renderCircular = guard(circularMatch, () => ( + <span className="openapi-schema__circular"> + {circularMatch ? `circular(${circularMatch[1]})` : "circular"} + </span> + )); + const renderEnumDescriptions = guard( getEnumDescriptionMarkdown(enumDescriptions), (value) => { @@ -206,6 +217,7 @@ export default function SchemaItem(props: Props) { {renderNullable} {renderRequired} {renderDeprecated} + {renderCircular} </span> {renderSchemaDescription} {renderEnumDescriptions} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss b/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss index 5e0933346..fef5bd6ec 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss +++ b/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss @@ -46,6 +46,7 @@ --openapi-required: var(--ifm-color-danger); --openapi-deprecated: var(--ifm-color-warning); --openapi-nullable: var(--ifm-color-info); + --openapi-circular: var(--ifm-color-primary); --openapi-code-blue: var(--ifm-color-info); --openapi-code-red: var(--ifm-color-danger); --openapi-code-orange: var(--ifm-color-warning); From 7f94b4813b49be43b58c907fc96f70dc64525f49 Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:02:31 -0500 Subject: [PATCH 06/15] Fix circular allOf merging --- .../src/markdown/createSchema.ts | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts index bea754db0..492c070a9 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts @@ -783,17 +783,49 @@ export function createNodes( } if (schema.allOf !== undefined) { - const mergedSchemas = mergeAllOf(schema) as SchemaObject; - if ( - mergedSchemas.oneOf !== undefined || - mergedSchemas.anyOf !== undefined + schema.allOf.length && + typeof schema.allOf[0] === "string" && + (schema.allOf[0] as string).includes("circular") ) { - nodes.push(createAnyOneOf(mergedSchemas)); - } + nodes.push( + create("div", { + style: { + marginTop: ".5rem", + marginBottom: ".5rem", + marginLeft: "1rem", + }, + children: createDescription(schema.allOf[0]), + }) + ); - if (mergedSchemas.properties !== undefined) { - nodes.push(createProperties(mergedSchemas)); + const rest = schema.allOf.slice(1); + if (rest.length) { + const mergedSchemas = mergeAllOf({ allOf: rest } as SchemaObject); + if ( + mergedSchemas.oneOf !== undefined || + mergedSchemas.anyOf !== undefined + ) { + nodes.push(createAnyOneOf(mergedSchemas)); + } + + if (mergedSchemas.properties !== undefined) { + nodes.push(createProperties(mergedSchemas)); + } + } + } else { + const mergedSchemas = mergeAllOf(schema) as SchemaObject; + + if ( + mergedSchemas.oneOf !== undefined || + mergedSchemas.anyOf !== undefined + ) { + nodes.push(createAnyOneOf(mergedSchemas)); + } + + if (mergedSchemas.properties !== undefined) { + nodes.push(createProperties(mergedSchemas)); + } } } From 21e4275988514869f28af5fae78c93df6f5e9758 Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:04:43 -0500 Subject: [PATCH 07/15] docs: note to rebuild packages after edits --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 642fcd31a..272b89258 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,8 @@ yarn build-packages yarn watch:demo ``` +Whenever you modify the plugin or theme sources, run `yarn build-packages` again so the demo picks up your latest code. + ## Credits Special thanks to [@bourdakos1](https://github.com/bourdakos1) (Nick Bourdakos), the author of [docusaurus-openapi](https://github.com/cloud-annotations/docusaurus-openapi), which this project is heavily based on. From a9ebe393d7cb4f359ebab04fda9a867b71815de8 Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:50:22 -0500 Subject: [PATCH 08/15] Improve circular ref detection --- .../src/openapi/utils/loadAndResolveSpec.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts index 3a4525c7f..d525e0f01 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts @@ -123,7 +123,11 @@ async function resolveJsonRefs(specUrlOrObject: object | string) { } } -function markCircularRefs(obj: any, stack: any[] = []): boolean | "circular" { +function markCircularRefs( + obj: any, + stack: any[] = [], + key: string | undefined = undefined +): boolean | "circular" { if (!obj || typeof obj !== "object") { return false; } @@ -139,23 +143,29 @@ function markCircularRefs(obj: any, stack: any[] = []): boolean | "circular" { for (let i = 0; i < obj.length; i++) { const val = obj[i]; if (typeof val === "object" && val !== null) { - const res = markCircularRefs(val, stack); + const res = markCircularRefs(val, stack, String(i)); if (res) { if (res === "circular") { - obj[i] = { title: val.title, "x-circular-ref": true }; + obj[i] = { + title: val.title ?? String(i), + "x-circular-ref": true, + }; } foundCircular = true; } } } } else { - for (const key of Object.keys(obj)) { - const val = obj[key]; + for (const k of Object.keys(obj)) { + const val = obj[k]; if (typeof val === "object" && val !== null) { - const res = markCircularRefs(val, stack); + const res = markCircularRefs(val, stack, k); if (res) { if (res === "circular") { - obj[key] = { title: val.title, "x-circular-ref": true }; + obj[k] = { + title: val.title ?? k, + "x-circular-ref": true, + }; } foundCircular = true; } From bb6916c67102c75e8ce8930a391d025711251257 Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:32:34 -0500 Subject: [PATCH 09/15] Handle circular refs anywhere in allOf --- .../src/markdown/createSchema.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts index 492c070a9..952864f47 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts @@ -783,11 +783,11 @@ export function createNodes( } if (schema.allOf !== undefined) { - if ( - schema.allOf.length && - typeof schema.allOf[0] === "string" && - (schema.allOf[0] as string).includes("circular") - ) { + const circularIndex = schema.allOf.findIndex((item: any) => { + return typeof item === "string" && item.includes("circular"); + }); + + if (circularIndex !== -1) { nodes.push( create("div", { style: { @@ -795,11 +795,14 @@ export function createNodes( marginBottom: ".5rem", marginLeft: "1rem", }, - children: createDescription(schema.allOf[0]), + children: createDescription(schema.allOf[circularIndex] as string), }) ); - const rest = schema.allOf.slice(1); + const rest = schema.allOf + .slice(0, circularIndex) + .concat(schema.allOf.slice(circularIndex + 1)); + if (rest.length) { const mergedSchemas = mergeAllOf({ allOf: rest } as SchemaObject); if ( From 4c207edd55a26023b4096367d3f0ff097d4b41a9 Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Wed, 16 Jul 2025 17:17:55 -0500 Subject: [PATCH 10/15] test: verify petstore friend circular --- .../__fixtures__/examples/petstore.yaml | 1355 +++++++++++++++++ .../src/openapi/petstoreCircular.test.ts | 25 + 2 files changed, 1380 insertions(+) create mode 100644 packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/petstore.yaml create mode 100644 packages/docusaurus-plugin-openapi-docs/src/openapi/petstoreCircular.test.ts diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/petstore.yaml b/packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/petstore.yaml new file mode 100644 index 000000000..1de97c514 --- /dev/null +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/petstore.yaml @@ -0,0 +1,1355 @@ +openapi: 3.0.0 +servers: + - url: https://petstore.swagger.io/v2 + description: Default server + - url: https://petstore.swagger.io/sandbox + description: Sandbox server + - url: http://127.0.0.1:4010 + description: Prism Mock API (local) +info: + description: | + This is a sample server Petstore server. + You can find out more about Swagger at + [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). + For this sample, you can use the api key `special-key` to test the authorization filters. + + ## Introduction + This API is documented in **OpenAPI format** and is based on + [Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team. + It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo) + tool and [ReDoc](https://github.com/Redocly/redoc) documentation. In addition to standard + OpenAPI syntax we use a few [vendor extensions](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md). + + ## OpenAPI Specification + This API is documented in **OpenAPI format** and is based on + [Petstore sample](http://petstore.swagger.io/) provided by [swagger.io](http://swagger.io) team. + It was **extended** to illustrate features of [generator-openapi-repo](https://github.com/Rebilly/generator-openapi-repo) + tool and [ReDoc](https://github.com/Redocly/redoc) documentation. In addition to standard + OpenAPI syntax we use a few [vendor extensions](https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md). + + ## Cross-Origin Resource Sharing + This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/). + And that allows cross-domain communication from the browser. + All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site. + + ## Authentication + + Petstore offers two forms of authentication: + - API Key + - OAuth2 + + OAuth2 - an open protocol to allow secure authorization in a simple + and standard method from web, mobile and desktop applications. + + version: 2.0.0 + title: Swagger Petstore YAML + termsOfService: "http://swagger.io/terms/" + contact: + name: API Support + email: apiteam@swagger.io + url: https://github.com/Redocly/redoc + x-logo: + url: "https://redocly.github.io/redoc/petstore-logo.png" + altText: Petstore logo + x-dark-logo: + url: "/img/petstore-logo-dark.png" + altText: "Petstore dark logo" + license: + name: Apache 2.0 + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +externalDocs: + description: Find out how to create Github repo for your OpenAPI spec. + url: "https://github.com/Rebilly/generator-openapi-repo" +tags: + - name: pet + description: Everything about your Pets + x-displayName: Pets + - name: store + description: Access to Petstore orders + x-displayName: Petstore Orders + - name: user + description: Operations about user + x-displayName: Users + - name: pet_model + x-displayName: The Pet Model + description: | + <SchemaDefinition schemaRef="#/components/schemas/Pet" /> + - name: store_model + x-displayName: The Order Model + description: | + <SchemaDefinition schemaRef="#/components/schemas/Order" exampleRef="#/components/examples/Order" showReadOnly={true} showWriteOnly={true} /> +x-tagGroups: + - name: General + tags: + - pet + - store + - name: User Management + tags: + - user + - name: Models + tags: + - pet_model + - store_model +paths: + /pet: + parameters: + - name: Accept-Language + in: header + description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US" + example: en-US + required: false + schema: + type: string + default: en-AU + - name: cookieParam + in: cookie + description: Some cookie + required: true + schema: + type: integer + format: int64 + post: + tags: + - pet + summary: Add a new pet to the store + description: Add new pet to the store inventory. + operationId: addPet + responses: + "200": + description: | + All good, here's some MDX: + + :::note + + Some content but no markdown is supported :( + + ::: + + :::tip + A TIP with no leading or trailing spaces between delimiters. + ::: + + :::info + + Some **content** with _Markdown_ `syntax`. Check [this `api`](#). + + | Month | Savings | + | -------- | ------- | + | January | $250 | + | February | $80 | + | March | $420 | + + Hmm..... + + ::: + + :::warning + + Some **content** with _Markdown_ `syntax`. Check [this `api`](#) which is not supported :( yet + + ::: + + :::danger + + Some plain text + + Some more plain text + + And more + + ::: + + A **code snippet**! + + ```python + print("hello") + ``` + + _And_ a table! + + | Month | Savings | + | -------- | ------- | + | January | $250 | + | February | $80 | + | March | $420 | + content: + application/json: + schema: + type: object + properties: + data: + oneOf: + - type: string + - type: object + "405": + description: Invalid input + security: + - petstore_auth: + - "write:pets" + - "read:pets" + - api_key: [] + - ApiKeyAuth: [] + - BasicAuth: [] + - BearerAuth: [] + - OAuth2: [] + - OpenID: [] + + x-codeSamples: + - lang: "C#" + source: | + PetStore.v1.Pet pet = new PetStore.v1.Pet(); + pet.setApiKey("your api key"); + pet.petType = PetStore.v1.Pet.TYPE_DOG; + pet.name = "Rex"; + // set other fields + PetStoreResponse response = pet.create(); + if (response.statusCode == HttpStatusCode.Created) + { + // Successfully created + } + else + { + // Something wrong -- check response for errors + Console.WriteLine(response.getRawResponse()); + } + - lang: PHP + label: Custom + source: | + $form = new \PetStore\Entities\Pet(); + $form->setPetType("Dog"); + $form->setName("Rex"); + // set other fields + try { + $pet = $client->pets()->create($form); + } catch (UnprocessableEntityException $e) { + var_dump($e->getErrors()); + } + requestBody: + $ref: "#/components/requestBodies/Pet" + put: + tags: + - pet + summary: Update an existing pet + description: "" + operationId: updatePet + responses: + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + security: + - petstore_auth: + - "write:pets" + - "read:pets" + x-codeSamples: + - lang: PHP + source: | + $form = new \PetStore\Entities\Pet(); + $form->setPetId(1); + $form->setPetType("Dog"); + $form->setName("Rex"); + // set other fields + try { + $pet = $client->pets()->update($form); + } catch (UnprocessableEntityException $e) { + var_dump($e->getErrors()); + } + requestBody: + $ref: "#/components/requestBodies/Pet" + "/pet/{petId}": + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + deprecated: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + application/xml: + schema: + $ref: "#/components/schemas/Pet" + + "400": + description: Invalid ID supplied + "404": + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: "" + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + "405": + description: Invalid input + security: + - petstore_auth: + - "write:pets" + - "read:pets" + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: "" + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + example: "Bearer <TOKEN>" + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "400": + description: Invalid pet value + security: + - petstore_auth: + - "write:pets" + - "read:pets" + "/pet/{petId}/uploadImage": + post: + tags: + - pet + summary: uploads an image + description: "" + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiResponse" + security: + - petstore_auth: + - "write:pets" + - "read:pets" + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + schema: + type: array + minItems: 1 + maxItems: 3 + items: + type: string + enum: + - available + - pending + - sold + x-enumDescriptions: + available: When the pet is available + pending: When the pet is being sold + sold: When the pet has been sold + default: available + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + application/xml: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + "400": + description: Invalid status value + security: + - api_key: [] + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + deprecated: true + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + schema: + type: array + items: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + application/xml: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + "400": + description: Invalid tag value + security: + - petstore_auth: + - "write:pets" + - "read:pets" + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: "" + operationId: placeOrder + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + application/xml: + schema: + $ref: "#/components/schemas/Order" + "400": + description: Invalid Order + content: + application/json: + example: + status: 400 + message: "Invalid Order" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + examples: + OrderDelivered: + summary: Order delivered + value: + quantity: 4 + shipDate: 2022-10-12 + status: delivered + requestId: 444-4444-444-4444 + OrderPlaced: + summary: Order placed + value: + quantity: 10 + shipDate: 2022-10-01 + status: placed + requestId: 111-222-333-444 + OrderApproved: + summary: Order approved + value: + quantity: 1000 + shipDate: 2022-09-01 + status: approved + requestId: 000-111-222-333 + description: order placed for purchasing the pet + required: true + "/store/order/{orderId}": + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value <= 5 or > 10. Other values + will generated exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + application/xml: + schema: + $ref: "#/components/schemas/Order" + "400": + description: Invalid ID supplied + "404": + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + minimum: 1 + responses: + "400": + description: Invalid ID supplied + "404": + description: Order not found + /store/subscribe: + post: + tags: + - store + summary: Subscribe to the Store events + description: Add subscription for a store events + requestBody: + content: + application/json: + schema: + type: object + properties: + callbackUrl: + type: string + format: uri + description: This URL will be called by the server when the desired event will occur + example: https://myserver.com/send/callback/here + eventName: + type: string + description: Event name for the subscription + enum: + - orderInProgress + - orderShipped + - orderDelivered + example: orderInProgress + required: + - callbackUrl + - eventName + responses: + "201": + description: Subscription added + content: + application/json: + schema: + type: object + properties: + subscriptionId: + type: string + example: AAA-123-BBB-456 + callbacks: + orderInProgress: + "{$request.body#/callbackUrl}?event={$request.body#/eventName}": + servers: + - url: //callback-url.path-level/v1 + description: Path level server 1 + - url: //callback-url.path-level/v2 + description: Path level server 2 + post: + summary: Order in Progress (Summary) + description: A callback triggered every time an Order is updated status to "inProgress" (Description) + externalDocs: + description: Find out more + url: "https://more-details.com/demo" + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: "123" + timestamp: + type: string + format: date-time + example: "2018-10-19T16:46:45Z" + status: + type: string + example: "inProgress" + application/xml: + schema: + type: object + properties: + orderId: + type: string + example: "123" + example: | + <?xml version="1.0" encoding="UTF-8"?> + <root> + <orderId>123</orderId> + <status>inProgress</status> + <timestamp>2018-10-19T16:46:45Z</timestamp> + </root> + responses: + "200": + description: Callback successfully processed and no retries will be performed + content: + application/json: + schema: + type: object + properties: + someProp: + type: string + example: "123" + "299": + description: Response for cancelling subscription + "500": + description: Callback processing failed and retries will be performed + x-codeSamples: + - lang: "C#" + source: | + PetStore.v1.Pet pet = new PetStore.v1.Pet(); + pet.setApiKey("your api key"); + pet.petType = PetStore.v1.Pet.TYPE_DOG; + pet.name = "Rex"; + // set other fields + PetStoreResponse response = pet.create(); + if (response.statusCode == HttpStatusCode.Created) + { + // Successfully created + } + else + { + // Something wrong -- check response for errors + Console.WriteLine(response.getRawResponse()); + } + - lang: PHP + source: | + $form = new \PetStore\Entities\Pet(); + $form->setPetType("Dog"); + $form->setName("Rex"); + // set other fields + try { + $pet = $client->pets()->create($form); + } catch (UnprocessableEntityException $e) { + var_dump($e->getErrors()); + } + put: + description: Order in Progress (Only Description) + servers: + - url: //callback-url.operation-level/v1 + description: Operation level server 1 (Operation override) + - url: //callback-url.operation-level/v2 + description: Operation level server 2 (Operation override) + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: "123" + timestamp: + type: string + format: date-time + example: "2018-10-19T16:46:45Z" + status: + type: string + example: "inProgress" + application/xml: + schema: + type: object + properties: + orderId: + type: string + example: "123" + example: | + <?xml version="1.0" encoding="UTF-8"?> + <root> + <orderId>123</orderId> + <status>inProgress</status> + <timestamp>2018-10-19T16:46:45Z</timestamp> + </root> + responses: + "200": + description: Callback successfully processed and no retries will be performed + content: + application/json: + schema: + type: object + properties: + someProp: + type: string + example: "123" + orderShipped: + "{$request.body#/callbackUrl}?event={$request.body#/eventName}": + post: + description: A callback triggered every time an Order is shipped to the recipient + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: "123" + timestamp: + type: string + format: date-time + example: "2018-10-19T16:46:45Z" + estimatedDeliveryDate: + type: string + format: date-time + example: "2018-11-11T16:00:00Z" + responses: + "200": + description: Callback successfully processed and no retries will be performed + orderDelivered: + "http://notificationServer.com?url={$request.body#/callbackUrl}&event={$request.body#/eventName}": + post: + deprecated: true + summary: Order delivered + description: A callback triggered every time an Order is delivered to the recipient + requestBody: + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + example: "123" + timestamp: + type: string + format: date-time + example: "2018-10-19T16:46:45Z" + responses: + "200": + description: Callback successfully processed and no retries will be performed + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Created user object + required: true + "/user/{username}": + get: + tags: + - user + summary: Get user by user name + description: "" + operationId: getUserByName + parameters: + - name: username + in: path + description: "The name that needs to be fetched. Use user1 for testing. " + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/User" + application/xml: + schema: + $ref: "#/components/schemas/User" + "400": + description: Invalid username supplied + "404": + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + responses: + "400": + description: Invalid user supplied + "404": + description: User not found + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "400": + description: Invalid username supplied + "404": + description: User not found + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: "" + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + requestBody: + $ref: "#/components/requestBodies/UserArray" + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input list + description: "" + operationId: createUsersWithListInput + responses: + default: + description: successful operation + requestBody: + $ref: "#/components/requestBodies/UserArray" + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: "" + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/json: + schema: + type: string + examples: + response: + summary: | + This is an example of using **Docusaurus** `markdown` in a summary. Note that admonitions are not fully supported. + value: OK + application/xml: + schema: + type: string + examples: + response: + value: <Message> OK </Message> + text/plain: + examples: + response: + value: OK + "400": + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: "" + operationId: logoutUser + responses: + default: + description: successful operation +components: + schemas: + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + Cat: + x-tags: + - pet + description: A representation of a cat + allOf: + - $ref: "#/components/schemas/Pet" + - type: object + properties: + huntingSkill: + type: string + description: The measured skill for hunting + default: lazy + example: adventurous + enum: + - clueless + - lazy + - adventurous + - aggressive + required: + - huntingSkill + Category: + type: object + properties: + id: + description: Category ID + allOf: + - $ref: "#/components/schemas/Id" + name: + description: Category name + type: string + minLength: 1 + sub: + description: Test Sub Category + type: object + properties: + prop1: + type: string + description: Dumb Property + xml: + name: Category + Dog: + description: A representation of a dog + allOf: + - $ref: "#/components/schemas/Pet" + - type: object + properties: + packSize: + type: integer + format: int32 + description: The size of the pack the dog is from + default: 1 + minimum: 1 + required: + - packSize + HoneyBee: + description: A representation of a honey bee + allOf: + - $ref: "#/components/schemas/Pet" + - type: object + properties: + honeyPerDay: + type: number + description: Average amount of honey produced per day in ounces + example: 3.14 + multipleOf: .01 + default: 0 + required: + - honeyPerDay + Id: + type: integer + format: int64 + readOnly: true + Order: + type: object + properties: + id: + description: Order ID + allOf: + - $ref: "#/components/schemas/Id" + petId: + description: Pet ID + allOf: + - $ref: "#/components/schemas/Id" + quantity: + type: integer + format: int32 + minimum: 1 + default: 1 + shipDate: + description: Estimated ship date + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + description: Indicates whenever order was completed or not + type: boolean + default: false + readOnly: true + requestId: + description: Unique Request Id + type: string + writeOnly: true + xml: + name: Order + Pet: + type: object + description: A pet + required: + - name + - photoUrls + - tags + discriminator: + propertyName: petType + mapping: + cat: "#/components/schemas/Cat" + dog: "#/components/schemas/Dog" + bee: "#/components/schemas/HoneyBee" + properties: + id: + externalDocs: + description: "Find more info here" + url: "https://example.com" + description: Pet ID + allOf: + - $ref: "#/components/schemas/Id" + category: + description: Categories this pet belongs to + allOf: + - $ref: "#/components/schemas/Category" + name: + description: The name given to a pet + type: string + example: Guru + photoUrls: + description: The list of URL to a cute photos featuring pet + type: array + maxItems: 20 + xml: + name: photoUrl + wrapped: true + items: + type: string + format: url + friend: + allOf: + - $ref: "#/components/schemas/Pet" + tags: + description: Tags attached to the pet + type: array + minItems: 1 + xml: + name: tag + wrapped: true + items: + $ref: "#/components/schemas/Tag" + status: + type: string + description: Pet status in the store + enum: + - available + - pending + - sold + x-enumDescriptions: + available: When the pet is available + pending: When the pet is being sold + sold: | + When the pet has been sold. + + These descriptions can contain line + + breaks and also [links](https://docusaurus.io/) + petType: + description: Type of a pet + type: string + oneOf: + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/HoneyBee" + xml: + name: Pet + Tag: + type: object + description: A pet tag + properties: + id: + description: Tag ID + allOf: + - $ref: "#/components/schemas/Id" + name: + description: Tag name + type: string + minLength: 1 + xml: + name: Tag + User: + type: object + properties: + id: + $ref: "#/components/schemas/Id" + pet: + oneOf: + - $ref: "#/components/schemas/Pet" + - $ref: "#/components/schemas/Tag" + username: + description: User supplied username + type: string + minLength: 4 + example: John78 + firstName: + description: User first name + type: string + minLength: 1 + example: John + lastName: + description: User last name + type: string + minLength: 1 + example: Smith + email: + description: User email address + type: string + format: email + example: john.smith@example.com + password: + type: string + description: >- + User password, MUST contain a mix of upper and lower case letters, + as well as digits + format: password + minLength: 8 + pattern: "/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/" + example: drowssaP123 + phone: + description: User phone number in international format + type: string + pattern: '/^\+(?:[0-9]-?){6,14}[0-9]$/' + example: +1-202-555-0192 + userStatus: + description: User status + type: integer + format: int32 + xml: + name: User + requestBodies: + Pet: + content: + application/json: + schema: + allOf: + - description: My Pet + title: Pettie + - $ref: "#/components/schemas/Pet" + example: + summary: A great example! + category: + name: Great Dane + sub: + prop1: Just a test property + name: Pepper + photoUrls: + - https://assets.orvis.com/is/image/orvisprd/great-dane + tags: + - name: Great Danes + status: pending + petType: + huntingSkill: lazy + application/xml: + schema: + type: "object" + properties: + name: + type: string + description: hooray + description: Pet object that needs to be added to the store + required: true + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/User" + description: List of user object + required: true + securitySchemes: + petstore_auth: + description: | + Get access to data while protecting your account credentials. + OAuth2 is also a safer and more secure way to give you access. + type: oauth2 + flows: + implicit: + authorizationUrl: "http://petstore.swagger.io/api/oauth/dialog" + scopes: + "write:pets": modify pets in your account + "read:pets": read your pets + api_key: + description: > + For this sample, you can use the api key `special-key` to test the + authorization filters. + type: apiKey + name: api_key + in: header + BasicAuth: + type: http + scheme: basic + BearerAuth: + type: http + scheme: bearer + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + OpenID: + type: openIdConnect + openIdConnectUrl: https://example.com/.well-known/openid-configuration + OAuth2: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: https://example.com/oauth/authorize + tokenUrl: https://example.com/oauth/token + scopes: + read: Grants read access + write: Grants write access + admin: Grants access to admin operations +x-webhooks: + newPet: + post: + summary: New pet + description: Information about a new pet in the systems + operationId: newPet + tags: + - pet + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + responses: + "200": + description: Return a 200 status to indicate that the data was received successfully diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/petstoreCircular.test.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/petstoreCircular.test.ts new file mode 100644 index 000000000..10ccc0d74 --- /dev/null +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/petstoreCircular.test.ts @@ -0,0 +1,25 @@ +/* ============================================================================ + * 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 path from "path"; + +// eslint-disable-next-line import/no-extraneous-dependencies +import { posixPath } from "@docusaurus/utils"; + +import { loadAndResolveSpec } from "./utils/loadAndResolveSpec"; + +describe("petstore circular refs", () => { + it("labels friend property as circular", async () => { + const file = posixPath( + path.join(__dirname, "__fixtures__/examples/petstore.yaml") + ); + const spec: any = await loadAndResolveSpec(file); + expect(spec.components.schemas.Pet.properties.friend.allOf[0]).toBe( + "circular(Pet)" + ); + }); +}); From 19fec96b37b0c3424ce27d4d3a6572d9a682231b Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Thu, 17 Jul 2025 09:11:08 -0500 Subject: [PATCH 11/15] Fix circular schema labels for primitive refs --- .../src/markdown/schema.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts index 185019c17..e6161dc07 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts @@ -7,7 +7,10 @@ import { SchemaObject } from "../openapi/types"; -function prettyName(schema: SchemaObject, circular?: boolean) { +function prettyName(schema: SchemaObject | string, circular?: boolean) { + if (typeof schema === "string") { + return schema.startsWith("circular(") ? schema : ""; + } if (schema.format) { if (schema.type) { return `${schema.type}<${schema.format}>`; @@ -51,10 +54,10 @@ function prettyName(schema: SchemaObject, circular?: boolean) { } export function getSchemaName( - schema: SchemaObject, + schema: SchemaObject | string, circular?: boolean ): string { - if (schema.items) { + if (typeof schema !== "string" && schema.items) { return prettyName(schema.items, circular) + "[]"; } From 42e6a4faca6d51e1896b8fb097b7598f2150356b Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Thu, 17 Jul 2025 09:43:24 -0500 Subject: [PATCH 12/15] Handle multiple circular refs in allOf --- .../__snapshots__/createSchema.test.ts.snap | 55 ------------------- .../src/markdown/createSchema.ts | 32 ++++------- 2 files changed, 10 insertions(+), 77 deletions(-) diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/__snapshots__/createSchema.test.ts.snap b/packages/docusaurus-plugin-openapi-docs/src/markdown/__snapshots__/createSchema.test.ts.snap index 3d4d9548d..7b2a8b125 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/__snapshots__/createSchema.test.ts.snap +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/__snapshots__/createSchema.test.ts.snap @@ -364,24 +364,6 @@ Array [ qualifierMessage={undefined} schema={{ type: \\"string\\" }} ></SchemaItem>; -", - "<SchemaItem - collapsible={false} - name={\\"parentProp1\\"} - required={false} - schemaName={\\"string\\"} - qualifierMessage={undefined} - schema={{ type: \\"string\\" }} -></SchemaItem>; -", - "<SchemaItem - collapsible={false} - name={\\"parentProp2\\"} - required={false} - schemaName={\\"string\\"} - qualifierMessage={undefined} - schema={{ type: \\"string\\" }} -></SchemaItem>; ", ] `; @@ -729,15 +711,6 @@ Array [ qualifierMessage={undefined} schema={{ type: \\"string\\" }} ></SchemaItem>; -", - "<SchemaItem - collapsible={false} - name={\\"type\\"} - required={false} - schemaName={\\"string\\"} - qualifierMessage={undefined} - schema={{ type: \\"string\\" }} -></SchemaItem>; ", ] `; @@ -824,34 +797,6 @@ Array [ qualifierMessage={undefined} schema={{ type: \\"string\\" }} ></SchemaItem>; -", - "<div className={\\"openapi-discriminator__item openapi-schema__list-item\\"}> - <div> - <span className={\\"openapi-schema__container\\"}> - <strong - className={\\"openapi-discriminator__name openapi-schema__property\\"} - > - type - </strong> - <span className={\\"openapi-schema__name\\"}>string</span> - </span> - <div style={{ paddingLeft: \\"1rem\\" }}> - **Possible values:** [\`typeA\`, \`typeB\`] - </div> - <DiscriminatorTabs className={\\"openapi-tabs__discriminator\\"}> - <TabItem label={\\"typeA\\"} value={\\"0-item-discriminator\\"}> - <div style={{ marginTop: \\".5rem\\", marginBottom: \\".5rem\\" }}> - #/definitions/TypeA - </div> - </TabItem> - <TabItem label={\\"typeB\\"} value={\\"1-item-discriminator\\"}> - <div style={{ marginTop: \\".5rem\\", marginBottom: \\".5rem\\" }}> - #/definitions/TypeB - </div> - </TabItem> - </DiscriminatorTabs> - </div> -</div>; ", ] `; diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts index 952864f47..c1e217536 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts @@ -783,11 +783,11 @@ export function createNodes( } if (schema.allOf !== undefined) { - const circularIndex = schema.allOf.findIndex((item: any) => { + const circularItems = schema.allOf.filter((item: any) => { return typeof item === "string" && item.includes("circular"); }); - if (circularIndex !== -1) { + for (const label of circularItems) { nodes.push( create("div", { style: { @@ -795,29 +795,17 @@ export function createNodes( marginBottom: ".5rem", marginLeft: "1rem", }, - children: createDescription(schema.allOf[circularIndex] as string), + children: createDescription(label as string), }) ); + } - const rest = schema.allOf - .slice(0, circularIndex) - .concat(schema.allOf.slice(circularIndex + 1)); - - if (rest.length) { - const mergedSchemas = mergeAllOf({ allOf: rest } as SchemaObject); - if ( - mergedSchemas.oneOf !== undefined || - mergedSchemas.anyOf !== undefined - ) { - nodes.push(createAnyOneOf(mergedSchemas)); - } - - if (mergedSchemas.properties !== undefined) { - nodes.push(createProperties(mergedSchemas)); - } - } - } else { - const mergedSchemas = mergeAllOf(schema) as SchemaObject; + const rest = schema.allOf.filter((item: any) => { + return !(typeof item === "string" && item.includes("circular")); + }); + + if (rest.length) { + const mergedSchemas = mergeAllOf({ allOf: rest } as SchemaObject); if ( mergedSchemas.oneOf !== undefined || From 251e37755bb275f64602ed0c27c5d5460584acf9 Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:20:06 -0500 Subject: [PATCH 13/15] Render circular refs using SchemaItem --- .../src/markdown/createSchema.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts index c1e217536..41f0ef850 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts @@ -789,13 +789,13 @@ export function createNodes( for (const label of circularItems) { nodes.push( - create("div", { - style: { - marginTop: ".5rem", - marginBottom: ".5rem", - marginLeft: "1rem", - }, - children: createDescription(label as string), + create("SchemaItem", { + collapsible: false, + name: "", + required: false, + schemaName: label, + qualifierMessage: undefined, + schema: {}, }) ); } From ec2e2852d291f442bd5eab32ff819d349c01833d Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:57:10 -0500 Subject: [PATCH 14/15] feat: support circular labels from x-circular-ref --- .../src/markdown/createSchema.ts | 39 ++++++++++++++++--- .../src/markdown/schema.ts | 3 ++ .../src/theme/SchemaItem/index.tsx | 14 +++++-- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts index 41f0ef850..ef6cef99b 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts @@ -784,16 +784,28 @@ export function createNodes( if (schema.allOf !== undefined) { const circularItems = schema.allOf.filter((item: any) => { - return typeof item === "string" && item.includes("circular"); + if (typeof item === "string" && item.includes("circular")) { + return true; + } + if (typeof item === "object" && item["x-circular-ref"]) { + return true; + } + return false; }); for (const label of circularItems) { + const schemaName = + typeof label === "string" + ? label + : label.title + ? `circular(${label.title})` + : "circular()"; nodes.push( create("SchemaItem", { collapsible: false, name: "", required: false, - schemaName: label, + schemaName, qualifierMessage: undefined, schema: {}, }) @@ -801,7 +813,13 @@ export function createNodes( } const rest = schema.allOf.filter((item: any) => { - return !(typeof item === "string" && item.includes("circular")); + if (typeof item === "string" && item.includes("circular")) { + return false; + } + if (typeof item === "object" && item["x-circular-ref"]) { + return false; + } + return true; }); if (rest.length) { @@ -828,14 +846,25 @@ export function createNodes( if (schema.type !== undefined) { if (schema.allOf) { //handle circular result in allOf - if (schema.allOf.length && typeof schema.allOf[0] === "string") { + const first: any = schema.allOf[0]; + if ( + schema.allOf.length && + ((typeof first === "string" && first.includes("circular")) || + (typeof first === "object" && first["x-circular-ref"])) + ) { + const label = + typeof first === "string" + ? first + : first.title + ? `circular(${first.title})` + : "circular()"; return create("div", { style: { marginTop: ".5rem", marginBottom: ".5rem", marginLeft: "1rem", }, - children: createDescription(schema.allOf[0]), + children: createDescription(label), }); } } diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts index e6161dc07..d3576e4ce 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts @@ -11,6 +11,9 @@ function prettyName(schema: SchemaObject | string, circular?: boolean) { if (typeof schema === "string") { return schema.startsWith("circular(") ? schema : ""; } + if (schema["x-circular-ref"]) { + return schema.title ? `circular(${schema.title})` : "circular()"; + } if (schema.format) { if (schema.type) { return `${schema.type}<${schema.format}>`; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx index 288223466..5f565d99b 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/SchemaItem/index.tsx @@ -97,10 +97,16 @@ export default function SchemaItem(props: Props) { ? schemaName.match(/^circular\\(([^)]*)\\)/) : null; - const renderCircular = guard(circularMatch, () => ( - <span className="openapi-schema__circular"> - {circularMatch ? `circular(${circularMatch[1]})` : "circular"} - </span> + const circularTitle = schema?.["x-circular-ref"] + ? schema.title + ? `circular(${schema.title})` + : "circular" + : circularMatch + ? `circular(${circularMatch[1]})` + : null; + + const renderCircular = guard(circularTitle, () => ( + <span className="openapi-schema__circular">{circularTitle}</span> )); const renderEnumDescriptions = guard( From 063ec0d6f45efd75f32c4b4fa16675e7bba1e553 Mon Sep 17 00:00:00 2001 From: Steven Serrata <9343811+sserrata@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:50:20 -0500 Subject: [PATCH 15/15] Allow circular flag in schema types --- packages/docusaurus-plugin-openapi-docs/src/openapi/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/types.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/types.ts index b623a6fb1..8bf328e02 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/types.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/types.ts @@ -359,6 +359,7 @@ export type SchemaObject = Omit< example?: any; deprecated?: boolean; "x-tags"?: string[]; + "x-circular-ref"?: boolean; "x-enumDescriptions"?: Record<string, string>; }; @@ -392,6 +393,7 @@ export type SchemaObjectWithRef = Omit< externalDocs?: ExternalDocumentationObject; example?: any; deprecated?: boolean; + "x-circular-ref"?: boolean; }; export interface DiscriminatorObject {