From d67699a1f1c97f233d3eacaf14386bfea9f4897c Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Fri, 18 Apr 2025 08:35:44 +0200 Subject: [PATCH] Add OpenAPI Webhook block (#3160) --- .changeset/light-mirrors-give.md | 6 + bun.lock | 26 ++-- package.json | 2 +- packages/cache-tags/package.json | 2 +- .../src/components/DocumentView/Block.tsx | 5 +- .../DocumentView/OpenAPI/OpenAPIWebhook.tsx | 58 +++++++++ .../DocumentView/OpenAPI/context.tsx | 8 +- .../components/DocumentView/OpenAPI/index.ts | 1 + .../components/DocumentView/OpenAPI/style.css | 3 +- packages/gitbook/src/lib/openapi/fetch.ts | 3 +- .../lib/openapi/resolveOpenAPIWebhookBlock.ts | 61 +++++++++ packages/gitbook/src/lib/openapi/types.ts | 6 + .../react-openapi/src/OpenAPICodeSample.tsx | 8 +- .../src/OpenAPICodeSampleInteractive.tsx | 14 ++- packages/react-openapi/src/OpenAPIExample.tsx | 78 ------------ ...onseMediaType.tsx => OpenAPIMediaType.tsx} | 76 +++++------ .../react-openapi/src/OpenAPIOperation.tsx | 100 +-------------- .../react-openapi/src/OpenAPIRequestBody.tsx | 13 +- .../src/OpenAPIResponseExample.tsx | 55 ++------ packages/react-openapi/src/OpenAPISpec.tsx | 11 +- packages/react-openapi/src/OpenAPIWebhook.tsx | 30 +++++ .../src/OpenAPIWebhookExample.tsx | 59 +++++++++ .../src/common/OpenAPIColumnSpec.tsx | 30 +++++ .../common/OpenAPIOperationDescription.tsx | 31 +++++ .../src/common/OpenAPIStability.tsx | 23 ++++ .../src/common/OpenAPISummary.tsx | 44 +++++++ packages/react-openapi/src/index.ts | 4 +- .../src/resolveOpenAPIWebhook.ts | 99 +++++++++++++++ .../src/schemas/OpenAPISchemas.tsx | 3 +- packages/react-openapi/src/types.ts | 11 ++ packages/react-openapi/src/util/example.tsx | 119 ++++++++++++++++++ 31 files changed, 684 insertions(+), 305 deletions(-) create mode 100644 .changeset/light-mirrors-give.md create mode 100644 packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx create mode 100644 packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts rename packages/react-openapi/src/{OpenAPIResponseMediaType.tsx => OpenAPIMediaType.tsx} (55%) create mode 100644 packages/react-openapi/src/OpenAPIWebhook.tsx create mode 100644 packages/react-openapi/src/OpenAPIWebhookExample.tsx create mode 100644 packages/react-openapi/src/common/OpenAPIColumnSpec.tsx create mode 100644 packages/react-openapi/src/common/OpenAPIOperationDescription.tsx create mode 100644 packages/react-openapi/src/common/OpenAPIStability.tsx create mode 100644 packages/react-openapi/src/common/OpenAPISummary.tsx create mode 100644 packages/react-openapi/src/resolveOpenAPIWebhook.ts create mode 100644 packages/react-openapi/src/util/example.tsx diff --git a/.changeset/light-mirrors-give.md b/.changeset/light-mirrors-give.md new file mode 100644 index 000000000..5850a5024 --- /dev/null +++ b/.changeset/light-mirrors-give.md @@ -0,0 +1,6 @@ +--- +'@gitbook/react-openapi': minor +'gitbook': minor +--- + +Add OpenAPI Webhook block diff --git a/bun.lock b/bun.lock index 98d42deb7..323725884 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ "name": "@gitbook/cache-tags", "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.109.0", + "@gitbook/api": "^0.111.0", "assert-never": "^1.2.1", }, "devDependencies": { @@ -259,7 +259,7 @@ }, "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "0.109.0", + "@gitbook/api": "0.111.0", "react": "18.3.1", "react-dom": "18.3.1", }, @@ -628,7 +628,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.109.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-nzhxmiNR2MMHh+HoU7fFdOJ+u4BFyXjtsVquQf3VhZMjeI71yeLx5JEbjkOcijVd/GMAtvpYlYR2qcPqzq27gw=="], + "@gitbook/api": ["@gitbook/api@0.111.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-E5Pk28kPD4p6XNWdwFM9pgDijdByseIZQqcFK+/hoW5tEZa5Yw/plRKJyN1hmwfPL6SKq6Maf0fbIzTQiVXyQQ=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], @@ -4032,7 +4032,7 @@ "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "gitbook-v2/next": ["next@15.3.1-canary.7", "", { "dependencies": { "@next/env": "15.3.1-canary.7", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.1-canary.7", "@next/swc-darwin-x64": "15.3.1-canary.7", "@next/swc-linux-arm64-gnu": "15.3.1-canary.7", "@next/swc-linux-arm64-musl": "15.3.1-canary.7", "@next/swc-linux-x64-gnu": "15.3.1-canary.7", "@next/swc-linux-x64-musl": "15.3.1-canary.7", "@next/swc-win32-arm64-msvc": "15.3.1-canary.7", "@next/swc-win32-x64-msvc": "15.3.1-canary.7", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RblvC7A7k8cKpSa6JSCbaatb6JAncv2niUSH7TqXt6xHUoItnxa3Fu7xkducFJQdcW2Hv2h0+NBu6kaKf0S4zw=="], + "gitbook-v2/next": ["next@15.3.1-canary.8", "", { "dependencies": { "@next/env": "15.3.1-canary.8", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.1-canary.8", "@next/swc-darwin-x64": "15.3.1-canary.8", "@next/swc-linux-arm64-gnu": "15.3.1-canary.8", "@next/swc-linux-arm64-musl": "15.3.1-canary.8", "@next/swc-linux-x64-gnu": "15.3.1-canary.8", "@next/swc-linux-x64-musl": "15.3.1-canary.8", "@next/swc-win32-arm64-msvc": "15.3.1-canary.8", "@next/swc-win32-x64-msvc": "15.3.1-canary.8", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Of5a3BTTIl/iUvL2a9Jh7m7G/H8z4Pj5Vs54CLvcdadokxSNgLOpjzbDgFR8J4PawLx6+MOMy19m9Cvr6EPGug=="], "global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="], @@ -4908,23 +4908,23 @@ "gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "gitbook-v2/next/@next/env": ["@next/env@15.3.1-canary.7", "", {}, "sha512-QSzAlzxaGrQYj3Nzgxd35WM6pmxmNk9lOCn0nsNLPidvc3H6wj/fV3Pl8/tVCYQP61luGuA0Ib0VwZc8ja119w=="], + "gitbook-v2/next/@next/env": ["@next/env@15.3.1-canary.8", "", {}, "sha512-ShZTo0hNhbTRrp7k6oUDSck4Xx4hhfSeLBp35jvGaw1QMZzWYr5v/oc0kEt0bfMdl+833flwKV7kFR3BnrULfg=="], - "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.1-canary.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fs7JYU0MKtpgsDBIvbT/wn1MwKP8C+bqcb6IjWnDtVcciFKzYDs/tPCBW6CtYg1wpHqm9horZDA1ytb/sAghxA=="], + "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.1-canary.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZaDynM+pbrnLLlBAxH/CDGp9KN79OFrLcT1ejlWyo86V3SS9Gyqr4nmTuvTevByTTpr1VHReQel8Zbq0Pttu7Q=="], - "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.1-canary.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Efl6es/IEXJf5groF78fc1PrTEwK9bOyi6fRjp1bsa/AWOHp8rTc3l0T6EeY5T0SWeYilisZZa9rax7nTgw4bw=="], + "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.1-canary.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-BsMR8WqeCDAX8C9RYAO8TI4ttpuqKk2oYMb1+bCrOYi857SMfB4vWD4PWmMvj7mwFXGqrxly4W6CBQlD66A+fg=="], - "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.1-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-IS71I8z2wlecRZRHXqWTtxVozZt1h9IH/2MX1P/WkCjQH98acFmCkw0jo/y70mrZ4lw6tRGTNkSBMun+5P5P+w=="], + "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.1-canary.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-xL+K2SW+/46j/KnKNf1gizM1bxwcEaE56eCEG9RPoYS/lfxHLuHcR9O2MlcPI90g/rIN2HXmHeuKRbXbnVy15g=="], - "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.1-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-uJ4env9fZxIFM+aRBCjNeG91HHZ2cRXiF25xJqGmCC1Y5jT5oYAB/tIBCFRSj6NRzdt4pZAamCMRS41WzzVVUQ=="], + "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.1-canary.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-bzXlCUXkjIRsMTb6rr7OsWEQmdO2rZgKijnMGBJzEpa9ROq95VJTF+rdyrLmHuc4fPsAGDn/C18V3E1YOi4ipQ=="], - "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.1-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-/+efTL2TxoSAYCxdf+g6WvDKIJDoaETvF3I+lxpixhctfrIwpr2qnG0e6u2M55k1iYiiyFIOA63fpIsGnIMJZA=="], + "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.1-canary.8", "", { "os": "linux", "cpu": "x64" }, "sha512-snbPQ9th7eoYYMZpNGhEvX4EbqGjjkiXzTEm2F/oDadlZR2wI6egtfQHiZqeczL7XwG3M66hBJ1/U20wdTDtvA=="], - "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.1-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-Hb+agfULOjEhJAmq/r9OTVAo8KxFlnuCT7BEUbSuVP/Gxn883al6juSG7ZZfk7P4a+ZU6iVYyHKIFAKCarZmUA=="], + "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.1-canary.8", "", { "os": "linux", "cpu": "x64" }, "sha512-5hmcaGazc3w6rg/gbQOrmuw0kKShf4Egs0JlUPop/ZiRDfZO5KlyrGY0hUD+2pLG7Yx3B/fTbj86cHKfQZDMBw=="], - "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.1-canary.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-gNd5Vq7k9xIEQj85JoavHGRsyvu1zKOjVoPUYtsZRxXw6kcBg84x2g8NeT/3SmwH1PiFU/sjQXtRzr2Kuc2j3A=="], + "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.1-canary.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-t6uwWC/UbQ8CQGyBVbEmpcJC41yAvz2eZZGec9EoO0ZyQ/fbvjOkqfPnf+WcjWifWNeuiWogjN1UBg7YK9IaVA=="], - "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.1-canary.7", "", { "os": "win32", "cpu": "x64" }, "sha512-D5i+KinvNNmspXfgIvEZ1RFOKVhOgdtoSC7/xEUm55hEkcWFVHOtVVPvgV7WUeo1elNxbgxytzmyKbLaifP03g=="], + "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.1-canary.8", "", { "os": "win32", "cpu": "x64" }, "sha512-93gNqVYwlr9bz6Pm5dQnqjpuOEQyvDre0+H39UqS7h2KuFh3sf2MejhAEr2uFEux4mb7TN0PlohaihO7YxJ3fw=="], "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], diff --git a/package.json b/package.json index d93f0167e..736eabe41 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@codemirror/state": "6.4.1", "react": "18.3.1", "react-dom": "18.3.1", - "@gitbook/api": "0.109.0" + "@gitbook/api": "0.111.0" }, "private": true, "scripts": { diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json index d9baf2d71..3d25aa8f4 100644 --- a/packages/cache-tags/package.json +++ b/packages/cache-tags/package.json @@ -10,7 +10,7 @@ }, "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.109.0", + "@gitbook/api": "^0.111.0", "assert-never": "^1.2.1" }, "devDependencies": { diff --git a/packages/gitbook/src/components/DocumentView/Block.tsx b/packages/gitbook/src/components/DocumentView/Block.tsx index f3acafce0..60998b50b 100644 --- a/packages/gitbook/src/components/DocumentView/Block.tsx +++ b/packages/gitbook/src/components/DocumentView/Block.tsx @@ -25,7 +25,7 @@ import { IntegrationBlock } from './Integration'; import { List } from './List'; import { ListItem } from './ListItem'; import { BlockMath } from './Math'; -import { OpenAPIOperation, OpenAPISchemas } from './OpenAPI'; +import { OpenAPIOperation, OpenAPISchemas, OpenAPIWebhook } from './OpenAPI'; import { Paragraph } from './Paragraph'; import { Quote } from './Quote'; import { ReusableContent } from './ReusableContent'; @@ -85,6 +85,8 @@ export function Block(props: BlockProps) { return ; case 'openapi-schemas': return ; + case 'openapi-webhook': + return ; case 'embed': return ; case 'blockquote': @@ -159,6 +161,7 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue } case 'swagger': case 'openapi-operation': case 'openapi-schemas': + case 'openapi-webhook': case 'math': case 'divider': case 'content-ref': diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx new file mode 100644 index 000000000..087f1ae94 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx @@ -0,0 +1,58 @@ +import { OpenAPIWebhook as BaseOpenAPIWebhook } from '@gitbook/react-openapi'; + +import { resolveOpenAPIWebhookBlock } from '@/lib/openapi/resolveOpenAPIWebhookBlock'; +import { tcls } from '@/lib/tailwind'; + +import type { BlockProps } from '../Block'; + +import './scalar.css'; +import './style.css'; +import type { OpenAPIWebhookBlock } from '@/lib/openapi/types'; +import { getOpenAPIContext } from './context'; + +/** + * Render an openapi block or an openapi-webhook block. + */ +export async function OpenAPIWebhook(props: BlockProps) { + const { style } = props; + return ( +
+ +
+ ); +} + +async function OpenAPIWebhookBody(props: BlockProps) { + const { block, context } = props; + + if (!context.contentContext) { + return null; + } + + const { data, specUrl, error } = await resolveOpenAPIWebhookBlock({ + block, + context: context.contentContext, + }); + + if (error) { + return ( +
+

+ Error with {specUrl}: {error.message} +

+
+ ); + } + + if (!data || !specUrl) { + return null; + } + + return ( + + ); +} diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx index a40411fa6..d470b14ca 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx @@ -11,13 +11,17 @@ import { Heading } from '../Heading'; import './scalar.css'; import './style.css'; -import type { AnyOpenAPIOperationsBlock, OpenAPISchemasBlock } from '@/lib/openapi/types'; +import type { + AnyOpenAPIOperationsBlock, + OpenAPISchemasBlock, + OpenAPIWebhookBlock, +} from '@/lib/openapi/types'; /** * Get the OpenAPI context to render a block. */ export function getOpenAPIContext(args: { - props: BlockProps; + props: BlockProps; specUrl: string; }): OpenAPIContext { const { props, specUrl } = args; diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/index.ts b/packages/gitbook/src/components/DocumentView/OpenAPI/index.ts index 25daa70d3..a0b24ee1a 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/index.ts +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/index.ts @@ -1,2 +1,3 @@ export * from './OpenAPIOperation'; export * from './OpenAPISchemas'; +export * from './OpenAPIWebhook'; diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 8daebeba3..ad409378e 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -1,6 +1,7 @@ /* Layout Components */ .openapi-operation, -.openapi-schemas { +.openapi-schemas, +.openapi-webhook { @apply flex-1 flex flex-col gap-8 mb-14 min-w-0; } diff --git a/packages/gitbook/src/lib/openapi/fetch.ts b/packages/gitbook/src/lib/openapi/fetch.ts index 41f2b442e..d1f8c03ce 100644 --- a/packages/gitbook/src/lib/openapi/fetch.ts +++ b/packages/gitbook/src/lib/openapi/fetch.ts @@ -5,6 +5,7 @@ import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cac import type { AnyOpenAPIOperationsBlock, OpenAPISchemasBlock, + OpenAPIWebhookBlock, ResolveOpenAPIBlockArgs, } from '@/lib/openapi/types'; import { getCloudflareRequestGlobal } from '@v2/lib/data/cloudflare'; @@ -16,7 +17,7 @@ import { isV2 } from '../v2'; import { enrichFilesystem } from './enrich'; import type { FetchOpenAPIFilesystemResult } from './types'; -type AnyOpenAPIBlock = AnyOpenAPIOperationsBlock | OpenAPISchemasBlock; +type AnyOpenAPIBlock = AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock; /** * Fetch OpenAPI block. diff --git a/packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts b/packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts new file mode 100644 index 000000000..817e080c3 --- /dev/null +++ b/packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts @@ -0,0 +1,61 @@ +import { fetchOpenAPIFilesystem } from '@/lib/openapi/fetch'; +import { OpenAPIParseError } from '@gitbook/openapi-parser'; +import { type OpenAPIWebhookData, resolveOpenAPIWebhook } from '@gitbook/react-openapi'; +import type { + OpenAPIWebhookBlock, + ResolveOpenAPIBlockArgs, + ResolveOpenAPIBlockResult, +} from './types'; + +type ResolveOpenAPIWebhookBlockResult = ResolveOpenAPIBlockResult; + +const weakmap = new WeakMap>(); + +/** + * Cache the result of resolving an OpenAPI block. + * It is important because the resolve is called in sections and in the block itself. + */ +export function resolveOpenAPIWebhookBlock( + args: ResolveOpenAPIBlockArgs +): Promise { + if (weakmap.has(args.block)) { + return weakmap.get(args.block)!; + } + + const result = baseResolveOpenAPIWebhookBlock(args); + weakmap.set(args.block, result); + return result; +} + +/** + * Resolve OpenAPI webhook block. + */ +async function baseResolveOpenAPIWebhookBlock( + args: ResolveOpenAPIBlockArgs +): Promise { + const { context, block } = args; + if (!block.data.name || !block.data.method) { + return { data: null, specUrl: null }; + } + + try { + const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context }); + + if (!filesystem) { + return { data: null, specUrl: null }; + } + + const data = await resolveOpenAPIWebhook(filesystem, { + name: block.data.name, + method: block.data.method, + }); + + return { data, specUrl }; + } catch (error) { + if (error instanceof OpenAPIParseError) { + return { error }; + } + + throw error; + } +} diff --git a/packages/gitbook/src/lib/openapi/types.ts b/packages/gitbook/src/lib/openapi/types.ts index 99810d25b..af6997c3f 100644 --- a/packages/gitbook/src/lib/openapi/types.ts +++ b/packages/gitbook/src/lib/openapi/types.ts @@ -2,6 +2,7 @@ import type { DocumentBlockOpenAPI, DocumentBlockOpenAPIOperation, DocumentBlockOpenAPISchemas, + DocumentBlockOpenAPIWebhook, } from '@gitbook/api'; import type { Filesystem, OpenAPIParseError, OpenAPIV3xDocument } from '@gitbook/openapi-parser'; import type { GitBookAnyContext } from '@v2/lib/context'; @@ -16,6 +17,11 @@ export type AnyOpenAPIOperationsBlock = DocumentBlockOpenAPI | DocumentBlockOpen */ export type OpenAPISchemasBlock = DocumentBlockOpenAPISchemas; +/** + * Type for OpenAPI Webhook block + */ +export type OpenAPIWebhookBlock = DocumentBlockOpenAPIWebhook; + /** * Arguments for resolving OpenAPI block. */ diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 17cb66454..091731ec4 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -22,7 +22,7 @@ export function OpenAPICodeSample(props: { data: OpenAPIOperationData; context: OpenAPIContext; }) { - const { data } = props; + const { data, context } = props; // If code samples are disabled at operation level, we don't display the code samples. if (data.operation['x-codeSamples'] === false) { @@ -43,7 +43,9 @@ export function OpenAPICodeSample(props: { return null; } - return ; + return ( + + ); } /** @@ -146,6 +148,7 @@ function generateCodeSamples(props: { method={data.method} path={data.path} renderers={renderers} + blockKey={context.blockKey} /> ), footer: ( @@ -207,6 +210,7 @@ function OpenAPICodeSampleFooter(props: { path={data.path} renderers={renderers} selectIcon={context.icons.chevronDown} + blockKey={context.blockKey} /> ) : ( diff --git a/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx b/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx index 109df423c..b1c40b101 100644 --- a/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx +++ b/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx @@ -2,18 +2,20 @@ import clsx from 'clsx'; import type { MediaTypeRenderer } from './OpenAPICodeSample'; import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect'; +import { createStateKey } from './utils'; export function OpenAPIMediaTypeExamplesSelector(props: { method: string; path: string; renderers: MediaTypeRenderer[]; selectIcon?: React.ReactNode; + blockKey?: string; }) { - const { method, path, renderers, selectIcon } = props; + const { method, path, renderers, selectIcon, blockKey } = props; if (!renderers[0]) { throw new Error('No renderers provided'); } - const stateKey = `media-type-${method}-${path}`; + const stateKey = createStateKey('request-body-media-type', blockKey); const state = useSelectState(stateKey, renderers[0].mediaType); const selected = renderers.find((r) => r.mediaType === state.key) || renderers[0]; @@ -102,13 +104,17 @@ export function OpenAPIMediaTypeExamplesBody(props: { method: string; path: string; renderers: MediaTypeRenderer[]; + blockKey?: string; }) { - const { renderers, method, path } = props; + const { renderers, method, path, blockKey } = props; if (!renderers[0]) { throw new Error('No renderers provided'); } - const mediaTypeState = useSelectState(`media-type-${method}-${path}`, renderers[0].mediaType); + const mediaTypeState = useSelectState( + createStateKey('request-body-media-type', blockKey), + renderers[0].mediaType + ); const selected = renderers.find((r) => r.mediaType === mediaTypeState.key) ?? renderers[0]; if (selected.examples.length === 0) { return selected.element; diff --git a/packages/react-openapi/src/OpenAPIExample.tsx b/packages/react-openapi/src/OpenAPIExample.tsx index 56b5bfe15..f9a2b85a8 100644 --- a/packages/react-openapi/src/OpenAPIExample.tsx +++ b/packages/react-openapi/src/OpenAPIExample.tsx @@ -1,9 +1,7 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; -import { generateSchemaExample } from './generateSchemaExample'; import { json2xml } from './json2xml'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import type { OpenAPIContext } from './types'; -import { checkIsReference } from './utils'; /** * Display an example. @@ -51,79 +49,3 @@ export function OpenAPIEmptyExample() { ); } - -/** - * Generate an example from a reference object. - */ -export function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject { - return { summary: 'Unresolved reference', value: { $ref: ref.$ref } }; -} - -/** - * Get examples from a media type object. - */ -export function getExamplesFromMediaTypeObject(args: { - mediaType: string; - mediaTypeObject: OpenAPIV3.MediaTypeObject; -}): { key: string; example: OpenAPIV3.ExampleObject }[] { - const { mediaTypeObject, mediaType } = args; - if (mediaTypeObject.examples) { - return Object.entries(mediaTypeObject.examples).map(([key, example]) => { - return { - key, - example: checkIsReference(example) ? getExampleFromReference(example) : example, - }; - }); - } - - if (mediaTypeObject.example) { - return [{ key: 'default', example: { value: mediaTypeObject.example } }]; - } - - if (mediaTypeObject.schema) { - if (mediaType === 'application/xml') { - // @TODO normally we should use the name of the schema but we don't have it - // fix it when we got the reference name - const root = mediaTypeObject.schema.xml?.name ?? 'object'; - return [ - { - key: 'default', - example: { - value: { - [root]: generateSchemaExample(mediaTypeObject.schema, { - xml: mediaType === 'application/xml', - mode: 'read', - }), - }, - }, - }, - ]; - } - return [ - { - key: 'default', - example: { - value: generateSchemaExample(mediaTypeObject.schema, { - mode: 'read', - }), - }, - }, - ]; - } - return []; -} - -/** - * Get example from a schema object. - */ -export function getExampleFromSchema(args: { - schema: OpenAPIV3.SchemaObject; -}): OpenAPIV3.ExampleObject { - const { schema } = args; - - if (schema.example) { - return { value: schema.example }; - } - - return { value: generateSchemaExample(schema, { mode: 'read' }) }; -} diff --git a/packages/react-openapi/src/OpenAPIResponseMediaType.tsx b/packages/react-openapi/src/OpenAPIMediaType.tsx similarity index 55% rename from packages/react-openapi/src/OpenAPIResponseMediaType.tsx rename to packages/react-openapi/src/OpenAPIMediaType.tsx index b2ca31fcd..56f6c4740 100644 --- a/packages/react-openapi/src/OpenAPIResponseMediaType.tsx +++ b/packages/react-openapi/src/OpenAPIMediaType.tsx @@ -3,37 +3,30 @@ import type { Key } from 'react-aria'; import { OpenAPIEmptyExample } from './OpenAPIExample'; import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect'; import { StaticSection } from './StaticSection'; -import { createStateKey } from './utils'; -type OpenAPIResponseMediaTypeItem = OpenAPISelectItem & { +type OpenAPIMediaTypeItem = OpenAPISelectItem & { body: React.ReactNode; - examples?: OpenAPIResponseMediaTypeItem[]; + examples?: OpenAPIMediaTypeItem[]; }; /** * Get the state of the response examples select. */ -export function useResponseMediaTypesState( - blockKey: string | undefined, - initialKey: Key = 'default' -) { - return useSelectState(getResponseMediaTypeStateKey(blockKey), initialKey); +export function useMediaTypesState(stateKey: string | undefined, initialKey: Key = 'default') { + return useSelectState(stateKey, initialKey); } -function useResponseMediaTypeExamplesState( - blockKey: string | undefined, - initialKey: Key = 'default' -) { - return useSelectState(getResponseMediaTypeExamplesStateKey(blockKey), initialKey); +function useMediaTypeExamplesState(stateKey: string | undefined, initialKey: Key = 'default') { + return useSelectState(stateKey, initialKey); } -export function OpenAPIResponseMediaTypeContent(props: { - items: OpenAPIResponseMediaTypeItem[]; - blockKey?: string; +export function OpenAPIMediaTypeContent(props: { + items: OpenAPIMediaTypeItem[]; selectIcon?: React.ReactNode; + stateKey: string; }) { - const { blockKey, items, selectIcon } = props; - const state = useResponseMediaTypesState(blockKey, items[0]?.key); + const { stateKey, items, selectIcon } = props; + const state = useMediaTypesState(stateKey, items[0]?.key); const examples = items.find((item) => item.key === state.key)?.examples ?? []; @@ -45,28 +38,28 @@ export function OpenAPIResponseMediaTypeContent(props: { 1 || examples.length > 1 ? ( - ) : null } className="openapi-response-media-types-examples" > - + ); } -function OpenAPIResponseMediaTypeFooter(props: { - items: OpenAPIResponseMediaTypeItem[]; - examples?: OpenAPIResponseMediaTypeItem[]; - blockKey?: string; +function OpenAPIMediaTypeFooter(props: { + items: OpenAPIMediaTypeItem[]; + examples?: OpenAPIMediaTypeItem[]; selectIcon?: React.ReactNode; + stateKey: string; }) { - const { items, examples, blockKey, selectIcon } = props; + const { items, examples, stateKey, selectIcon } = props; return ( <> @@ -74,7 +67,7 @@ function OpenAPIResponseMediaTypeFooter(props: { {items.map((item) => ( @@ -89,7 +82,7 @@ function OpenAPIResponseMediaTypeFooter(props: { {examples.map((example) => ( @@ -103,18 +96,18 @@ function OpenAPIResponseMediaTypeFooter(props: { ); } -function OpenAPIResponseMediaTypeBody(props: { - items: OpenAPIResponseMediaTypeItem[]; - examples?: OpenAPIResponseMediaTypeItem[]; - blockKey?: string; +function OpenAPIMediaTypeBody(props: { + items: OpenAPIMediaTypeItem[]; + examples?: OpenAPIMediaTypeItem[]; + stateKey: string; }) { - const { blockKey, items, examples } = props; - const state = useResponseMediaTypesState(blockKey, items[0]?.key); + const { stateKey, items, examples } = props; + const state = useMediaTypesState(stateKey, items[0]?.key); const selectedItem = items.find((item) => item.key === state.key) ?? items[0]; - const exampleState = useResponseMediaTypeExamplesState( - blockKey, + const exampleState = useMediaTypeExamplesState( + `${stateKey}-examples`, selectedItem?.examples?.[0]?.key ); @@ -135,14 +128,3 @@ function OpenAPIResponseMediaTypeBody(props: { return selectedItem.body; } - -/** - * Return the state key for the response media types. - */ -function getResponseMediaTypeStateKey(blockKey: string | undefined) { - return createStateKey('response-media-types', blockKey); -} - -function getResponseMediaTypeExamplesStateKey(blockKey: string | undefined) { - return createStateKey('response-media-types-examples', blockKey); -} diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index f58375b1c..32418c982 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -1,18 +1,9 @@ import clsx from 'clsx'; - -import type { - OpenAPICustomOperationProperties, - OpenAPIStability, - OpenAPIV3, -} from '@gitbook/openapi-parser'; -import { Markdown } from './Markdown'; import { OpenAPICodeSample } from './OpenAPICodeSample'; -import { OpenAPIPath } from './OpenAPIPath'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; -import { OpenAPISpec } from './OpenAPISpec'; -import { getOpenAPIClientContext } from './context'; +import { OpenAPIColumnSpec } from './common/OpenAPIColumnSpec'; +import { OpenAPISummary } from './common/OpenAPISummary'; import type { OpenAPIContext, OpenAPIOperationData } from './types'; -import { resolveDescription } from './utils'; /** * Display an interactive OpenAPI operation. @@ -23,46 +14,12 @@ export function OpenAPIOperation(props: { context: OpenAPIContext; }) { const { className, data, context } = props; - const { operation } = data; - - const clientContext = getOpenAPIClientContext(context); return (
-
- {(operation.deprecated || operation['x-stability']) && ( -
- {operation.deprecated && ( -
Deprecated
- )} - {operation['x-stability'] && ( - - )} -
- )} - {operation.summary - ? context.renderHeading({ - deprecated: operation.deprecated ?? false, - stability: operation['x-stability'], - title: operation.summary, - }) - : null} - -
+
-
- {operation['x-deprecated-sunset'] ? ( -
- This operation is deprecated and will be sunset on{' '} - - {operation['x-deprecated-sunset']} - - {'.'} -
- ) : null} - - -
+
@@ -73,52 +30,3 @@ export function OpenAPIOperation(props: {
); } - -function OpenAPIOperationDescription(props: { - operation: OpenAPIV3.OperationObject; - context: OpenAPIContext; -}) { - const { operation } = props; - if (operation['x-gitbook-description-document']) { - return ( -
- {props.context.renderDocument({ - document: operation['x-gitbook-description-document'], - })} -
- ); - } - - const description = resolveDescription(operation); - if (!description) { - return null; - } - - return ( -
- -
- ); -} - -const stabilityEnum = { - experimental: 'Experimental', - alpha: 'Alpha', - beta: 'Beta', -} as const; - -function OpenAPIOperationStability(props: { stability: OpenAPIStability }) { - const { stability } = props; - - const foundStability = stabilityEnum[stability]; - - if (!foundStability) { - return null; - } - - return ( -
- {foundStability} -
- ); -} diff --git a/packages/react-openapi/src/OpenAPIRequestBody.tsx b/packages/react-openapi/src/OpenAPIRequestBody.tsx index 7be68a3a5..1b23779c4 100644 --- a/packages/react-openapi/src/OpenAPIRequestBody.tsx +++ b/packages/react-openapi/src/OpenAPIRequestBody.tsx @@ -1,8 +1,8 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { InteractiveSection } from './InteractiveSection'; import { OpenAPIRootSchema } from './OpenAPISchemaServer'; -import type { OpenAPIClientContext, OpenAPIOperationData } from './types'; -import { checkIsReference } from './utils'; +import type { OpenAPIClientContext, OpenAPIOperationData, OpenAPIWebhookData } from './types'; +import { checkIsReference, createStateKey } from './utils'; /** * Display an interactive request body. @@ -10,20 +10,21 @@ import { checkIsReference } from './utils'; export function OpenAPIRequestBody(props: { requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject; context: OpenAPIClientContext; - data: OpenAPIOperationData; + data: OpenAPIOperationData | OpenAPIWebhookData; }) { const { requestBody, context, data } = props; - const { method, path } = data; if (checkIsReference(requestBody)) { return null; } + const header = 'name' in data ? 'Payload' : 'Body'; + return ( { diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx index 8640f568d..1ffc640b1 100644 --- a/packages/react-openapi/src/OpenAPIResponseExample.tsx +++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx @@ -1,22 +1,18 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { Markdown } from './Markdown'; -import { - OpenAPIEmptyExample, - OpenAPIExample, - getExampleFromReference, - getExamplesFromMediaTypeObject, -} from './OpenAPIExample'; +import { OpenAPIEmptyExample, OpenAPIExample } from './OpenAPIExample'; +import { OpenAPIMediaTypeContent } from './OpenAPIMediaType'; import { OpenAPIResponseExampleContent } from './OpenAPIResponseExampleContent'; -import { OpenAPIResponseMediaTypeContent } from './OpenAPIResponseMediaType'; -import type { OpenAPIContext, OpenAPIOperationData } from './types'; -import { getStatusCodeDefaultLabel } from './utils'; +import type { OpenAPIContext, OpenAPIOperationData, OpenAPIWebhookData } from './types'; +import { getExampleFromReference, getExamples } from './util/example'; +import { createStateKey, getStatusCodeDefaultLabel } from './utils'; import { checkIsReference, resolveDescription } from './utils'; /** * Display an example of the response content. */ export function OpenAPIResponseExample(props: { - data: OpenAPIOperationData; + data: OpenAPIOperationData | OpenAPIWebhookData; context: OpenAPIContext; }) { const { data, context } = props; @@ -127,45 +123,10 @@ function OpenAPIResponse(props: { }); return ( - ); } - -function getExamples(props: { - mediaTypeObject: OpenAPIV3.MediaTypeObject; - mediaType: string; - context: OpenAPIContext; -}) { - const { mediaTypeObject, mediaType } = props; - const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType }); - const syntax = getSyntaxFromMediaType(mediaType); - - return examples.map((example) => { - return { - key: example.key, - label: example.example.summary || example.key, - body: ( - - ), - }; - }); -} - -/** - * Get the syntax from a media type. - */ -function getSyntaxFromMediaType(mediaType: string): string { - if (mediaType.includes('json')) { - return 'json'; - } - - if (mediaType === 'application/xml') { - return 'xml'; - } - - return 'text'; -} diff --git a/packages/react-openapi/src/OpenAPISpec.tsx b/packages/react-openapi/src/OpenAPISpec.tsx index 4828288b4..e4988866a 100644 --- a/packages/react-openapi/src/OpenAPISpec.tsx +++ b/packages/react-openapi/src/OpenAPISpec.tsx @@ -5,17 +5,22 @@ import { OpenAPIResponses } from './OpenAPIResponses'; import { OpenAPISchemaProperties } from './OpenAPISchemaServer'; import { OpenAPISecurities } from './OpenAPISecurities'; import { StaticSection } from './StaticSection'; -import type { OpenAPIClientContext, OpenAPIOperationData } from './types'; +import type { OpenAPIClientContext, OpenAPIOperationData, OpenAPIWebhookData } from './types'; import { parameterToProperty } from './utils'; -export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAPIClientContext }) { +export function OpenAPISpec(props: { + data: OpenAPIOperationData | OpenAPIWebhookData; + context: OpenAPIClientContext; +}) { const { data, context } = props; - const { operation, securities } = data; + const { operation } = data; const parameters = operation.parameters ?? []; const parameterGroups = groupParameters(parameters); + const securities = 'securities' in data ? data.securities : []; + return ( <> {securities.length > 0 ? ( diff --git a/packages/react-openapi/src/OpenAPIWebhook.tsx b/packages/react-openapi/src/OpenAPIWebhook.tsx new file mode 100644 index 000000000..3878be56f --- /dev/null +++ b/packages/react-openapi/src/OpenAPIWebhook.tsx @@ -0,0 +1,30 @@ +import clsx from 'clsx'; +import { OpenAPIWebhookExample } from './OpenAPIWebhookExample'; +import { OpenAPIColumnSpec } from './common/OpenAPIColumnSpec'; +import { OpenAPISummary } from './common/OpenAPISummary'; +import type { OpenAPIContext, OpenAPIWebhookData } from './types'; + +/** + * Display an interactive OpenAPI webhook. + */ +export function OpenAPIWebhook(props: { + className?: string; + data: OpenAPIWebhookData; + context: OpenAPIContext; +}) { + const { className, data, context } = props; + + return ( +
+ +
+ +
+
+ +
+
+
+
+ ); +} diff --git a/packages/react-openapi/src/OpenAPIWebhookExample.tsx b/packages/react-openapi/src/OpenAPIWebhookExample.tsx new file mode 100644 index 000000000..7bb29b038 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIWebhookExample.tsx @@ -0,0 +1,59 @@ +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { OpenAPIEmptyExample } from './OpenAPIExample'; +import { OpenAPIMediaTypeContent } from './OpenAPIMediaType'; +import type { OpenAPIContext } from './types'; +import type { OpenAPIWebhookData } from './types'; +import { getExamples } from './util/example'; +import { createStateKey } from './utils'; + +export function OpenAPIWebhookExample(props: { + data: OpenAPIWebhookData; + context: OpenAPIContext; +}) { + const { data, context } = props; + const { operation } = data; + + const items = (() => { + if (!operation.requestBody) { + return []; + } + + return Object.entries( + operation.requestBody.content as Record + ).map(([key, value]) => { + const schema = value.schema; + + if (!schema) { + return { + key, + label: key, + body: , + }; + } + + return { + key, + label: key, + body: <>, + examples: getExamples({ + mediaTypeObject: value, + mediaType: key, + context, + }), + }; + }); + })(); + + return ( +
+

Payload

+
+ +
+
+ ); +} diff --git a/packages/react-openapi/src/common/OpenAPIColumnSpec.tsx b/packages/react-openapi/src/common/OpenAPIColumnSpec.tsx new file mode 100644 index 000000000..848f08351 --- /dev/null +++ b/packages/react-openapi/src/common/OpenAPIColumnSpec.tsx @@ -0,0 +1,30 @@ +import { OpenAPISpec } from '../OpenAPISpec'; +import { getOpenAPIClientContext } from '../context'; +import type { OpenAPIContext, OpenAPIOperationData, OpenAPIWebhookData } from '../types'; +import { OpenAPIOperationDescription } from './OpenAPIOperationDescription'; + +export function OpenAPIColumnSpec(props: { + data: OpenAPIOperationData | OpenAPIWebhookData; + context: OpenAPIContext; +}) { + const { data, context } = props; + const { operation } = data; + + const clientContext = getOpenAPIClientContext(context); + + return ( +
+ {operation['x-deprecated-sunset'] ? ( +
+ This operation is deprecated and will be sunset on{' '} + + {operation['x-deprecated-sunset']} + + {'.'} +
+ ) : null} + + +
+ ); +} diff --git a/packages/react-openapi/src/common/OpenAPIOperationDescription.tsx b/packages/react-openapi/src/common/OpenAPIOperationDescription.tsx new file mode 100644 index 000000000..2da004fa3 --- /dev/null +++ b/packages/react-openapi/src/common/OpenAPIOperationDescription.tsx @@ -0,0 +1,31 @@ +import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser'; +import { Markdown } from '../Markdown'; +import type { OpenAPIContext } from '../types'; +import { resolveDescription } from '../utils'; + +export function OpenAPIOperationDescription(props: { + operation: OpenAPIV3.OperationObject; + context: OpenAPIContext; +}) { + const { operation } = props; + if (operation['x-gitbook-description-document']) { + return ( +
+ {props.context.renderDocument({ + document: operation['x-gitbook-description-document'], + })} +
+ ); + } + + const description = resolveDescription(operation); + if (!description) { + return null; + } + + return ( +
+ +
+ ); +} diff --git a/packages/react-openapi/src/common/OpenAPIStability.tsx b/packages/react-openapi/src/common/OpenAPIStability.tsx new file mode 100644 index 000000000..5e6234f41 --- /dev/null +++ b/packages/react-openapi/src/common/OpenAPIStability.tsx @@ -0,0 +1,23 @@ +import type { OpenAPIStability as OpenAPIStabilityType } from '@gitbook/openapi-parser'; + +const stabilityEnum: Record = { + experimental: 'Experimental', + alpha: 'Alpha', + beta: 'Beta', +} as const; + +export function OpenAPIStability(props: { stability: OpenAPIStabilityType }) { + const { stability } = props; + + const foundStability = stabilityEnum[stability]; + + if (!foundStability) { + return null; + } + + return ( +
+ {foundStability} +
+ ); +} diff --git a/packages/react-openapi/src/common/OpenAPISummary.tsx b/packages/react-openapi/src/common/OpenAPISummary.tsx new file mode 100644 index 000000000..06ac0c79e --- /dev/null +++ b/packages/react-openapi/src/common/OpenAPISummary.tsx @@ -0,0 +1,44 @@ +import { OpenAPIPath } from '../OpenAPIPath'; +import type { OpenAPIContext, OpenAPIOperationData, OpenAPIWebhookData } from '../types'; +import { OpenAPIStability } from './OpenAPIStability'; + +export function OpenAPISummary(props: { + data: OpenAPIOperationData | OpenAPIWebhookData; + context: OpenAPIContext; +}) { + const { data, context } = props; + const { operation } = data; + + const title = (() => { + if (operation.summary) { + return operation.summary; + } + + if ('name' in data) { + return data.name; + } + + return undefined; + })(); + + return ( +
+ {(operation.deprecated || operation['x-stability']) && ( +
+ {operation.deprecated &&
Deprecated
} + {operation['x-stability'] && ( + + )} +
+ )} + {title + ? context.renderHeading({ + deprecated: operation.deprecated ?? false, + stability: operation['x-stability'], + title, + }) + : null} + {'path' in data ? : null} +
+ ); +} diff --git a/packages/react-openapi/src/index.ts b/packages/react-openapi/src/index.ts index ac4cd00c4..b30a180cb 100644 --- a/packages/react-openapi/src/index.ts +++ b/packages/react-openapi/src/index.ts @@ -1,5 +1,7 @@ export * from './schemas'; export * from './OpenAPIOperation'; +export * from './OpenAPIWebhook'; export * from './OpenAPIOperationContext'; export * from './resolveOpenAPIOperation'; -export type { OpenAPIOperationData, OpenAPIContext } from './types'; +export * from './resolveOpenAPIWebhook'; +export type { OpenAPIOperationData, OpenAPIContext, OpenAPIWebhookData } from './types'; diff --git a/packages/react-openapi/src/resolveOpenAPIWebhook.ts b/packages/react-openapi/src/resolveOpenAPIWebhook.ts new file mode 100644 index 000000000..f4c08a296 --- /dev/null +++ b/packages/react-openapi/src/resolveOpenAPIWebhook.ts @@ -0,0 +1,99 @@ +import { fromJSON, toJSON } from 'flatted'; + +import type { + Filesystem, + OpenAPIV3, + OpenAPIV3_1, + OpenAPIV3xDocument, +} from '@gitbook/openapi-parser'; +import { dereferenceFilesystem } from './dereference'; +import type { OpenAPIWebhookData } from './types'; + +export { fromJSON, toJSON }; + +/** + * Resolve an OpenAPI webhook in a file and compile it to a more usable format. + */ +export async function resolveOpenAPIWebhook( + filesystem: Filesystem, + webhookDescriptor: { + name: string; + method: string; + } +): Promise { + const { name, method } = webhookDescriptor; + const schema = await dereferenceFilesystem(filesystem); + let operation = getWebhookByNameAndMethod(schema, name, method); + + if (!operation) { + return null; + } + + // Resolve common parameters + const commonParameters = getPathObjectParameter(schema, name); + if (commonParameters) { + operation = { + ...operation, + parameters: [...commonParameters, ...(operation.parameters ?? [])], + }; + } + + const servers = 'servers' in schema ? (schema.servers ?? []) : []; + + return { + servers, + operation, + method, + name, + }; +} + +/** + * Get a path object from its path. + */ +function getPathObject( + schema: OpenAPIV3.Document | OpenAPIV3_1.Document, + name: string +): OpenAPIV3.PathItemObject | OpenAPIV3_1.PathItemObject | null { + if (schema.webhooks?.[name]) { + return schema.webhooks[name]; + } + return null; +} + +/** + * Resolve parameters from a path in an OpenAPI schema. + */ +function getPathObjectParameter( + schema: OpenAPIV3.Document | OpenAPIV3_1.Document, + path: string +): + | (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[] + | (OpenAPIV3.ParameterObject | OpenAPIV3_1.ReferenceObject)[] + | null { + const pathObject = getPathObject(schema, path); + if (pathObject?.parameters) { + return pathObject.parameters; + } + return null; +} + +/** + * Get an operation by its path and method. + */ +function getWebhookByNameAndMethod( + schema: OpenAPIV3.Document | OpenAPIV3_1.Document, + name: string, + method: string +): OpenAPIV3.OperationObject | null { + // Types are buffy for OpenAPIV3_1.OperationObject, so we use v3 + const pathObject = getPathObject(schema, name); + if (!pathObject) { + return null; + } + const normalizedMethod = method.toLowerCase(); + if (!pathObject[normalizedMethod]) { + return null; + } + return pathObject[normalizedMethod]; +} diff --git a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx index f090089c1..353878a7e 100644 --- a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx +++ b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx @@ -1,11 +1,12 @@ import type { OpenAPISchema } from '@gitbook/openapi-parser'; import clsx from 'clsx'; import { OpenAPIDisclosure } from '../OpenAPIDisclosure'; -import { OpenAPIExample, getExampleFromSchema } from '../OpenAPIExample'; +import { OpenAPIExample } from '../OpenAPIExample'; import { OpenAPIRootSchema } from '../OpenAPISchemaServer'; import { Section, SectionBody, StaticSection } from '../StaticSection'; import { getOpenAPIClientContext } from '../context'; import type { OpenAPIContext } from '../types'; +import { getExampleFromSchema } from '../util/example'; /** * OpenAPI Schemas component. diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 94dad6f5d..2d4435432 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -72,3 +72,14 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties { /** Securities that should be used for this operation */ securities: [string, OpenAPISecurityWithRequired][]; } + +export interface OpenAPIWebhookData extends OpenAPICustomSpecProperties { + name: string; + method: string; + + /** Servers to be used for this operation */ + servers: OpenAPIV3.ServerObject[]; + + /** Spec of the webhook */ + operation: OpenAPIV3.OperationObject; +} diff --git a/packages/react-openapi/src/util/example.tsx b/packages/react-openapi/src/util/example.tsx new file mode 100644 index 000000000..8559d5e32 --- /dev/null +++ b/packages/react-openapi/src/util/example.tsx @@ -0,0 +1,119 @@ +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { OpenAPIExample } from '../OpenAPIExample'; +import { generateSchemaExample } from '../generateSchemaExample'; +import type { OpenAPIContext } from '../types'; +import { checkIsReference } from '../utils'; + +/** + * Generate an example from a reference object. + */ +export function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject { + return { summary: 'Unresolved reference', value: { $ref: ref.$ref } }; +} + +/** + * Get examples from a media type object. + */ +export function getExamplesFromMediaTypeObject(args: { + mediaType: string; + mediaTypeObject: OpenAPIV3.MediaTypeObject; +}): { key: string; example: OpenAPIV3.ExampleObject }[] { + const { mediaTypeObject, mediaType } = args; + if (mediaTypeObject.examples) { + return Object.entries(mediaTypeObject.examples).map(([key, example]) => { + return { + key, + example: checkIsReference(example) ? getExampleFromReference(example) : example, + }; + }); + } + + if (mediaTypeObject.example) { + return [{ key: 'default', example: { value: mediaTypeObject.example } }]; + } + + if (mediaTypeObject.schema) { + if (mediaType === 'application/xml') { + // @TODO normally we should use the name of the schema but we don't have it + // fix it when we got the reference name + const root = mediaTypeObject.schema.xml?.name ?? 'object'; + return [ + { + key: 'default', + example: { + value: { + [root]: generateSchemaExample(mediaTypeObject.schema, { + xml: mediaType === 'application/xml', + mode: 'read', + }), + }, + }, + }, + ]; + } + return [ + { + key: 'default', + example: { + value: generateSchemaExample(mediaTypeObject.schema, { + mode: 'read', + }), + }, + }, + ]; + } + return []; +} + +/** + * Get example from a schema object. + */ +export function getExampleFromSchema(args: { + schema: OpenAPIV3.SchemaObject; +}): OpenAPIV3.ExampleObject { + const { schema } = args; + + if (schema.example) { + return { value: schema.example }; + } + + return { value: generateSchemaExample(schema, { mode: 'read' }) }; +} + +/** + * Get the examples from a media type object. + */ +export function getExamples(props: { + mediaTypeObject: OpenAPIV3.MediaTypeObject; + mediaType: string; + context: OpenAPIContext; +}) { + const { mediaTypeObject, mediaType } = props; + const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType }); + const syntax = getSyntaxFromMediaType(mediaType); + + return examples.map((example) => { + return { + key: example.key, + label: example.example.summary || example.key, + body: ( + + ), + }; + }); +} + +/** + * Get the syntax from a media type. + */ +function getSyntaxFromMediaType(mediaType: string): string { + if (mediaType.includes('json')) { + return 'json'; + } + + if (mediaType === 'application/xml') { + return 'xml'; + } + + return 'text'; +}