diff --git a/.github/workflows/canary-beta-release.yml b/.github/workflows/canary-beta-release.yml index 6762f3e25..abac23ecd 100644 --- a/.github/workflows/canary-beta-release.yml +++ b/.github/workflows/canary-beta-release.yml @@ -3,7 +3,7 @@ name: Canary Beta Release on: push: branches: - - v2.0.0 + - v3.0.0 paths: - packages/** @@ -14,7 +14,7 @@ jobs: publish-canary: name: Publish Canary Beta runs-on: ubuntu-latest - if: ${{ github.repository == 'PaloAltoNetworks/docusaurus-openapi-docs' && github.ref == 'refs/heads/v2.0.0' && github.event_name == 'push' }} + if: ${{ github.repository == 'PaloAltoNetworks/docusaurus-openapi-docs' && github.ref == 'refs/heads/v3.0.0' && github.event_name == 'push' }} steps: - name: Checkout uses: actions/checkout@v3 @@ -30,7 +30,7 @@ jobs: git config --global user.name "Steven Serrata" git config --global user.email "sserrata@paloaltonetworks.com" git fetch - git checkout v2.0.0 + git checkout v3.0.0 echo "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}" >> .npmrc env: NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8a42c4cf0..34cd3b01a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,7 +2,7 @@ name: "CodeQL" on: push: - branches: [main, v2.0.0] + branches: [main, v3.0.0] jobs: analyze: diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index b0454c33e..569c57acb 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -2,7 +2,7 @@ name: "Deploy Preview" on: pull_request_target: - branches: [main, v2.0.0] + branches: [main, v3.0.0] jobs: precheck: diff --git a/.github/workflows/pr-title-check.yaml b/.github/workflows/pr-title-check.yaml index 8c647198a..a7fbb042b 100644 --- a/.github/workflows/pr-title-check.yaml +++ b/.github/workflows/pr-title-check.yaml @@ -4,7 +4,7 @@ on: pull_request: branches: - main - - v2.0.0 + - v3.0.0 types: - opened - synchronize diff --git a/.github/workflows/release-beta.yaml b/.github/workflows/release-beta.yaml index 5903a8d48..68084aa56 100644 --- a/.github/workflows/release-beta.yaml +++ b/.github/workflows/release-beta.yaml @@ -3,7 +3,7 @@ name: Release Beta on: push: branches: - - v2.0.0 + - v3.0.0 env: FORCE_COLOR: true diff --git a/.gitignore b/.gitignore index bc9d2e278..f7c58c98b 100644 --- a/.gitignore +++ b/.gitignore @@ -137,6 +137,8 @@ dist demo/**/*.api.mdx demo/**/*.info.mdx demo/**/*.tag.mdx +demo/**/*.schema.mdx demo/**/sidebar.js demo/**/versions.json +.idea diff --git a/.vscode/settings.json b/.vscode/settings.json index 92660fde6..77cb6d20d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,21 +4,21 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ea0eebba..8ce96e91b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,136 @@ +## 2.2.0 (Jul 3, 2024) + +High level enhancements + +- Improved support for customizing code snippets +- Switched back to canonical postman depdendencies +- Improved support for OpenAPI readOnly/writeOnly +- Added support for OpenAPI `x-tags` + +Other enhancements and bug fixes + +- fix typo in attribute ([#864](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/864)) +- uncomment version dropdown styles ([#863](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/863)) +- revert to canonical postman libraries ([#861](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/861)) +- Fix tagGroup display when showSchemas is configured ([#851](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/851)) +- check to avoid tagGroup config before concat operation, api, schemas ([#854](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/854)) +- support empty object schema type ([#849](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/849)) +- ensure readOnly/writeOnly are evaluated first ([#848](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/848)) +- fix: markdown table within the description attribute cannot be rendered correctly ([#831](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/831)) +- Implement the `x-tags` extension for schema objects ([#837](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/837)) +- fix col row padding footer&pagination ([#810](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/810)) +- Update index.tsx ([#839](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/839)) +- Fix clean-api-docs not deleting sidebar.ts ([#829](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/829)) +- Add option to disable frontmatter api prop compression ([#800](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/800)) +- preventing to send form onClick left/right arrows in SchemaTabs component ([#796](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/796)) +- changed theme and plugin to headings ([#786](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/786)) +- Allow custom plugin to render ([#784](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/784)) +- Remove scrollbar width for Tab components ([#785](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/785)) + +## 2.1.3 (Mar 22, 2024) + +High level enhancements + +- bugfix + +Other enhancements and bug fixes + +- ensure correct eval of required properties with allOf ([#771](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/771)) + +## 2.1.2 (Mar 21, 2024) + +High level enhancements + +- Various bug fixes + +Other enhancements and bug fixes + +- Fix allOf schema qualifier and type ([#766](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/766)) +- Ensure qualifiers are rendered for polymorphic/primitive properties ([#765](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/765)) +- uncomment line preventing grouping by operation tags ([#764](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/764)) +- ensure resize observer is calculated once per frame to avoid loops ([#763](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/763)) + +## 2.1.1 (Mar 20, 2024) + +High level enhancements + +- Improved support for OpenAPI 3.1 + +Other enhancements and bug fixes + +- [bugfix] Ensure 0 and false are guarded correctly and add deprecated support to params ([#754](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/754)) +- upgrade openapi parsers ([#748](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/748)) + +## 2.1.0 (Mar 15, 2024) + +High level enhancements + +- Adds support for generating sidebar using x-tagGroup +- Adds support for generating schemas +- Improved x-codeSamples support + +Other enhancements and bug fixes + +- Upgrade dependencies ([#741](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/741)) +- update compatibility matrix ([#739](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/739)) +- feat: implement x-tagGroup feature ([#737](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/737)) +- feat: Implement schema pages behind a config option `showSchemas` ([#736](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/736)) +- fix: Guard only undefined and empty strings ([#725](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/725)) +- Conditional display of header Request ([#719](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/719)) +- Fix x-codeSamples load when switching language tabs ([#707](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/707)) + +## 2.0.4 (Jan 18, 2024) + +High level enhancements + +- Add x-codeSamples support +- Add callbacks support + +Other enhancements and bug fixes + +- Add support to x-codeSamples ([#697](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/697)) +- Remove deprecated node packages ([#699](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/699)) +- Add Callbacks support ([#691](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/691)) + +## 2.0.3 (Jan 9, 2024) + +High level enhancements + +- Add callbacks support +- Add markdown support to example/examples summary + +Other enhancements and bug fixes + +- Add markdown support to example summary ([#690](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/690)) +- Add Callbacks support ([#691](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/691)) + +## 2.0.2 (Dec 5, 2023) + +High level enhancements + +- Adds security schemes to API Explorer panel + +Other enhancements and bug fixes + +- Add missing security schemes component ([#673](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/673)) + +## 2.0.1 (Dec 1, 2023) + +High level enhancements + +- Improve support for handling multi-part form data + +Other enhancements and bug fixes + +- Improve support for handling multipart form data ([#666](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/666)) +- use SCHEMA_TYPE to distinguish request/response to support readOnly/writeOnly properties ([#665](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/665)) +- add negative look behind to exclude colon delimited path segments ([#663](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/pull/663)) +- Update deploy-preview.yml +- update v2.0.0 refs to v3.0.0 +- Update pr-title-check.yaml +- Update canary-beta-release.yml +- Update README.md + ## 2.0.0 (Nov 13, 2023) High level enhancements diff --git a/README.md b/README.md index 8e70c5744..c7b689d66 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Key Features: | Docusaurus OpenAPI Docs | Docusaurus | | ----------------------- | --------------- | +| 3.0.0-beta.x (beta) | `3.0.1 - 3.1.1` | | 2.0.x (current) | `2.4.1 - 2.4.3` | | 1.7.3 (legacy) | `2.0.1 - 2.2.0` | @@ -145,10 +146,11 @@ Here is an example of properly configuring `docusaurus.config.js` file for `docu The `docusaurus-plugin-openapi-docs` plugin can be configured with the following options: -| Name | Type | Default | Description | -| -------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | `string` | `null` | A unique plugin ID. | -| `docsPluginId` | `string` | `null` | The ID associated with the `plugin-content-docs` or `preset` instance used to render the OpenAPI docs (e.g. "your-plugin-id", "classic", "default"). | +| Name | Type | Default | Description | +| -------------- | -------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | `string` | `null` | A unique plugin ID. | +| `docsPlugin` | `string` | `@docusaurus/plugin-content-docs` | The plugin used to render the OpenAPI docs (ignored if the plugin instance referenced by `docsPluginId` is a `preset`). | +| `docsPluginId` | `string` | `null` | The plugin ID associated with the `preset` or configured `docsPlugin` instance used to render the OpenAPI docs (e.g. "your-plugin-id", "classic", "default"). | ### config @@ -169,12 +171,13 @@ The `docusaurus-plugin-openapi-docs` plugin can be configured with the following | `baseUrl` | `string` | `null` | _Optional:_ Version base URL used when generating version selector dropdown menu. | | `versions` | `object` | `null` | _Optional:_ Set of options for versioning configuration. See below for a list of supported options. | | `markdownGenerators` | `object` | `null` | _Optional:_ Customize MDX content with a set of options for specifying markdown generator functions. See below for a list of supported options. | +| `showSchemas` | `boolean` | `null` | _Optional:_ If set to `true`, generates schema pages and adds them to the sidebar. | `sidebarOptions` can be configured with the following options: | Name | Type | Default | Description | | -------------------- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `groupPathsBy` | `string` | `null` | Organize and group sidebar slice by specified option. Note: Currently, `groupPathsBy` only contains support for grouping by `tag`. | +| `groupPathsBy` | `string` | `null` | Organize and group sidebar slice by specified option. Note: Currently, `groupPathsBy` only contains support for grouping by `tag` and `tagGroup`. | | `categoryLinkSource` | `string` | `null` | Defines what source to use for rendering category link pages when grouping paths by tag.

The supported options are as follows:

`tag`: Sets the category link config type to `generated-index` and uses the tag description as the link config description.

`info`: Sets the category link config type to `doc` and renders the `info` section as the category link (recommended only for multi/micro-spec scenarios).

`none`: Does not create pages for categories, only groups that can be expanded/collapsed. | | `sidebarCollapsible` | `boolean` | `true` | Whether sidebar categories are collapsible by default. | | `sidebarCollapsed` | `boolean` | `true` | Whether sidebar categories are collapsed by default. | @@ -197,11 +200,12 @@ The `docusaurus-plugin-openapi-docs` plugin can be configured with the following `markdownGenerators` can be configured with the following options: -| Name | Type | Default | Description | -| ------------------ | ---------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| `createApiPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for API pages.

**Function type:** `(pageData: ApiPageMetadata) => string` | -| `createInfoPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for info pages.

**Function type:** `(pageData: InfoPageMetadata) => string` | -| `createTagPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for tag pages.

**Function type:** `(pageData: TagPageMetadata) => string` | +| Name | Type | Default | Description | +| -------------------- | ---------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `createApiPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for API pages.

**Function type:** `(pageData: ApiPageMetadata) => string` | +| `createInfoPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for info pages.

**Function type:** `(pageData: InfoPageMetadata) => string` | +| `createTagPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for tag pages.

**Function type:** `(pageData: TagPageMetadata) => string` | +| `createSchemaPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for schema pages.

**Function type:** `(pageData: SchemaPageMetadata) => string` | ## CLI Usage diff --git a/demo/docs/customization/languagetabs.md b/demo/docs/customization/languagetabs.md index 0d9de3d36..b40c8cdca 100644 --- a/demo/docs/customization/languagetabs.md +++ b/demo/docs/customization/languagetabs.md @@ -15,7 +15,7 @@ The Docusaurus OpenAPI docs plugin comes with support for 8 languages which you | `curl` | bash | `curl`\* | | `python` | python | `requests`\*, `http.client` | | `go` | go | `native`\* | -| `nodejs` | javascript | `axios`\*, `native`, `requests`, `unirest` | +| `nodejs` | javascript | `axios`\*, `native` | | `ruby` | ruby | `net::http`\* | | `csharp` | csharp | `restsharp`\*, `httpclient` | | `php` | php | `curl`\*, `guzzle`, `pecl_http`, `http_request2` | diff --git a/demo/docs/customization/styling.md b/demo/docs/customization/styling.md index 7a3c34c60..6a43a5e22 100644 --- a/demo/docs/customization/styling.md +++ b/demo/docs/customization/styling.md @@ -18,13 +18,15 @@ The demo site uses the following CSS to add coloured labels to each request incl ```css /* API Menu Items */ -.api-method > .menu__link { +.api-method > .menu__link, +.schema > .menu__link { align-items: center; justify-content: start; } -.api-method > .menu__link::before { - width: 50px; +.api-method > .menu__link::before, +.schema > .menu__link::before { + width: 55px; height: 20px; font-size: 12px; line-height: 20px; @@ -68,6 +70,16 @@ The demo site uses the following CSS to add coloured labels to each request incl content: "head"; background-color: var(--ifm-color-secondary-darkest); } + +.event > .menu__link::before { + content: "event"; + background-color: var(--ifm-color-secondary-darkest); +} + +.schema > .menu__link::before { + content: "schema"; + background-color: var(--ifm-color-secondary-darkest); +} ``` ## Alternative Styling @@ -76,13 +88,15 @@ In [this issue](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issu ```css /* Sidebar Method labels */ -.api-method > .menu__link { +.api-method > .menu__link, +.schema > .menu__link { align-items: center; justify-content: start; } -.api-method > .menu__link::before { - width: 50px; +.api-method > .menu__link::before, +.schema > .menu__link::before { + width: 55px; height: 20px; font-size: 12px; line-height: 20px; @@ -137,4 +151,18 @@ In [this issue](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/issu color: var(--ifm-color-secondary-contrast-foreground); border-color: var(--ifm-color-secondary-dark); } + +.event > .menu__link::before { + content: "event"; + background-color: var(--ifm-color-secondary-contrast-background); + color: var(--ifm-color-secondary-contrast-foreground); + border-color: var(--ifm-color-secondary-dark); +} + +.schema > .menu__link::before { + content: "schema"; + background-color: var(--ifm-color-secondary-contrast-background); + color: var(--ifm-color-secondary-contrast-foreground); + border-color: var(--ifm-color-secondary-dark); +} ``` diff --git a/demo/docs/intro.mdx b/demo/docs/intro.mdx index e5d43e55d..1143ae386 100644 --- a/demo/docs/intro.mdx +++ b/demo/docs/intro.mdx @@ -58,6 +58,7 @@ Key Features: | Docusaurus OpenAPI Docs | Docusaurus | | ----------------------- | --------------- | +| 3.0.0-beta.x (beta) | `3.0.1 - 3.1.1` | | 2.0.x (current) | `2.4.1 - 2.4.3` | | 1.7.3 (legacy) | `2.0.1 - 2.2.0` | @@ -95,13 +96,13 @@ yarn start Both the plugin and theme are currently designed to pair with a specific Docusaurus release. The Docusaurus badge in the `README.md` and at the top of this page will always reflect the current compatible versions. ::: -Plugin: +### Plugin ```bash yarn add docusaurus-plugin-openapi-docs ``` -Theme: +### Theme ```bash yarn add docusaurus-theme-openapi-docs @@ -336,6 +337,7 @@ The `docusaurus-plugin-openapi-docs` plugin can be configured with the following | `baseUrl` | `string` | `null` | _Optional:_ Version base URL used when generating version selector dropdown menu. | | `versions` | `object` | `null` | _Optional:_ Set of options for versioning configuration. See below for a list of supported options. | | `markdownGenerators` | `object` | `null` | _Optional:_ Customize MDX content with a set of options for specifying markdown generator functions. See below for a list of supported options. | +| `showSchemas` | `boolean` | `null` | _Optional:_ If set to `true`, generates schema pages and adds them to the sidebar. | ### sidebarOptions @@ -373,11 +375,12 @@ All versions will automatically inherit `sidebarOptions` from the parent/base co `markdownGenerators` can be configured with the following options: -| Name | Type | Default | Description | -| ------------------ | ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------| -| `createApiPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for API pages.

**Function type:** `(pageData: ApiPageMetadata) => string` | -| `createInfoPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for info pages.

**Function type:** `(pageData: InfoPageMetadata) => string` | -| `createTagPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for tag pages.

**Function type:** `(pageData: TagPageMetadata) => string` | +| Name | Type | Default | Description | +| ------------------- | ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------| +| `createApiPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for API pages.

**Function type:** `(pageData: ApiPageMetadata) => string` | +| `createInfoPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for info pages.

**Function type:** `(pageData: InfoPageMetadata) => string` | +| `createTagPageMD` | `function` | `null` | _Optional:_ Returns a string of the raw markdown body for tag pages.

**Function type:** `(pageData: TagPageMetadata) => string` | +| `createSchemaPageMD`| `function` | `null` | _Optional:_ Returns a string of the raw markdown body for schema pages.

**Function type:** `(pageData: SchemaPageMetadata) => string` | :::info NOTE If a config option is not provided, its default markdown generator will be used. diff --git a/demo/docs/sidebars.md b/demo/docs/sidebars.md index 73245e9e0..30bcb00d7 100644 --- a/demo/docs/sidebars.md +++ b/demo/docs/sidebars.md @@ -51,6 +51,14 @@ For greater control, you also have the option of constructing the sidebar object The OpenAPI docs plugin provides out-of-the-box support for grouping paths by "tag". What this means is that the plugin will automatically generate a sidebar slice using the first path tag as the "group by" value and the path itself as one of the `items` under that category. +### Grouping by TagGroup + +The OpenAPI docs plugin provides out-of-the-box support for grouping paths by "tagGroup". +What this means is that the plugin will automatically generate a sidebar slice using the first path group as the "group by" value and the path itself as one of the `tags` under that category. +Use `x-tagGroups` to group tags in the [Reference](https://redocly.com/docs/api-reference-docs/specification-extensions/x-tag-groups/) docs navigation sidebar. Add it to the root OpenAPI object. + +If `x-tagGroups` is used for grouping API paths, and you've also configured `showSchemas: true` for your OpenAPI Docs plugin, an additional "sibling" category labelled `Schemas` will be created and placed at the end of sidebar, after all the `tagGroups` categories. + ### Category Links Docusaurus now supports the ability to designate or customize what page gets displayed when a category is clicked. @@ -60,3 +68,9 @@ The OpenAPI Docs plugin can leverage this feature in a number of ways, including - Using the `generated-index` feature to create an index of all paths/endpoints available under a tag. - Setting the `tag` description of an OpenAPI specification as the content that displays when a category is clicked. - Setting the `info` section of an OpenAPI specification as the page that displays when a category is clicked (reserved primarily for micro-specs). + +### Grouping Schemas by `x-tags` + +The OpenAPI plugin provides out-of-the-box support for grouping schema objects into tags alongside path objects grouped by that same tag. + +What this means is that when the `groupPathsBy` sidebar option is set to `tag`, any `x-tag`ged schema objects will be gathered together with the tagged paths in that sidebar category. In the event that `showSchemas` is not configured, and `x-tags` is found on a schema object, the schema **will be included** in the relevant tag's category sidebar. diff --git a/demo/docusaurus.config.js b/demo/docusaurus.config.js index 409581e4e..c7355ece9 100644 --- a/demo/docusaurus.config.js +++ b/demo/docusaurus.config.js @@ -139,16 +139,20 @@ const config = { additionalLanguages: ["ruby", "csharp", "php", "java", "powershell"], }, languageTabs: [ + { + highlight: "python", + language: "python", + logoClass: "python", + }, { highlight: "bash", language: "curl", logoClass: "bash", }, { - highlight: "python", - language: "python", - logoClass: "python", - variant: "requests", + highlight: "csharp", + language: "csharp", + logoClass: "csharp", }, { highlight: "go", @@ -159,19 +163,12 @@ const config = { highlight: "javascript", language: "nodejs", logoClass: "nodejs", - variant: "axios", }, { highlight: "ruby", language: "ruby", logoClass: "ruby", }, - { - highlight: "csharp", - language: "csharp", - logoClass: "csharp", - variant: "httpclient", - }, { highlight: "php", language: "php", @@ -218,12 +215,16 @@ const config = { version: "2.0.0", // Current version label: "v2.0.0", // Current version label baseUrl: "/petstore_versioned/swagger-petstore-yaml", // Leading slash is important + downloadUrl: + "https://raw.githubusercontent.com/PaloAltoNetworks/docusaurus-openapi-docs/main/demo/examples/petstore.yaml", versions: { "1.0.0": { specPath: "examples/petstore-1.0.0.yaml", outputDir: "docs/petstore_versioned/1.0.0", // No trailing slash label: "v1.0.0", baseUrl: "/petstore_versioned/1.0.0/swagger-petstore-yaml", // Leading slash is important + downloadUrl: + "https://redocly.com/_spec/docs/openapi/petstore.json", }, }, }, @@ -236,9 +237,10 @@ const config = { categoryLinkSource: "tag", }, template: "api.mustache", // Customize API MDX with mustache template - downloadUrl: - "https://raw.githubusercontent.com/PaloAltoNetworks/docusaurus-openapi-docs/main/demo/examples/petstore.yaml", + downloadUrl: "/petstore.yaml", hideSendButton: false, + showSchemas: true, + disableCompression: true, }, cos: { specPath: "examples/openapi-cos.json", @@ -255,6 +257,14 @@ const config = { specPath: "examples/food/yogurtstore/openapi.yaml", outputDir: "docs/food/yogurtstore", }, + restaurant: { + specPath: "examples/food/restaurant/openapi.yaml", + outputDir: "docs/restaurant", + sidebarOptions: { + groupPathsBy: "tagGroup", + }, + showSchemas: true, + }, }, }, ], diff --git a/demo/examples/food/restaurant/_category_.json b/demo/examples/food/restaurant/_category_.json new file mode 100644 index 000000000..54240eaac --- /dev/null +++ b/demo/examples/food/restaurant/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Restaurant", + "collapsed": true +} diff --git a/demo/examples/food/restaurant/openapi.yaml b/demo/examples/food/restaurant/openapi.yaml new file mode 100644 index 000000000..be9eb022b --- /dev/null +++ b/demo/examples/food/restaurant/openapi.yaml @@ -0,0 +1,81 @@ +openapi: 3.0.3 +info: + title: Restaurant Example + version: 1.0.0 + description: Sample description. +paths: + /menu: + get: + tags: + - tag1 + summary: Get Menu + description: Froyo's the best! + responses: + 200: + description: OK + /products: + get: + tags: + - tag1 + - tag2 + summary: List All Products + description: Froyo's the best! + responses: + 200: + description: OK + /drinks: + get: + tags: + - tag1 + - tag2 + summary: List All Drinks + description: Froyo's the best! + responses: + 200: + description: OK + /pay: + post: + tags: + - tag3 + summary: Make Payment + description: Froyo's the best! + responses: + 200: + description: OK + +components: + schemas: + Payment: + type: object + properties: + amount: + type: number + method: + type: string + enum: [cash, card, check] + +tags: + - name: tag1 + description: Everything about your restaurant + x-displayName: Tag 1 + - name: tag2 + description: Tag 2 description + x-displayName: Tag 2 + - name: tag3 + description: Tag 3 description + x-displayName: Tag 3 + +x-tagGroups: + - name: Tag 1 & 2 + tags: + - tag1 + - tag2 + - name: Trinity + tags: + - tag1 + - tag2 + - tag3 + - name: Last Two + tags: + - tag2 + - tag3 diff --git a/demo/examples/petstore.yaml b/demo/examples/petstore.yaml index c27f0d903..c31ac04ae 100644 --- a/demo/examples/petstore.yaml +++ b/demo/examples/petstore.yaml @@ -37,7 +37,7 @@ info: 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. @@ -147,6 +147,7 @@ paths: Console.WriteLine(response.getRawResponse()); } - lang: PHP + label: Custom source: | $form = new \PetStore\Entities\Pet(); $form->setPetType("Dog"); @@ -933,6 +934,8 @@ components: message: type: string Cat: + x-tags: + - pet description: A representation of a cat allOf: - $ref: "#/components/schemas/Pet" @@ -995,6 +998,7 @@ components: description: Average amount of honey produced per day in ounces example: 3.14 multipleOf: .01 + default: 0 required: - honeyPerDay Id: diff --git a/demo/package.json b/demo/package.json index e3f779d18..aad62d64d 100644 --- a/demo/package.json +++ b/demo/package.json @@ -1,6 +1,6 @@ { "name": "demo", - "version": "2.0.0", + "version": "2.2.0", "private": true, "scripts": { "docusaurus": "docusaurus", @@ -26,8 +26,8 @@ "@docusaurus/preset-classic": "2.4.3", "@mdx-js/react": "^1.6.22", "clsx": "^1.1.1", - "docusaurus-plugin-openapi-docs": "^2.0.0", - "docusaurus-theme-openapi-docs": "^2.0.0", + "docusaurus-plugin-openapi-docs": "^2.2.0", + "docusaurus-theme-openapi-docs": "^2.2.0", "prism-react-renderer": "^1.3.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/demo/sidebars.js b/demo/sidebars.js index c9755b4ec..0ec8de6b1 100644 --- a/demo/sidebars.js +++ b/demo/sidebars.js @@ -100,6 +100,16 @@ const sidebars = { }, ], }, + { + type: "category", + label: "Restaurant", + link: { + type: "generated-index", + title: "Restaurant API", + slug: "/category/restaurant-api", + }, + items: require("./docs/restaurant/sidebar.js"), + }, ], "petstore-2.0.0": [ { diff --git a/demo/src/css/custom.css b/demo/src/css/custom.css index 6dc5ea86d..4ca833446 100644 --- a/demo/src/css/custom.css +++ b/demo/src/css/custom.css @@ -37,13 +37,15 @@ a:any-link:hover { } /* Sidebar Method labels */ -.api-method > .menu__link { +.api-method > .menu__link, +.schema > .menu__link { align-items: center; justify-content: start; } -.api-method > .menu__link::before { - width: 50px; +.api-method > .menu__link::before, +.schema > .menu__link::before { + width: 55px; height: 20px; font-size: 12px; line-height: 20px; @@ -93,6 +95,11 @@ a:any-link:hover { background-color: var(--ifm-color-secondary-darkest); } +.schema > .menu__link::before { + content: "schema"; + background-color: var(--ifm-color-secondary-darkest); +} + /* GitHub Header Link */ .header-github-link:hover { opacity: 0.6; diff --git a/demo/static/petstore.yaml b/demo/static/petstore.yaml new file mode 100644 index 000000000..c31ac04ae --- /dev/null +++ b/demo/static/petstore.yaml @@ -0,0 +1,1271 @@ +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: | + + - name: store_model + x-displayName: The Order Model + description: | + +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: + "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 " + - 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 + 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: | + + + 123 + inProgress + 2018-10-19T16:46:45Z + + 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: | + + + 123 + inProgress + 2018-10-19T16:46:45Z + + 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: + value: OK + application/xml: + schema: + type: string + examples: + response: + value: OK + 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 + 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 + 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 + 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: + 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/lerna.json b/lerna.json index db66e79ea..91d66593d 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.0.0", + "version": "2.2.0", "npmClient": "yarn", "useWorkspaces": true } diff --git a/package.json b/package.json index 0e7c3a1c8..c61cdd9a5 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ }, "devDependencies": { "@babel/core": "^7.16.0", + "@eslint-community/eslint-utils": "^4.4.0", "@testing-library/cypress": "^8.0.1", "@types/jest": "^27.0.2", "@types/node": "^17.0.2", @@ -76,5 +77,6 @@ }, "engines": { "node": ">=14" - } + }, + "packageManager": "yarn@1.22.1" } diff --git a/packages/docusaurus-plugin-openapi-docs/README.md b/packages/docusaurus-plugin-openapi-docs/README.md index 5be5e9751..a775eeccb 100644 --- a/packages/docusaurus-plugin-openapi-docs/README.md +++ b/packages/docusaurus-plugin-openapi-docs/README.md @@ -31,6 +31,7 @@ Key Features: | Docusaurus OpenAPI Docs | Docusaurus | | ----------------------- | --------------- | +| 3.0.0-beta.x (beta) | `3.0.1 - 3.1.1` | | 2.0.x (current) | `2.4.1 - 2.4.3` | | 1.7.3 (legacy) | `2.0.1 - 2.2.0` | @@ -159,6 +160,7 @@ The `docusaurus-plugin-openapi-docs` plugin can be configured with the following | `baseUrl` | `string` | `null` | _Optional:_ Version base URL used when generating version selector dropdown menu. | | `versions` | `object` | `null` | _Optional:_ Set of options for versioning configuration. See below for a list of supported options. | | `markdownGenerators` | `object` | `null` | _Optional:_ Customize MDX content with a set of options for specifying markdown generator functions. See below for a list of supported options. | +| `showSchemas` | `boolean` | `null` | _Optional:_ If set to `true`, generates schema pages and adds them to the sidebar. | `sidebarOptions` can be configured with the following options: diff --git a/packages/docusaurus-plugin-openapi-docs/package.json b/packages/docusaurus-plugin-openapi-docs/package.json index 0fe385bc3..599116f1d 100644 --- a/packages/docusaurus-plugin-openapi-docs/package.json +++ b/packages/docusaurus-plugin-openapi-docs/package.json @@ -1,7 +1,7 @@ { "name": "docusaurus-plugin-openapi-docs", "description": "OpenAPI plugin for Docusaurus.", - "version": "2.0.0", + "version": "2.2.0", "license": "MIT", "keywords": [ "openapi", @@ -36,13 +36,11 @@ "@types/mustache": "^4.1.2" }, "dependencies": { - "@apidevtools/json-schema-ref-parser": "^10.1.0", + "@apidevtools/json-schema-ref-parser": "^11.5.4", "@docusaurus/plugin-content-docs": ">=2.4.1 <=2.4.3", "@docusaurus/utils": ">=2.4.1 <=2.4.3", "@docusaurus/utils-validation": ">=2.4.1 <=2.4.3", - "@paloaltonetworks/openapi-to-postmanv2": "3.1.0-hotfix.1", - "@paloaltonetworks/postman-collection": "^4.1.0", - "@redocly/openapi-core": "^1.0.0-beta.125", + "@redocly/openapi-core": "^1.10.5", "chalk": "^4.1.2", "clsx": "^1.1.1", "fs-extra": "^9.0.1", @@ -50,6 +48,8 @@ "json-schema-merge-allof": "^0.8.1", "lodash": "^4.17.20", "mustache": "^4.2.0", + "openapi-to-postmanv2": "^4.21.0", + "postman-collection": "^4.4.0", "slugify": "^1.6.5", "swagger2openapi": "^7.0.8", "xml-formatter": "^2.6.1" diff --git a/packages/docusaurus-plugin-openapi-docs/src/index.ts b/packages/docusaurus-plugin-openapi-docs/src/index.ts index ea679f25b..730d1de45 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/index.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/index.ts @@ -14,11 +14,25 @@ import { Globby, posixPath } from "@docusaurus/utils"; import chalk from "chalk"; import { render } from "mustache"; -import { createApiPageMD, createInfoPageMD, createTagPageMD } from "./markdown"; +import { + createApiPageMD, + createInfoPageMD, + createSchemaPageMD, + createTagPageMD, +} from "./markdown"; import { readOpenapiFiles, processOpenapiFiles } from "./openapi"; import { OptionsSchema } from "./options"; import generateSidebarSlice from "./sidebars"; -import type { PluginOptions, LoadedContent, APIOptions } from "./types"; +import type { + PluginOptions, + LoadedContent, + APIOptions, + ApiMetadata, + ApiPageMetadata, + InfoPageMetadata, + TagPageMetadata, + SchemaPageMetadata, +} from "./types"; export function isURL(str: string): boolean { return /^(https?:)\/\//m.test(str); @@ -26,6 +40,7 @@ export function isURL(str: string): boolean { export function getDocsPluginConfig( presetsPlugins: any[], + plugin: string, pluginId: string ): Object | undefined { // eslint-disable-next-line array-callback-return @@ -37,10 +52,7 @@ export function getDocsPluginConfig( } // Search plugin-content-docs instances - if ( - typeof data[0] === "string" && - data[0] === "@docusaurus/plugin-content-docs" - ) { + if (typeof data[0] === "string" && data[0] === plugin) { const configPluginId = data[1].id ? data[1].id : "default"; if (configPluginId === pluginId) { return data[1]; @@ -56,7 +68,7 @@ export function getDocsPluginConfig( } // Search plugin-content-docs instances - if (filteredConfig[0] === "@docusaurus/plugin-content-docs") { + if (filteredConfig[0] === plugin) { const configPluginId = filteredConfig[1].id ? filteredConfig[1].id : "default"; @@ -80,14 +92,22 @@ export default function pluginOpenAPIDocs( context: LoadContext, options: PluginOptions ): Plugin { - const { config, docsPluginId } = options; + const { + config, + docsPlugin = "@docusaurus/plugin-content-docs", + docsPluginId, + } = options; const { siteDir, siteConfig } = context; // Get routeBasePath and path from plugin-content-docs or preset const presets: any = siteConfig.presets; const plugins: any = siteConfig.plugins; const presetsPlugins = presets.concat(plugins); - let docData: any = getDocsPluginConfig(presetsPlugins, docsPluginId); + let docData: any = getDocsPluginConfig( + presetsPlugins, + docsPlugin, + docsPluginId + ); let docRouteBasePath = docData ? docData.routeBasePath : undefined; let docPath = docData ? (docData.path ? docData.path : "docs") : undefined; @@ -99,6 +119,7 @@ export default function pluginOpenAPIDocs( markdownGenerators, downloadUrl, sidebarOptions, + disableCompression, } = options; // Remove trailing slash before proceeding @@ -106,7 +127,7 @@ export default function pluginOpenAPIDocs( // Override docPath if pluginId provided if (pluginId) { - docData = getDocsPluginConfig(presetsPlugins, pluginId); + docData = getDocsPluginConfig(presetsPlugins, docsPlugin, pluginId); docRouteBasePath = docData ? docData.routeBasePath : undefined; docPath = docData ? (docData.path ? docData.path : "docs") : undefined; } @@ -117,7 +138,7 @@ export default function pluginOpenAPIDocs( try { const openapiFiles = await readOpenapiFiles(contentPath); - const [loadedApi, tags] = await processOpenapiFiles( + const [loadedApi, tags, tagGroups] = await processOpenapiFiles( openapiFiles, options, sidebarOptions! @@ -141,7 +162,8 @@ export default function pluginOpenAPIDocs( options, loadedApi, tags, - docPath + docPath, + tagGroups ); const sidebarSliceTemplate = `module.exports = {{{slice}}};`; @@ -244,25 +266,51 @@ import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; \`\`\` `; + const schemaMdTemplate = `--- +id: {{{id}}} +title: "{{{title}}}" +description: "{{{frontMatter.description}}}" +sidebar_label: "{{{title}}}" +hide_title: true +schema: true +custom_edit_url: null +--- + +{{{markdown}}} + `; + const apiPageGenerator = markdownGenerators?.createApiPageMD ?? createApiPageMD; const infoPageGenerator = markdownGenerators?.createInfoPageMD ?? createInfoPageMD; const tagPageGenerator = markdownGenerators?.createTagPageMD ?? createTagPageMD; + const schemaPageGenerator = + markdownGenerators?.createSchemaPageMD ?? createSchemaPageMD; + + const pageGeneratorByType: { + [key in ApiMetadata["type"]]: ( + pageData: { + api: ApiPageMetadata; + info: InfoPageMetadata; + tag: TagPageMetadata; + schema: SchemaPageMetadata; + }[key] + ) => string; + } = { + api: apiPageGenerator, + info: infoPageGenerator, + tag: tagPageGenerator, + schema: schemaPageGenerator, + }; loadedApi.map(async (item) => { if (item.type === "info") { - if (downloadUrl && isURL(downloadUrl)) { + if (downloadUrl) { item.downloadUrl = downloadUrl; } } - const markdown = - item.type === "api" - ? apiPageGenerator(item) - : item.type === "info" - ? infoPageGenerator(item) - : tagPageGenerator(item); + const markdown = pageGeneratorByType[item.type](item as any); item.markdown = markdown; if (item.type === "api") { // opportunity to compress JSON @@ -272,9 +320,11 @@ import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; // const deserialize = (s: any) => { // return zlib.inflateSync(Buffer.from(s, "base64")).toString(); // }; - item.json = zlib - .deflateSync(JSON.stringify(item.api)) - .toString("base64"); + disableCompression === true + ? (item.json = JSON.stringify(item.api)) + : (item.json = zlib + .deflateSync(JSON.stringify(item.api)) + .toString("base64")); let infoBasePath = `${outputDir}/${item.infoId}`; if (docRouteBasePath) { infoBasePath = `${docRouteBasePath}/${outputDir @@ -363,6 +413,49 @@ import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; } } } + + if (item.type === "schema") { + if (!fs.existsSync(`${outputDir}/schemas/${item.id}.schema.mdx`)) { + if (!fs.existsSync(`${outputDir}/schemas`)) { + try { + fs.mkdirSync(`${outputDir}/schemas`, { recursive: true }); + console.log( + chalk.green(`Successfully created "${outputDir}/schemas"`) + ); + } catch (err) { + console.error( + chalk.red(`Failed to create "${outputDir}/schemas"`), + chalk.yellow(err) + ); + } + } + try { + // kebabCase(arg) returns 0-length string when arg is undefined + if (item.id.length === 0) { + throw Error("Schema must have title defined"); + } + // eslint-disable-next-line testing-library/render-result-naming-convention + const schemaView = render(schemaMdTemplate, item); + fs.writeFileSync( + `${outputDir}/schemas/${item.id}.schema.mdx`, + schemaView, + "utf8" + ); + console.log( + chalk.green( + `Successfully created "${outputDir}/${item.id}.schema.mdx"` + ) + ); + } catch (err) { + console.error( + chalk.red( + `Failed to write "${outputDir}/${item.id}.schema.mdx"` + ), + chalk.yellow(err) + ); + } + } + } return; }); @@ -380,7 +473,11 @@ import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; cwd: path.resolve(apiDir), deep: 1, }); - const sidebarFile = await Globby(["sidebar.js"], { + const schemaMdxFiles = await Globby(["*.schema.mdx"], { + cwd: path.resolve(apiDir, "schemas"), + deep: 1, + }); + const sidebarFile = await Globby(["sidebar.{js,ts}"], { cwd: path.resolve(apiDir), deep: 1, }); @@ -397,6 +494,21 @@ import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; }) ); + schemaMdxFiles.map((mdx) => + fs.unlink(`${apiDir}/schemas/${mdx}`, (err) => { + if (err) { + console.error( + chalk.red(`Cleanup failed for "${apiDir}/schemas/${mdx}"`), + chalk.yellow(err) + ); + } else { + console.log( + chalk.green(`Cleanup succeeded for "${apiDir}/schemas/${mdx}"`) + ); + } + }) + ); + sidebarFile.map((sidebar) => fs.unlink(`${apiDir}/${sidebar}`, (err) => { if (err) { @@ -420,6 +532,7 @@ import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; version: version, label: metadata.label, baseUrl: metadata.baseUrl, + downloadUrl: metadata.downloadUrl, }); } @@ -570,6 +683,7 @@ import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; delete parentConfig.version; delete parentConfig.label; delete parentConfig.baseUrl; + delete parentConfig.downloadUrl; // TODO: handle when no versions are defined by version command is passed if (versionId === "all") { 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 8a1b9c507..9bf878b02 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 @@ -35,13 +35,7 @@ Array [ Array [ -
+
string
  • @@ -57,35 +51,17 @@ Array [
  • -
    +
    boolean
    -
    +
    number
    -
    +
    string
    diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createAuthentication.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createAuthentication.ts index 013764016..fe8012ad9 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createAuthentication.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createAuthentication.ts @@ -5,9 +5,9 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { OAuthFlowObject, SecuritySchemeObject } from "../openapi/types"; import { createDescription } from "./createDescription"; import { create, guard } from "./utils"; +import { OAuthFlowObject, SecuritySchemeObject } from "../openapi/types"; export function createAuthentication(securitySchemes: SecuritySchemeObject) { if (!securitySchemes || !Object.keys(securitySchemes).length) return ""; diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createCallbacks.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createCallbacks.ts new file mode 100644 index 000000000..ec32760e6 --- /dev/null +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createCallbacks.ts @@ -0,0 +1,95 @@ +/* ============================================================================ + * 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 { createDescription } from "./createDescription"; +import { createMethodEndpoint } from "./createMethodEndpoint"; +import { createRequestBodyDetails } from "./createRequestBodyDetails"; +import { createStatusCodes } from "./createStatusCodes"; +import { create } from "./utils"; +import { MediaTypeObject } from "../openapi/types"; +import { ApiItem } from "../types"; + +interface Props { + callbacks: ApiItem["callbacks"]; +} + +interface RequestBodyProps { + title: string; + body: { + content?: { + [key: string]: MediaTypeObject; + }; + description?: string; + required?: boolean; + }; +} + +export function createCallbacks({ callbacks }: Props) { + if (callbacks === undefined) { + return undefined; + } + + const callbacksNames = Object.keys(callbacks); + if (callbacksNames.length === 0) { + return undefined; + } + + return create("div", { + children: [ + create("div", { + className: "openapi__divider", + }), + create("h2", { + children: "Callbacks", + id: "callbacks", + }), + create("OperationTabs", { + className: "openapi-tabs__operation", + children: callbacksNames.flatMap((name) => { + const path = Object.keys(callbacks[name])[0]; + const methods = new Map([ + ["delete", callbacks[name][path].delete], + ["get", callbacks[name][path].get], + ["head", callbacks[name][path].head], + ["options", callbacks[name][path].options], + ["patch", callbacks[name][path].patch], + ["post", callbacks[name][path].post], + ["put", callbacks[name][path].put], + ["trace", callbacks[name][path].trace], + ]); + + return Array.from(methods).flatMap(([method, operationObject]) => { + if (!operationObject) return []; + + const { description, requestBody, responses } = operationObject; + + return [ + create("TabItem", { + label: `${method.toUpperCase()} ${name}`, + value: `${method}-${name}`, + children: [ + createMethodEndpoint(method, path), + // TODO: add `deprecation notice` when markdown support is added + createDescription(description), + createRequestBodyDetails({ + title: "Body", + body: requestBody, + } as RequestBodyProps), + createStatusCodes({ + id: "callbacks-responses", + label: "Callbacks Responses", + responses, + }), + ], + }), + ]; + }); + }), + }), + ], + }); +} diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createContactInfo.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createContactInfo.ts index 1a6d772db..fd927200c 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createContactInfo.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createContactInfo.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { ContactObject } from "../openapi/types"; import { create, guard } from "./utils"; +import { ContactObject } from "../openapi/types"; export function createContactInfo(contact: ContactObject) { if (!contact || !Object.keys(contact).length) return ""; diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createLicense.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createLicense.ts index c1d981dbc..86cc17780 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createLicense.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createLicense.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { LicenseObject } from "../openapi/types"; import { create, guard } from "./utils"; +import { LicenseObject } from "../openapi/types"; export function createLicense(license: LicenseObject) { if (!license || !Object.keys(license).length) return ""; diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createLogo.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createLogo.ts index 8e9006bb0..090255985 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createLogo.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createLogo.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { LogoObject } from "../openapi/types"; import { create, guard } from "./utils"; +import { LogoObject } from "../openapi/types"; export function createLogo( logo: LogoObject | undefined, diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createParamsDetails.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createParamsDetails.ts index 9c5b1db49..f3415e286 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createParamsDetails.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createParamsDetails.ts @@ -5,10 +5,10 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { ApiItem } from "../types"; import { createDetails } from "./createDetails"; import { createDetailsSummary } from "./createDetailsSummary"; import { create } from "./utils"; +import { ApiItem } from "../types"; interface Props { parameters: ApiItem["parameters"]; diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createRequestBodyDetails.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createRequestBodyDetails.ts index 47aa0b861..19cae0633 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createRequestBodyDetails.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createRequestBodyDetails.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { MediaTypeObject } from "../openapi/types"; import { createRequestSchema } from "./createRequestSchema"; +import { MediaTypeObject } from "../openapi/types"; interface Props { title: string; @@ -19,6 +19,6 @@ interface Props { }; } -export function createRequestBodyDetails({ title, body }: Props): any { +export function createRequestBodyDetails({ title, body }: Props) { return createRequestSchema({ title, body }); } diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createRequestSchema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createRequestSchema.ts index f22726264..6b7805a90 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createRequestSchema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createRequestSchema.ts @@ -5,12 +5,12 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { MediaTypeObject } from "../openapi/types"; import { createDescription } from "./createDescription"; import { createDetails } from "./createDetails"; import { createDetailsSummary } from "./createDetailsSummary"; import { createNodes } from "./createSchema"; import { create, guard } from "./utils"; +import { MediaTypeObject } from "../openapi/types"; interface Props { style?: any; @@ -90,7 +90,7 @@ export function createRequestSchema({ title, body, ...rest }: Props) { }), create("ul", { style: { marginLeft: "1rem" }, - children: createNodes(firstBody), + children: createNodes(firstBody, "request"), }), ], }), @@ -108,12 +108,6 @@ export function createRequestSchema({ title, body, ...rest }: Props) { return undefined; } - // we don't show the table if there is no properties to show - if (firstBody.properties !== undefined) { - if (Object.keys(firstBody.properties).length === 0) { - return undefined; - } - } return create("MimeTabs", { className: "openapi-tabs__mime", children: [ @@ -161,7 +155,7 @@ export function createRequestSchema({ title, body, ...rest }: Props) { }), create("ul", { style: { marginLeft: "1rem" }, - children: createNodes(firstBody), + children: createNodes(firstBody, "request"), }), ], }), diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createResponseSchema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createResponseSchema.ts index 378ba5159..7ce159f2a 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createResponseSchema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createResponseSchema.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { MediaTypeObject } from "../openapi/types"; import { createDescription } from "./createDescription"; import { createDetails } from "./createDetails"; import { createDetailsSummary } from "./createDetailsSummary"; @@ -16,6 +15,7 @@ import { createResponseExamples, } from "./createStatusCodes"; import { create, guard } from "./utils"; +import { MediaTypeObject } from "../openapi/types"; interface Props { style?: any; @@ -60,12 +60,6 @@ export function createResponseSchema({ title, body, ...rest }: Props) { return undefined; } - if (firstBody?.properties !== undefined) { - if (Object.keys(firstBody?.properties).length === 0) { - return undefined; - } - } - return create("TabItem", { label: `${mimeType}`, value: `${mimeType}`, @@ -118,7 +112,7 @@ export function createResponseSchema({ title, body, ...rest }: Props) { }), create("ul", { style: { marginLeft: "1rem" }, - children: createNodes(firstBody!), + children: createNodes(firstBody!, "response"), }), ], }), diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.test.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.test.ts index 0232ff4ce..5299210e9 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.test.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.test.ts @@ -7,12 +7,13 @@ import * as prettier from "prettier"; -import { SchemaObject } from "../openapi/types"; import { createNodes } from "./createSchema"; +import { SchemaObject } from "../openapi/types"; describe("createNodes", () => { it("should create readable MODs for oneOf primitive properties", () => { const schema: SchemaObject = { + "x-tags": ["clown"], type: "object", properties: { oneOfProperty: { @@ -48,7 +49,7 @@ describe("createNodes", () => { }, }; expect( - createNodes(schema).map((md: any) => + createNodes(schema, "request").map((md: any) => prettier.format(md, { parser: "babel" }) ) ).toMatchSnapshot(); diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts index 1c6d34f4b..7331ed3c2 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts @@ -7,7 +7,6 @@ import clsx from "clsx"; -import { SchemaObject } from "../openapi/types"; import { createClosingArrayBracket, createOpeningArrayBracket, @@ -17,9 +16,12 @@ import { createDetails } from "./createDetails"; import { createDetailsSummary } from "./createDetailsSummary"; import { getQualifierMessage, getSchemaName } from "./schema"; import { create, guard } from "./utils"; +import { SchemaObject } from "../openapi/types"; const jsonSchemaMergeAllOf = require("json-schema-merge-allof"); +let SCHEMA_TYPE: "request" | "response"; + /** * Returns a merged representation of allOf array of schemas. */ @@ -29,6 +31,9 @@ export function mergeAllOf(allOf: SchemaObject[]) { readOnly: function () { return true; }, + writeOnly: function () { + return true; + }, example: function () { return true; }, @@ -39,7 +44,7 @@ export function mergeAllOf(allOf: SchemaObject[]) { ignoreAdditionalProperties: true, }); - const required = allOf.reduce((acc, cur) => { + const mergedRequired = allOf.reduce((acc, cur) => { if (Array.isArray(cur.required)) { const next = [...acc, ...cur.required]; return next; @@ -47,7 +52,7 @@ export function mergeAllOf(allOf: SchemaObject[]) { return acc; }, [] as any); - return { mergedSchemas, required }; + return { mergedSchemas, mergedRequired }; } /** @@ -74,7 +79,7 @@ function createAnyOneOf(schema: SchemaObject): any { } if (anyOneSchema.allOf !== undefined) { - anyOneChildren.push(createNodes(anyOneSchema)); + anyOneChildren.push(createNodes(anyOneSchema, SCHEMA_TYPE)); delete anyOneSchema.allOf; } @@ -89,7 +94,7 @@ function createAnyOneOf(schema: SchemaObject): any { anyOneSchema.type === "integer" || anyOneSchema.type === "boolean" ) { - anyOneChildren.push(createNodes(anyOneSchema)); + anyOneChildren.push(createNodes(anyOneSchema, SCHEMA_TYPE)); } if (anyOneChildren.length) { if (schema.type === "array") { @@ -124,6 +129,16 @@ function createAnyOneOf(schema: SchemaObject): any { */ function createProperties(schema: SchemaObject) { const discriminator = schema.discriminator; + if (Object.keys(schema.properties!).length === 0) { + return create("SchemaItem", { + collapsible: false, + name: "", + required: false, + schemaName: "object", + qualifierMessage: undefined, + schema: {}, + }); + } return Object.entries(schema.properties!).map(([key, val]) => { return createEdges({ name: key, @@ -255,9 +270,8 @@ function createItems(schema: SchemaObject) { // TODO: figure out if and how we should pass merged required array const { mergedSchemas, - }: { mergedSchemas: SchemaObject; required: string[] } = mergeAllOf( - schema.items?.allOf - ); + }: { mergedSchemas: SchemaObject; mergedRequired: string[] | boolean } = + mergeAllOf(schema.items?.allOf); // Handles combo anyOf/oneOf + properties if ( @@ -304,7 +318,7 @@ function createItems(schema: SchemaObject) { ) { return [ createOpeningArrayBracket(), - createNodes(schema.items), + createNodes(schema.items, SCHEMA_TYPE), createClosingArrayBracket(), ].flat(); } @@ -411,7 +425,7 @@ function createDetailsNode( children: createDescription(description), }) ), - createNodes(schema), + createNodes(schema, SCHEMA_TYPE), ], }), ], @@ -565,7 +579,7 @@ function createPropertyDiscriminator( // className: "openapi-tabs__discriminator-item", label: label, value: `${index}-item-discriminator`, - children: [createNodes(discriminator?.mapping[key])], + children: [createNodes(discriminator?.mapping[key], SCHEMA_TYPE)], }); }), }), @@ -590,8 +604,19 @@ function createEdges({ required, discriminator, }: EdgeProps): any { - const schemaName = getSchemaName(schema); + if (SCHEMA_TYPE === "request") { + if (schema.readOnly && schema.readOnly === true) { + return undefined; + } + } + if (SCHEMA_TYPE === "response") { + if (schema.writeOnly && schema.writeOnly === true) { + return undefined; + } + } + + const schemaName = getSchemaName(schema); if (discriminator !== undefined && discriminator.propertyName === name) { return createPropertyDiscriminator( name, @@ -613,13 +638,23 @@ function createEdges({ } if (schema.allOf !== undefined) { - const { - mergedSchemas, - required, - }: { mergedSchemas: SchemaObject; required: string[] | boolean } = - mergeAllOf(schema.allOf); - const mergedSchemaName = getSchemaName(mergedSchemas); + const { mergedSchemas }: { mergedSchemas: SchemaObject } = mergeAllOf( + schema.allOf + ); + if (SCHEMA_TYPE === "request") { + if (mergedSchemas.readOnly && mergedSchemas.readOnly === true) { + return undefined; + } + } + + if (SCHEMA_TYPE === "response") { + if (mergedSchemas.writeOnly && mergedSchemas.writeOnly === true) { + return undefined; + } + } + + const mergedSchemaName = getSchemaName(mergedSchemas); if ( mergedSchemas.oneOf !== undefined || mergedSchemas.anyOf !== undefined @@ -664,16 +699,12 @@ function createEdges({ ); } - if (mergedSchemas.readOnly && mergedSchemas.readOnly === true) { - return undefined; - } - return create("SchemaItem", { collapsible: false, name, required: Array.isArray(required) ? required.includes(name) : required, - schemaName: schemaName, - qualifierMessage: getQualifierMessage(schema), + schemaName: mergedSchemaName, + qualifierMessage: getQualifierMessage(mergedSchemas), schema: mergedSchemas, }); } @@ -719,10 +750,6 @@ function createEdges({ ); } - if (schema.readOnly && schema.readOnly === true) { - return undefined; - } - // primitives and array of non-objects return create("SchemaItem", { collapsible: false, @@ -737,7 +764,22 @@ function createEdges({ /** * Creates a hierarchical level of a schema tree. Nodes produce edges that can branch into sub-nodes with edges, recursively. */ -export function createNodes(schema: SchemaObject): any { +export function createNodes( + schema: SchemaObject, + schemaType: "request" | "response" +): any { + SCHEMA_TYPE = schemaType; + if (SCHEMA_TYPE === "request") { + if (schema.readOnly && schema.readOnly === true) { + return undefined; + } + } + + if (SCHEMA_TYPE === "response") { + if (schema.writeOnly && schema.writeOnly === true) { + return undefined; + } + } const nodes = []; // if (schema.discriminator !== undefined) { // return createDiscriminator(schema); @@ -792,9 +834,18 @@ export function createNodes(schema: SchemaObject): any { style: { marginTop: ".5rem", marginBottom: ".5rem", - marginLeft: "1rem", }, - children: createDescription(schema.type), + children: [ + createDescription(schema.type), + guard(getQualifierMessage(schema), (message) => + create("div", { + style: { + paddingTop: "1rem", + }, + children: createDescription(message), + }) + ), + ], }); } @@ -804,9 +855,18 @@ export function createNodes(schema: SchemaObject): any { style: { marginTop: ".5rem", marginBottom: ".5rem", - marginLeft: "1rem", }, - children: [createDescription(schema)], + children: [ + createDescription(schema), + guard(getQualifierMessage(schema), (message) => + create("div", { + style: { + paddingTop: "1rem", + }, + children: createDescription(message), + }) + ), + ], }); } diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createStatusCodes.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createStatusCodes.ts index c7376f712..2a99c1272 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createStatusCodes.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createStatusCodes.ts @@ -7,14 +7,14 @@ import format from "xml-formatter"; -import { sampleResponseFromSchema } from "../openapi/createResponseExample"; -import { ApiItem } from "../types"; import { createDescription } from "./createDescription"; import { createDetails } from "./createDetails"; import { createDetailsSummary } from "./createDetailsSummary"; import { createResponseSchema } from "./createResponseSchema"; import { create } from "./utils"; import { guard } from "./utils"; +import { sampleResponseFromSchema } from "../openapi/createResponseExample"; +import { ApiItem } from "../types"; export default function json2xml(o: any, tab: any) { var toXml = function (v: any, name: string, ind: any) { @@ -54,6 +54,8 @@ export default function json2xml(o: any, tab: any) { } interface Props { + id?: string; + label?: string; responses: ApiItem["responses"]; } @@ -125,7 +127,7 @@ export function createResponseExamples( value: `${exampleName}`, children: [ guard(exampleValue.summary, (summary) => [ - create("p", { + create("Markdown", { children: ` ${summary}`, }), ]), @@ -141,7 +143,7 @@ export function createResponseExamples( value: `${exampleName}`, children: [ guard(exampleValue.summary, (summary) => [ - create("p", { + create("Markdown", { children: ` ${summary}`, }), ]), @@ -169,7 +171,7 @@ export function createResponseExample(responseExample: any, mimeType: string) { value: `Example`, children: [ guard(responseExample.summary, (summary) => [ - create("p", { + create("Markdown", { children: ` ${summary}`, }), ]), @@ -185,7 +187,7 @@ export function createResponseExample(responseExample: any, mimeType: string) { value: `Example`, children: [ guard(responseExample.summary, (summary) => [ - create("p", { + create("Markdown", { children: ` ${summary}`, }), ]), @@ -254,7 +256,7 @@ export function createExampleFromSchema(schema: any, mimeType: string) { return undefined; } -export function createStatusCodes({ responses }: Props) { +export function createStatusCodes({ label, id, responses }: Props) { if (responses === undefined) { return undefined; } @@ -269,6 +271,8 @@ export function createStatusCodes({ responses }: Props) { create("div", { children: [ create("ApiTabs", { + label, + id, children: codes.map((code) => { const responseHeaders: any = responses[code].headers; return create("TabItem", { @@ -281,7 +285,7 @@ export function createStatusCodes({ responses }: Props) { responseHeaders && createDetails({ className: "openapi-markdown__details", - "data-collaposed": true, + "data-collapsed": true, open: false, style: { textAlign: "left", marginBottom: "1rem" }, children: [ diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/index.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/index.ts index 4b6d4bd59..533af429c 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/index.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/index.ts @@ -5,15 +5,9 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { - ContactObject, - LicenseObject, - MediaTypeObject, - SecuritySchemeObject, -} from "../openapi/types"; -import { ApiPageMetadata, InfoPageMetadata, TagPageMetadata } from "../types"; import { createAuthentication } from "./createAuthentication"; import { createAuthorization } from "./createAuthorization"; +import { createCallbacks } from "./createCallbacks"; import { createContactInfo } from "./createContactInfo"; import { createDeprecationNotice } from "./createDeprecationNotice"; import { createDescription } from "./createDescription"; @@ -25,13 +19,26 @@ import { createMethodEndpoint } from "./createMethodEndpoint"; import { createParamsDetails } from "./createParamsDetails"; import { createRequestBodyDetails } from "./createRequestBodyDetails"; import { createRequestHeader } from "./createRequestHeader"; +import { createNodes } from "./createSchema"; import { createStatusCodes } from "./createStatusCodes"; import { createTermsOfService } from "./createTermsOfService"; import { createVendorExtensions } from "./createVendorExtensions"; import { createVersionBadge } from "./createVersionBadge"; -import { greaterThan, lessThan, render } from "./utils"; +import { create, greaterThan, lessThan, render } from "./utils"; +import { + ContactObject, + LicenseObject, + MediaTypeObject, + SecuritySchemeObject, +} from "../openapi/types"; +import { + ApiPageMetadata, + InfoPageMetadata, + SchemaPageMetadata, + TagPageMetadata, +} from "../types"; -interface Props { +interface RequestBodyProps { title: string; body: { content?: { @@ -54,6 +61,7 @@ export function createApiPageMD({ parameters, requestBody, responses, + callbacks, }, infoPath, frontMatter, @@ -68,14 +76,18 @@ export function createApiPageMD({ `import ResponseSamples from "@theme/ResponseSamples";\n`, `import SchemaItem from "@theme/SchemaItem";\n`, `import SchemaTabs from "@theme/SchemaTabs";\n`, + `import Markdown from "@theme/Markdown";\n`, + `import OperationTabs from "@theme/OperationTabs";\n`, `import TabItem from "@theme/TabItem";\n\n`, createHeading(title.replace(lessThan, "<").replace(greaterThan, ">")), createMethodEndpoint(method, path), infoPath && createAuthorization(infoPath), - frontMatter.show_extensions && createVendorExtensions(extensions), + frontMatter.show_extensions + ? createVendorExtensions(extensions) + : undefined, createDeprecationNotice({ deprecated, description: deprecatedDescription }), createDescription(description), - createRequestHeader("Request"), + requestBody || parameters ? createRequestHeader("Request") : undefined, createParamsDetails({ parameters, type: "path" }), createParamsDetails({ parameters, type: "query" }), createParamsDetails({ parameters, type: "header" }), @@ -83,8 +95,9 @@ export function createApiPageMD({ createRequestBodyDetails({ title: "Body", body: requestBody, - } as Props), + } as RequestBodyProps), createStatusCodes({ responses }), + createCallbacks({ callbacks }), ]); } @@ -123,3 +136,18 @@ export function createInfoPageMD({ export function createTagPageMD({ tag: { description } }: TagPageMetadata) { return render([createDescription(description)]); } + +export function createSchemaPageMD({ schema }: SchemaPageMetadata) { + const { title = "", description } = schema; + return render([ + `import DiscriminatorTabs from "@theme/DiscriminatorTabs";\n`, + `import SchemaItem from "@theme/SchemaItem";\n`, + `import SchemaTabs from "@theme/SchemaTabs";\n`, + `import TabItem from "@theme/TabItem";\n\n`, + createHeading(title.replace(lessThan, "<").replace(greaterThan, ">")), + createDescription(description), + create("ul", { + children: createNodes(schema, "response"), + }), + ]); +} diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts index be09c11cd..5ac9c92b9 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/schema.ts @@ -40,6 +40,10 @@ function prettyName(schema: SchemaObject, circular?: boolean) { // return schema.type; } + if (schema.title && schema.type) { + return `${schema.title} (${schema.type})`; + } + return schema.title ?? schema.type; } diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/utils.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/utils.ts index bc8b209a8..ea37fb69b 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/utils.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/utils.ts @@ -24,7 +24,7 @@ export function guard( value: T | undefined, cb: (value: T) => Children ): string { - if (!!value) { + if (!!value || value === 0) { const children = cb(value); return render(children); } @@ -47,3 +47,25 @@ export const lessThan = export const greaterThan = /(?/gu; export const codeFence = /`{1,3}[\s\S]*?`{1,3}/g; +export const curlyBrackets = /([{}])/g; +export const codeBlock = /(^```.*[\s\S]*?```$|`[^`].+?`)/gm; + +export function clean(value: string | undefined): string { + if (!value) { + return ""; + } + + let sections = value.split(codeBlock); + for (let sectionIndex in sections) { + if (!sections[sectionIndex].startsWith("`")) { + sections[sectionIndex] = sections[sectionIndex] + .replace(lessThan, "<") + .replace(greaterThan, ">") + .replace(codeFence, function (match) { + return match.replace(/\\>/g, ">"); + }) + .replace(curlyBrackets, "\\$1"); + } + } + return sections.join(""); +} diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi-to-postmanv2.d.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi-to-postmanv2.d.ts index f39042f25..a5bad23e0 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi-to-postmanv2.d.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi-to-postmanv2.d.ts @@ -5,6 +5,6 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -declare module "@paloaltonetworks/openapi-to-postmanv2" { +declare module "openapi-to-postmanv2" { export default any; } diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/openapi.yaml b/packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/openapi.yaml index 03a7005ec..6c9fa2f8d 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/openapi.yaml +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/__fixtures__/examples/openapi.yaml @@ -11,3 +11,39 @@ paths: responses: 200: description: OK + +tags: + - name: tag1 + description: Everything about your Pets + x-displayName: Tag 1 + - name: tag2 + description: Tag 2 description + x-displayName: Tag 2 + - name: tag3 + description: Tag 3 description + x-displayName: Tag 3 + - name: tag4 + description: Tag 4 description + x-displayName: Tag 4 + +x-tagGroups: + - name: Tag 1 & 2 + tags: + - tag1 + - tag2 + - name: Trinity + tags: + - tag1 + - tag2 + - tag3 + - name: Last Two + tags: + - tag3 + - tag4 + +components: + schemas: + HelloString: + x-tags: + - tag1 + type: string diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts index 8413cb8ea..4ec427383 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts @@ -8,8 +8,8 @@ import chalk from "chalk"; import merge from "lodash/merge"; -import { mergeAllOf } from "../markdown/createSchema"; import { SchemaObject } from "./types"; +import { mergeAllOf } from "../markdown/createSchema"; interface OASTypeToTypeMap { string: string; diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts index 11a287e8b..7b0492dab 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts @@ -8,8 +8,8 @@ import chalk from "chalk"; import merge from "lodash/merge"; -import { mergeAllOf } from "../markdown/createSchema"; import { SchemaObject } from "./types"; +import { mergeAllOf } from "../markdown/createSchema"; interface OASTypeToTypeMap { string: string; diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.test.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.test.ts index c1c412f0b..678a003ff 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.test.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.test.ts @@ -28,6 +28,13 @@ describe("openapi", () => { const yaml = results.find((x) => x.source.endsWith("openapi.yaml")); expect(yaml).toBeTruthy(); expect(yaml?.sourceDirName).toBe("."); + + expect(yaml?.data.tags).toBeDefined(); + expect(yaml?.data["x-tagGroups"]).toBeDefined(); + + expect( + yaml?.data.components?.schemas?.HelloString["x-tags"] + ).toBeDefined(); }); }); }); diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.ts index 99bc85a35..abba4b17b 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.ts @@ -8,28 +8,29 @@ import path from "path"; import { Globby, GlobExcludeDefault, posixPath } from "@docusaurus/utils"; -import Converter from "@paloaltonetworks/openapi-to-postmanv2"; -import sdk from "@paloaltonetworks/postman-collection"; -import Collection from "@paloaltonetworks/postman-collection"; import chalk from "chalk"; import fs from "fs-extra"; import cloneDeep from "lodash/cloneDeep"; import kebabCase from "lodash/kebabCase"; import unionBy from "lodash/unionBy"; import uniq from "lodash/uniq"; +import Converter from "openapi-to-postmanv2"; +import Collection from "postman-collection"; +import sdk from "postman-collection"; +import { sampleRequestFromSchema } from "./createRequestExample"; +import { OpenApiObject, TagGroupObject, TagObject } from "./types"; +import { loadAndResolveSpec } from "./utils/loadAndResolveSpec"; import { isURL } from "../index"; import { ApiMetadata, APIOptions, ApiPageMetadata, InfoPageMetadata, + SchemaPageMetadata, SidebarOptions, TagPageMetadata, } from "../types"; -import { sampleRequestFromSchema } from "./createRequestExample"; -import { OpenApiObject, TagObject } from "./types"; -import { loadAndResolveSpec } from "./utils/loadAndResolveSpec"; /** * Convenience function for converting raw JSON to a Postman Collection object. @@ -409,6 +410,56 @@ function createItems( } } + if ( + options?.showSchemas === true || + Object.entries(openapiData?.components?.schemas ?? {}) + .flatMap(([_, s]) => s["x-tags"]) + .filter((item) => !!item).length > 0 + ) { + // Gather schemas + for (let [schema, schemaObject] of Object.entries( + openapiData?.components?.schemas ?? {} + )) { + if (options?.showSchemas === true || schemaObject["x-tags"]) { + const baseIdSpaces = + schemaObject?.title?.replace(" ", "-").toLowerCase() ?? ""; + const baseId = kebabCase(baseIdSpaces); + + const schemaDescription = schemaObject.description; + let splitDescription: any; + if (schemaDescription) { + splitDescription = schemaDescription.match(/[^\r\n]+/g); + } + + const schemaPage: PartialPage = { + type: "schema", + id: baseId, + infoId: infoId ?? "", + unversionedId: baseId, + title: schemaObject.title + ? schemaObject.title.replace(/((?:^|[^\\])(?:\\{2})*)"/g, "$1'") + : schema, + description: schemaObject.description + ? schemaObject.description.replace( + /((?:^|[^\\])(?:\\{2})*)"/g, + "$1'" + ) + : "", + frontMatter: { + description: splitDescription + ? splitDescription[0] + .replace(/((?:^|[^\\])(?:\\{2})*)"/g, "$1'") + .replace(/\s+$/, "") + : "", + }, + schema: schemaObject, + }; + + items.push(schemaPage); + } + } + } + if (sidebarOptions?.categoryLinkSource === "tag") { // Get global tags const tags: TagObject[] = openapiData.tags ?? []; @@ -469,10 +520,13 @@ function bindCollectionToApiItems( const method = item.request.method.toLowerCase(); const path = item.request.url .getPath({ unresolved: true }) // unresolved returns "/:variableName" instead of "/" - .replace(/:([a-z0-9-_]+)/gi, "{$1}"); // replace "/:variableName" with "/{variableName}" - + .replace(/(? { - if (item.type === "info" || item.type === "tag") { + if ( + item.type === "info" || + item.type === "tag" || + item.type === "schema" + ) { return false; } return item.api.path === path && item.api.method === method; @@ -535,7 +589,7 @@ export async function processOpenapiFiles( files: OpenApiFiles[], options: APIOptions, sidebarOptions: SidebarOptions -): Promise<[ApiMetadata[], TagObject[][]]> { +): Promise<[ApiMetadata[], TagObject[][], TagGroupObject[]]> { const promises = files.map(async (file) => { if (file.data !== undefined) { const processedFile = await processOpenapiFile( @@ -547,7 +601,8 @@ export async function processOpenapiFiles( ...item, })); const tags = processedFile[1]; - return [itemsObjectsArray, tags]; + const tagGroups = processedFile[2]; + return [itemsObjectsArray, tags, tagGroups]; } console.warn( chalk.yellow( @@ -566,6 +621,7 @@ export async function processOpenapiFiles( // Remove undefined items due to transient parsing errors return x !== undefined; }); + const tags = metadata .map(function (x) { return x[1]; @@ -574,14 +630,29 @@ export async function processOpenapiFiles( // Remove undefined tags due to transient parsing errors return x !== undefined; }); - return [items as ApiMetadata[], tags as TagObject[][]]; + + const tagGroups = metadata + .map(function (x) { + return x[2]; + }) + .flat() + .filter(function (x) { + // Remove undefined tags due to transient parsing errors + return x !== undefined; + }); + + return [ + items as ApiMetadata[], + tags as TagObject[][], + tagGroups as TagGroupObject[], + ]; } export async function processOpenapiFile( openapiData: OpenApiObject, options: APIOptions, sidebarOptions: SidebarOptions -): Promise<[ApiMetadata[], TagObject[]]> { +): Promise<[ApiMetadata[], TagObject[], TagGroupObject[]]> { const postmanCollection = await createPostmanCollection(openapiData); const items = createItems(openapiData, options, sidebarOptions); @@ -591,7 +662,13 @@ export async function processOpenapiFile( if (openapiData.tags !== undefined) { tags = openapiData.tags; } - return [items, tags]; + + let tagGroups: TagGroupObject[] = []; + if (openapiData["x-tagGroups"] !== undefined) { + tagGroups = openapiData["x-tagGroups"]; + } + + return [items, tags, tagGroups]; } // order for picking items as a display name of tags diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/types.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/types.ts index 72e6468d4..b9e45c48a 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/types.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/types.ts @@ -22,6 +22,7 @@ export interface OpenApiObject { externalDocs?: ExternalDocumentationObject; swagger?: string; "x-webhooks"?: PathsObject; + "x-tagGroups"?: TagGroupObject[]; } export interface OpenApiObjectWithRef { @@ -311,6 +312,11 @@ export interface TagObject { "x-displayName"?: string; } +export interface TagGroupObject { + name: string; + tags: string[]; +} + export interface ReferenceObject { $ref: string; } @@ -346,6 +352,7 @@ export type SchemaObject = Omit< externalDocs?: ExternalDocumentationObject; example?: any; deprecated?: boolean; + "x-tags"?: string[]; }; export type SchemaObjectWithRef = Omit< 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..2b75c3844 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/loadAndResolveSpec.ts @@ -13,8 +13,8 @@ import chalk from "chalk"; // @ts-ignore import { convertObj } from "swagger2openapi"; -import { OpenApiObject } from "../types"; import { OpenAPIParser } from "./services/OpenAPIParser"; +import { OpenApiObject } from "../types"; function serializer(replacer: any, cycleReplacer: any) { var stack: any = [], diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/services/OpenAPIParser.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/services/OpenAPIParser.ts index 8f18c8b30..3e90a75af 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/services/OpenAPIParser.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/services/OpenAPIParser.ts @@ -7,11 +7,11 @@ // @ts-nocheck +import { RedocNormalizedOptions } from "./RedocNormalizedOptions"; import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from "../types"; import { isArray, isBoolean } from "../utils/helpers"; import { JsonPointer } from "../utils/JsonPointer"; import { getDefinitionName, isNamedDefinition } from "../utils/openapi"; -import { RedocNormalizedOptions } from "./RedocNormalizedOptions"; export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] }; diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/utils/openapi.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/utils/openapi.ts index 2fbd16d00..241581485 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/utils/openapi.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/utils/utils/openapi.ts @@ -9,6 +9,13 @@ import { dirname } from "path"; +import { + isNumeric, + removeQueryString, + resolveUrl, + isArray, + isBoolean, +} from "./helpers"; import { OpenAPIParser } from "../services/OpenAPIParser"; import { OpenAPIEncoding, @@ -21,13 +28,6 @@ import { OpenAPIServer, Referenced, } from "../types"; -import { - isNumeric, - removeQueryString, - resolveUrl, - isArray, - isBoolean, -} from "./helpers"; function isWildcardStatusCode( statusCode: string | number diff --git a/packages/docusaurus-plugin-openapi-docs/src/options.ts b/packages/docusaurus-plugin-openapi-docs/src/options.ts index d2a56805d..e3f11ddec 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/options.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/options.ts @@ -8,7 +8,7 @@ import { Joi } from "@docusaurus/utils-validation"; const sidebarOptions = Joi.object({ - groupPathsBy: Joi.string().valid("tag"), + groupPathsBy: Joi.string().valid("tag", "tagGroup"), categoryLinkSource: Joi.string().valid("tag", "info", "auto"), customProps: Joi.object(), sidebarCollapsible: Joi.boolean(), @@ -23,6 +23,7 @@ const markdownGenerators = Joi.object({ export const OptionsSchema = Joi.object({ id: Joi.string().required(), + docsPlugin: Joi.string(), docsPluginId: Joi.string().required(), config: Joi.object() .pattern( @@ -37,6 +38,8 @@ export const OptionsSchema = Joi.object({ showExtensions: Joi.boolean(), sidebarOptions: sidebarOptions, markdownGenerators: markdownGenerators, + showSchemas: Joi.boolean(), + disableCompression: Joi.boolean(), version: Joi.string().when("versions", { is: Joi.exist(), then: Joi.required(), @@ -56,6 +59,7 @@ export const OptionsSchema = Joi.object({ outputDir: Joi.string().required(), label: Joi.string().required(), baseUrl: Joi.string().required(), + downloadUrl: Joi.string(), }) ), }) diff --git a/packages/docusaurus-plugin-openapi-docs/src/postman-collection.d.ts b/packages/docusaurus-plugin-openapi-docs/src/postman-collection.d.ts index 9084c02e9..1e002ccd4 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/postman-collection.d.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/postman-collection.d.ts @@ -5,6 +5,6 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -declare module "@paloaltonetworks/postman-collection" { +declare module "postman-collection" { export default any; } diff --git a/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.ts b/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.ts index 285415ead..def167105 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.ts @@ -7,6 +7,7 @@ import path from "path"; +import { ProcessedSidebarItem } from "@docusaurus/plugin-content-docs/lib/sidebars/types"; import { ProcessedSidebar, SidebarItemCategory, @@ -18,12 +19,14 @@ import clsx from "clsx"; import { kebabCase } from "lodash"; import uniq from "lodash/uniq"; -import { TagObject } from "../openapi/types"; +import { TagGroupObject, TagObject } from "../openapi/types"; import type { SidebarOptions, APIOptions, ApiPageMetadata, ApiMetadata, + InfoPageMetadata, + SchemaPageMetadata, } from "../types"; function isApiItem(item: ApiMetadata): item is ApiMetadata { @@ -34,14 +37,18 @@ function isInfoItem(item: ApiMetadata): item is ApiMetadata { return item.type === "info"; } +function isSchemaItem(item: ApiMetadata): item is ApiMetadata { + return item.type === "schema"; +} + function groupByTags( - items: ApiPageMetadata[], + items: ApiMetadata[], sidebarOptions: SidebarOptions, options: APIOptions, tags: TagObject[][], docPath: string ): ProcessedSidebar { - let { outputDir, label } = options; + let { outputDir, label, showSchemas } = options; // Remove trailing slash before proceeding outputDir = outputDir.replace(/\/$/, ""); @@ -53,8 +60,9 @@ function groupByTags( categoryLinkSource, } = sidebarOptions; - const apiItems = items.filter(isApiItem); - const infoItems = items.filter(isInfoItem); + const apiItems = items.filter(isApiItem) as ApiPageMetadata[]; + const infoItems = items.filter(isInfoItem) as InfoPageMetadata[]; + const schemaItems = items.filter(isSchemaItem) as SchemaPageMetadata[]; const intros = infoItems.map((item: any) => { return { id: item.id, @@ -70,38 +78,56 @@ function groupByTags( .flatMap((item) => item.api.tags) .filter((item): item is string => !!item) ); + const schemaTags = uniq( + schemaItems + .flatMap((item) => item.schema["x-tags"]) + .filter((item): item is string => !!item) + ); - // Combine globally defined tags with operation tags - // Only include global tag if referenced in operation tags + // Combine globally defined tags with operation and schema tags + // Only include global tag if referenced in operation/schema tags let apiTags: string[] = []; tags.flat().forEach((tag) => { // Should we also check x-displayName? - if (operationTags.includes(tag.name!)) { + if (operationTags.includes(tag.name!) || schemaTags.includes(tag.name!)) { apiTags.push(tag.name!); } }); - apiTags = uniq(apiTags.concat(operationTags)); + + if (sidebarOptions.groupPathsBy !== "tagGroup") { + apiTags = uniq(apiTags.concat(operationTags, schemaTags)); + } const basePath = docPath ? outputDir.split(docPath!)[1].replace(/^\/+/g, "") : outputDir.slice(outputDir.indexOf("/", 1)).replace(/^\/+/g, ""); - function createDocItem(item: ApiPageMetadata): SidebarItemDoc { + function createDocItem( + item: ApiPageMetadata | SchemaPageMetadata + ): SidebarItemDoc { const sidebar_label = item.frontMatter.sidebar_label; const title = item.title; - const id = item.id; + const id = item.type === "schema" ? `schemas/${item.id}` : item.id; + const className = + item.type === "api" + ? clsx( + { + "menu__list-item--deprecated": item.api.deprecated, + "api-method": !!item.api.method, + }, + item.api.method + ) + : clsx( + { + "menu__list-item--deprecated": item.schema.deprecated, + }, + "schema" + ); return { type: "doc" as const, - id: - basePath === "" || undefined ? `${item.id}` : `${basePath}/${item.id}`, + id: basePath === "" || undefined ? `${id}` : `${basePath}/${id}`, label: (sidebar_label as string) ?? title ?? id, customProps: customProps, - className: clsx( - { - "menu__list-item--deprecated": item.api.deprecated, - "api-method": !!item.api.method, - }, - item.api.method - ), + className: className ? className : undefined, }; } @@ -169,15 +195,20 @@ function groupByTags( } as SidebarItemCategoryLinkConfig; } + const taggedApiItems = apiItems.filter( + (item) => !!item.api.tags?.includes(tag) + ); + const taggedSchemaItems = schemaItems.filter( + (item) => !!item.schema["x-tags"]?.includes(tag) + ); + return { type: "category" as const, label: tagObject?.["x-displayName"] ?? tag, link: linkConfig, collapsible: sidebarCollapsible, collapsed: sidebarCollapsed, - items: apiItems - .filter((item) => !!item.api.tags?.includes(tag)) - .map(createDocItem), + items: [...taggedSchemaItems, ...taggedApiItems].map(createDocItem), }; }) .filter((item) => item.items.length > 0); // Filter out any categories with no items. @@ -201,13 +232,28 @@ function groupByTags( ]; } + let schemas: SidebarItemCategory[] = []; + if (showSchemas && schemaItems.length > 0) { + schemas = [ + { + type: "category" as const, + label: "Schemas", + collapsible: sidebarCollapsible!, + collapsed: sidebarCollapsed!, + items: schemaItems + .filter(({ schema }) => !schema["x-tags"]) + .map(createDocItem), + }, + ]; + } + // Shift root intro doc to top of sidebar // TODO: Add input validation for categoryLinkSource options if (rootIntroDoc && categoryLinkSource !== "info") { tagged.unshift(rootIntroDoc as any); } - return [...tagged, ...untagged]; + return [...tagged, ...untagged, ...schemas]; } export default function generateSidebarSlice( @@ -215,18 +261,55 @@ export default function generateSidebarSlice( options: APIOptions, api: ApiMetadata[], tags: TagObject[][], - docPath: string + docPath: string, + tagGroups?: TagGroupObject[] ) { let sidebarSlice: ProcessedSidebar = []; - if (sidebarOptions.groupPathsBy === "tag") { - sidebarSlice = groupByTags( - api as ApiPageMetadata[], - sidebarOptions, - options, - tags, - docPath - ); + if (sidebarOptions.groupPathsBy === "tagGroup") { + let schemasGroup: ProcessedSidebar = []; + tagGroups?.forEach((tagGroup) => { + //filter tags only included in group + const filteredTags: TagObject[] = []; + tags[0].forEach((tag) => { + if (tagGroup.tags.includes(tag.name as string)) { + filteredTags.push(tag); + } + }); + + const groupCategory = { + type: "category" as const, + label: tagGroup.name, + collapsible: true, + collapsed: true, + items: groupByTags( + api, + sidebarOptions, + options, + [filteredTags], + docPath + ), + }; + + if (options.showSchemas) { + // For the first tagGroup, save the generated "Schemas" category for later. + if (schemasGroup.length === 0) { + schemasGroup = groupCategory.items?.filter( + (item) => item.type === "category" && item.label === "Schemas" + ); + } + // Remove the "Schemas" category from every `groupCategory`. + groupCategory.items = groupCategory.items.filter((item) => + "label" in item ? item.label !== "Schemas" : true + ); + } + sidebarSlice.push(groupCategory as ProcessedSidebarItem); + }); + // Add `schemasGroup` to the end of the sidebar. + sidebarSlice.push(...schemasGroup); + } else if (sidebarOptions.groupPathsBy === "tag") { + sidebarSlice = groupByTags(api, sidebarOptions, options, tags, docPath); } + return sidebarSlice; } diff --git a/packages/docusaurus-plugin-openapi-docs/src/types.ts b/packages/docusaurus-plugin-openapi-docs/src/types.ts index 7c12d0805..8eee4a0e1 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/types.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/types.ts @@ -5,11 +5,12 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import type Request from "@paloaltonetworks/postman-collection"; +import type Request from "postman-collection"; import { InfoObject, OperationObject, + SchemaObject, SecuritySchemeObject, TagObject, } from "./openapi/types"; @@ -22,6 +23,7 @@ export type { } from "@docusaurus/plugin-content-docs-types"; export interface PluginOptions { id?: string; + docsPlugin?: string; docsPluginId: string; config: { [key: string]: APIOptions; @@ -44,12 +46,15 @@ export interface APIOptions { }; proxy?: string; markdownGenerators?: MarkdownGenerator; + showSchemas?: boolean; + disableCompression?: boolean; } export interface MarkdownGenerator { createApiPageMD?: (pageData: ApiPageMetadata) => string; createInfoPageMD?: (pageData: InfoPageMetadata) => string; createTagPageMD?: (pageData: TagPageMetadata) => string; + createSchemaPageMD?: (pageData: SchemaPageMetadata) => string; } export interface SidebarOptions { @@ -65,6 +70,7 @@ export interface APIVersionOptions { outputDir: string; label: string; baseUrl: string; + downloadUrl?: string; } export interface LoadedContent { @@ -72,7 +78,11 @@ export interface LoadedContent { // loadedDocs: DocPageMetadata[]; TODO: cleanup } -export type ApiMetadata = ApiPageMetadata | InfoPageMetadata | TagPageMetadata; +export type ApiMetadata = + | ApiPageMetadata + | InfoPageMetadata + | TagPageMetadata + | SchemaPageMetadata; export interface ApiMetadataBase { sidebar?: string; @@ -131,6 +141,19 @@ export interface TagPageMetadata extends ApiMetadataBase { markdown?: string; } +export interface SchemaPageMetadata extends ApiMetadataBase { + type: "schema"; + schema: SchemaObject; + markdown?: string; +} + +export interface TagGroupPageMetadata extends ApiMetadataBase { + type: "tagGroup"; + name: string; + tags: TagObject[]; + markdown?: string; +} + export type ApiInfo = InfoObject; export interface ApiNavLink { diff --git a/packages/docusaurus-theme-openapi-docs/package.json b/packages/docusaurus-theme-openapi-docs/package.json index f2004b4b6..5fca1c869 100644 --- a/packages/docusaurus-theme-openapi-docs/package.json +++ b/packages/docusaurus-theme-openapi-docs/package.json @@ -1,7 +1,7 @@ { "name": "docusaurus-theme-openapi-docs", "description": "OpenAPI theme for Docusaurus.", - "version": "2.0.0", + "version": "2.2.0", "license": "MIT", "keywords": [ "openapi", @@ -37,17 +37,17 @@ "dependencies": { "@docusaurus/theme-common": ">=2.4.1 <=2.4.3", "@hookform/error-message": "^2.0.1", - "@paloaltonetworks/postman-code-generators": "1.1.15-patch.2", - "@paloaltonetworks/postman-collection": "^4.1.0", "@reduxjs/toolkit": "^1.7.1", "clsx": "^1.1.1", "copy-text-to-clipboard": "^3.1.0", "crypto-js": "^4.1.1", - "docusaurus-plugin-openapi-docs": "^2.0.0", + "docusaurus-plugin-openapi-docs": "^2.2.0", "docusaurus-plugin-sass": "^0.2.3", "file-saver": "^2.0.5", "lodash": "^4.17.20", "node-polyfill-webpack-plugin": "^2.0.1", + "postman-code-generators": "^1.10.1", + "postman-collection": "^4.4.0", "prism-react-renderer": "^1.3.5", "react-hook-form": "^7.43.8", "react-live": "^4.0.0", diff --git a/packages/docusaurus-theme-openapi-docs/src/markdown/utils.test.ts b/packages/docusaurus-theme-openapi-docs/src/markdown/utils.test.ts new file mode 100644 index 000000000..999b929db --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/markdown/utils.test.ts @@ -0,0 +1,49 @@ +/* ============================================================================ + * 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 { guard } from "./utils"; + +describe("guard", () => { + it("should guard empty strings", () => { + const actual = guard("", (_) => { + throw new Error("Should not be called"); + }); + expect(actual).toBe(""); + }); + + it("should guard undefined", () => { + const actual = guard(undefined, (value) => { + throw new Error("Should not be called"); + }); + expect(actual).toBe(""); + }); + + it("should guard false booleans", () => { + const actual = guard(false, (value) => `${value}`); + expect(actual).toBe(""); + }); + + it("should not guard strings", () => { + const actual = guard("hello", (value) => value); + expect(actual).toBe("hello"); + }); + + it("should not guard numbers", () => { + const actual = guard(1, (value) => `${value}`); + expect(actual).toBe("1"); + }); + + it("should not guard numbers equals to 0", () => { + const actual = guard(0, (value) => `${value}`); + expect(actual).toBe("0"); + }); + + it("should not guard true booleans", () => { + const actual = guard(true, (value) => `${value}`); + expect(actual).toBe("true"); + }); +}); diff --git a/packages/docusaurus-theme-openapi-docs/src/markdown/utils.ts b/packages/docusaurus-theme-openapi-docs/src/markdown/utils.ts index b18701e07..7221cb92c 100644 --- a/packages/docusaurus-theme-openapi-docs/src/markdown/utils.ts +++ b/packages/docusaurus-theme-openapi-docs/src/markdown/utils.ts @@ -24,7 +24,7 @@ export function guard( value: T | undefined | string, cb: (value: T) => Children ): string { - if (!!value) { + if (!!value || value === 0) { const children = cb(value as T); return render(children); } diff --git a/packages/docusaurus-theme-openapi-docs/src/postman-code-generators.d.ts b/packages/docusaurus-theme-openapi-docs/src/postman-code-generators.d.ts index 712697695..a6a3d4152 100644 --- a/packages/docusaurus-theme-openapi-docs/src/postman-code-generators.d.ts +++ b/packages/docusaurus-theme-openapi-docs/src/postman-code-generators.d.ts @@ -6,4 +6,4 @@ * ========================================================================== */ // TODO: Remove this when https://github.com/facebook/docusaurus/issues/6087 is resolved. -declare module "@paloaltonetworks/postman-code-generators"; +declare module "postman-code-generators"; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme-openapi.d.ts b/packages/docusaurus-theme-openapi-docs/src/theme-openapi.d.ts index 8961e7d49..2456db3d1 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme-openapi.d.ts +++ b/packages/docusaurus-theme-openapi-docs/src/theme-openapi.d.ts @@ -63,6 +63,10 @@ declare module "@theme/SchemaTabs" { export default function SchemaTabs(props: any): JSX.Element; } +declare module "@theme/Markdown" { + export default function Markdown(props: any): JSX.Element; +} + declare module "@theme/ApiExplorer/Accept" { export default function Accept(): JSX.Element; } diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiDemoPanel/ApiCodeBlock/ExpandButton/index.js b/packages/docusaurus-theme-openapi-docs/src/theme/ApiDemoPanel/ApiCodeBlock/ExpandButton/index.js deleted file mode 100644 index cb71c3299..000000000 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiDemoPanel/ApiCodeBlock/ExpandButton/index.js +++ /dev/null @@ -1,165 +0,0 @@ -/* ============================================================================ - * Copyright (c) Palo Alto Networks - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * ========================================================================== */ - -import React, { useEffect } from "react"; - -import { usePrismTheme } from "@docusaurus/theme-common"; -import { translate } from "@docusaurus/Translate"; -import Container from "@theme/ApiDemoPanel/ApiCodeBlock/Container"; -import CopyButton from "@theme/ApiDemoPanel/ApiCodeBlock/CopyButton"; -import ExitButton from "@theme/ApiDemoPanel/ApiCodeBlock/ExitButton"; -import Line from "@theme/ApiDemoPanel/ApiCodeBlock/Line"; -import clsx from "clsx"; -import Highlight, { defaultProps } from "prism-react-renderer"; -import Modal from "react-modal"; - -export default function ExpandButton({ - code, - className, - language, - showLineNumbers, - blockClassName, - title, - lineClassNames, -}) { - const prismTheme = usePrismTheme(); - - function openModal() { - setIsOpen(true); - } - - function closeModal() { - setIsOpen(false); - } - - const [modalIsOpen, setIsOpen] = React.useState(false); - - useEffect(() => { - Modal.setAppElement("body"); - }); - - return ( - <> - - - - {title && ( -
    {title}
    - )} -
    - - {({ className, tokens, getLineProps, getTokenProps }) => ( -
    -                  
    -                    {tokens.map((line, i) => (
    -                      
    -                    ))}
    -                  
    -                
    - )} -
    -
    - - -
    -
    -
    -
    - - ); -} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiDemoPanel/ApiCodeBlock/Line/_Line.scss b/packages/docusaurus-theme-openapi-docs/src/theme/ApiDemoPanel/ApiCodeBlock/Line/_Line.scss deleted file mode 100644 index a4185edb9..000000000 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiDemoPanel/ApiCodeBlock/Line/_Line.scss +++ /dev/null @@ -1,46 +0,0 @@ -/* Intentionally has zero specificity, so that to be able to override -the background in custom CSS file due bug https://github.com/facebook/docusaurus/issues/3678 */ -:where(:root) { - --docusaurus-highlighted-code-line-bg: rgb(72 77 91); -} - -:where([data-theme="dark"]) { - --docusaurus-highlighted-code-line-bg: rgb(100 100 100); -} - -.theme-code-block-highlighted-line { - background-color: var(--docusaurus-highlighted-code-line-bg); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); -} - -.openapi-explorer__code-block-code-line { - display: table-row; - counter-increment: line-count; -} - -.openapi-explorer__code-block-code-line-number { - display: table-cell; - text-align: right; - width: 1%; - position: sticky; - left: 0; - padding: 0 var(--ifm-pre-padding); - background: var(--ifm-pre-background); - overflow-wrap: normal; -} - -.openapi-explorer__code-block-code-line-number::before { - content: counter(line-count); - opacity: 0.4; -} - -:global(.theme-code-block-highlighted-line) - .openapi-explorer__code-block-code-line-number::before { - opacity: 0.8; -} - -.openapi-explorer__code-block-code-line-number { - padding-right: var(--ifm-pre-padding); -} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx index 816c00fe6..b0dad0689 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx @@ -14,6 +14,7 @@ import FormSelect from "@theme/ApiExplorer/FormSelect"; import FormTextInput from "@theme/ApiExplorer/FormTextInput"; import LiveApp from "@theme/ApiExplorer/LiveEditor"; import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; +import Markdown from "@theme/Markdown"; import SchemaTabs from "@theme/SchemaTabs"; import TabItem from "@theme/TabItem"; import { RequestBodyObject } from "docusaurus-plugin-openapi-docs/src/openapi/types"; @@ -303,6 +304,7 @@ function Body({ {/* @ts-ignore */} + {example.summary && } {exampleBody && ( - {example.summary &&

    {example.summary}

    } + {example.summary && } {example.body && ( {example.body} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeSnippets/code-snippets-types.ts b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeSnippets/code-snippets-types.ts new file mode 100644 index 000000000..e46ed106d --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeSnippets/code-snippets-types.ts @@ -0,0 +1,55 @@ +/* ============================================================================ + * 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. + * ========================================================================== */ + +// https://github.com/github-linguist/linguist/blob/master/lib/linguist/popular.yml +export type CodeSampleLanguage = + | "C" + | "C#" + | "C++" + | "CoffeeScript" + | "CSS" + | "Dart" + | "DM" + | "Elixir" + | "Go" + | "Groovy" + | "HTML" + | "Java" + | "JavaScript" + | "Kotlin" + | "Objective-C" + | "Perl" + | "PHP" + | "PowerShell" + | "Python" + | "Ruby" + | "Rust" + | "Scala" + | "Shell" + | "Swift" + | "TypeScript"; + +export interface Language { + highlight: string; + language: string; + codeSampleLanguage: CodeSampleLanguage; + logoClass: string; + variant: string; + variants: string[]; + options?: { [key: string]: boolean }; + sample?: string; + samples?: string[]; + samplesSources?: string[]; + samplesLabels?: string[]; +} + +// https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples +export interface CodeSample { + source: string; + lang: CodeSampleLanguage; + label?: string; +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeSnippets/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeSnippets/index.tsx index 9bd90e344..0dfd13aa0 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeSnippets/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/CodeSnippets/index.tsx @@ -5,31 +5,35 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ +/* ============================================================================ + * Copyright (c) Palo Alto Networks + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + import React, { useState, useEffect } from "react"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; -import codegen from "@paloaltonetworks/postman-code-generators"; -import sdk from "@paloaltonetworks/postman-collection"; import ApiCodeBlock from "@theme/ApiExplorer/ApiCodeBlock"; import buildPostmanRequest from "@theme/ApiExplorer/buildPostmanRequest"; import CodeTabs from "@theme/ApiExplorer/CodeTabs"; import { useTypedSelector } from "@theme/ApiItem/hooks"; -import merge from "lodash/merge"; +import codegen from "postman-code-generators"; +import sdk from "postman-collection"; -export interface Language { - highlight: string; - language: string; - logoClass: string; - variant: string; - variants: string[]; - options: { [key: string]: boolean }; - source?: string; -} +import { CodeSample, Language } from "./code-snippets-types"; +import { + getCodeSampleSourceFromLanguage, + mergeArraysbyLanguage, + mergeCodeSampleLanguage, +} from "./languages"; export const languageSet: Language[] = [ { highlight: "bash", language: "curl", + codeSampleLanguage: "Shell", logoClass: "bash", options: { longFormat: false, @@ -42,6 +46,7 @@ export const languageSet: Language[] = [ { highlight: "python", language: "python", + codeSampleLanguage: "Python", logoClass: "python", options: { followRedirect: true, @@ -53,6 +58,7 @@ export const languageSet: Language[] = [ { highlight: "go", language: "go", + codeSampleLanguage: "Go", logoClass: "go", options: { followRedirect: true, @@ -64,6 +70,7 @@ export const languageSet: Language[] = [ { highlight: "javascript", language: "nodejs", + codeSampleLanguage: "JavaScript", logoClass: "nodejs", options: { ES6_enabled: true, @@ -71,11 +78,12 @@ export const languageSet: Language[] = [ trimRequestBody: true, }, variant: "axios", - variants: ["axios", "native", "request", "unirest"], + variants: ["axios", "native"], }, { highlight: "ruby", language: "ruby", + codeSampleLanguage: "Ruby", logoClass: "ruby", options: { followRedirect: true, @@ -87,6 +95,7 @@ export const languageSet: Language[] = [ { highlight: "csharp", language: "csharp", + codeSampleLanguage: "C#", logoClass: "csharp", options: { followRedirect: true, @@ -98,6 +107,7 @@ export const languageSet: Language[] = [ { highlight: "php", language: "php", + codeSampleLanguage: "PHP", logoClass: "php", options: { followRedirect: true, @@ -109,6 +119,7 @@ export const languageSet: Language[] = [ { highlight: "java", language: "java", + codeSampleLanguage: "Java", logoClass: "java", options: { followRedirect: true, @@ -120,6 +131,7 @@ export const languageSet: Language[] = [ { highlight: "powershell", language: "powershell", + codeSampleLanguage: "PowerShell", logoClass: "powershell", options: { followRedirect: true, @@ -132,10 +144,10 @@ export const languageSet: Language[] = [ export interface Props { postman: sdk.Request; - codeSamples: any; // TODO: Type this... + codeSamples: CodeSample[]; } -function CodeTab({ children, hidden, className, onClick }: any): JSX.Element { +function CodeTab({ children, hidden, className }: any): JSX.Element { return (