cloud-annotations / docusaurus-openapi

πŸ¦• OpenAPI plugin for generating API reference docs in Docusaurus v2.
https://docusaurus-openapi.netlify.app
MIT License
505 stars 83 forks source link

[Multi-Spec Umbrella] Support Nested Sidebar Navigation for Multiple API Specifications #82

Open blugavere opened 2 years ago

blugavere commented 2 years ago

I'd like to potentially use this library to render many OpenAPI specs within a single Docusaurus site.

The current implementation only supports top level navigation (with single level drop downs) for multiple OpenAPI specs. This is a limitation of docusaurus itself. If I wanted to render hundreds of specs, it would not scale very well - there's only so much real estate in the top nav.

A viable solution may be to use multiple top-level navs with drop-downs and see how far we can get until https://github.com/facebook/docusaurus/issues/5938 is addressed. At a certain point, though, top level navigation itself doesn't scale even with dropdowns.

Feature Request:

Allow multiple OpenAPI specs to be rendered within the Sidebar of a single route base path.

The current implementation groups operations by tag similar to the Swagger UI within the sidebar.

In order to support N specifications we should be able to render each spec as a category, or, an arbitrary grouping of specs as a category. This will scale better because vertical navigation is less constrained.

Current State

"Logo" "Tutorial" "Auth" "API" "Issue 21" "COS" "YAML" "Blog"
---
pet
    Recursive
    Find pet by ID

Target State

"Logo" "Tutorial" "Auth" "API Reference" "Blog"
---
Group A // specification grouping
    Pet Store // specification description
        pet // operation label/group
            Recursive // operation
            Find pet by ID // operation
    Issue 21 // server
        Missing Summary
Group B
    COS
        Authentication
        Generating an IAM Token
    YAML

A configuration could look something like:

{
  themeConfig:
    /** @type {import('docusaurus-preset-openapi').ThemeConfig} */
    ({
      navbar: {
        // ...
        items: [
          { to: "/auth", label: "Auth", position: "left" },
          {
            label: "API Reference",
            activeBasePath: "api",
            // only relevant at top level
            position: "left",
          },
          // ...
        ],
      },
    }),
  plugins: [
    // can maintain existing functionality
    [
      "docusaurus-plugin-openapi",
      {
        id: "authentication",
        // path to openapi spec
        path: "examples/auth.json",
        // navigation subpath - ties the plugin to the routing
        routeBasePath: "auth",
      },
    ],
    [
      "docusaurus-plugin-openapi",
      {
        // id: null - no need for id for a category
        // navigation subpath - ties the plugin to the routing
        routeBasePath: "api",
        items: [
          {
            label: "Group A",
            routeBasePath: "a",
            items: [
              {
                id: "pet-store",
                label: "Pet Store",
                // becomes api/a/pet-store/[operation]
                // http://localhost:3000/api/a/pet-store/recursive
                routeBasePath: "pet-store",
                // path to openapi spec
                path: "examples/pet-store.json",
              },
              {
                id: "issue21",
                label: "Issue 21",
                routeBasePath: "issue-21",
                path: "examples/openapi-issue-21.json",
              },
            ],
          },
          {
            label: "Group B",
            routeBasePath: "b",
            items: [
              {
                id: "cos",
                label: "COS",
                routeBasePath: "cos",
                path: "examples/cos.json",
              },
              {
                id: "yaml",
                label: "YAML",
                // http://localhost:3000/api/b/yaml/hello-world
                routeBasePath: "yaml",
                path: "examples/yaml.json",
              },
            ],
          },
        ],
      },
    ],
  ],
};

TODO:

bourdakos1 commented 2 years ago

@blugavere I like this!

I haven't thought too much about this, but what if instead of the items config object, we did something similar to how docs works?

The plugin config could look something like this:

plugins: [
  [
    "docusaurus-plugin-openapi",
    {
      path: "api",
      routeBasePath: "api",
    },
  ],
]

Then you could use the file structure to organize categories:

api
β”œβ”€ a
β”‚  β”œβ”€ _catagory_.json
β”‚  β”œβ”€ pet-store.json
β”‚  └─ openapi-issue-21.json
└─ b
   β”œβ”€ _catagory_.json
   β”œβ”€ cos.json
   └─ yaml.json
/* _catagory_.json */

{
  "label": "Group A",
  "position": 1
}

We could also use a similar sidebarPath: require.resolve('./sidebars.js'), as an escape hatch from the file path stuff?

blugavere commented 2 years ago

what if instead of the items config object, we did something similar to how docs works?

I like it! Maybe take it a bit further...

api
β”œβ”€ a
β”‚  β”œβ”€ _category_.json
β”‚  β”œβ”€ pet-store
β”‚  β”‚  β”œβ”€ _category_.json
β”‚  β”‚  β”œβ”€ _spec_.json # will generate children for pet/store/user operation groups
β”‚  β”‚  β”œβ”€ introduction.md # will be above the operation groups

image

bourdakos1 commented 2 years ago

I like the idea of being able to drop in md/mdx files, but I'm not sure I understand the purpose of _spec_.json?

bourdakos1 commented 2 years ago

Oh I see... MDX supports importing files, so someone could want to import json, hence interfering with the plugin?

I think I would rather have a way to exclude the non-openapi-json than forcing _spec_.json.

Since OpenAPI definitions require an openapi key, we have a decent heuristic for throwing out invalid json/yaml.

On top of that, if the non-openapi-json does have an openapi key for some reason, maybe we could have some sort of ignore config or maybe ignore anything that starts with an underscore?

i.e. _example.json

Thoughts?

blugavere commented 2 years ago

it's just a convention similar to _category_.json so the plugin knows which file(s) render for a given folder as the openapi spec.

in your example pet-store.json & openapi-issue-21.json are both in the a folder and so you can infer based on the specification info.title what the navigation label should be, so you're creating the sub-directory automatically as a default.

If you wanted to drop custom markdown into the folder where "Swagger Petstore" would render, you'd need a way to tie the "Swagger Petstore" spec to that folder. You can manually create the folder, add a custom Category name (via _category.json), & explicitly reference the OpenAPI spec that should render in that folder via the _spec_.json

api
β”œβ”€ a
β”‚  β”œβ”€ _catagory_.json
β”‚  β”œβ”€ pet-store.json
β”‚  └─ openapi-issue-21.json
β”‚  └─ NOT-VALID-OPENAPI.json # what does the plugin do?

Separately, /docs in docusaurus allows you to store arbitrary JSON files and reference them in your MDX files. For the plugin to know which files in the /docs folder (e.g. api in this example) to try to render you should be able to explicitly reference them in some configuration. Not sure if this should be the same spec.json file though b/c that can be confusing.

{
 "specifications": ["./pet-store.json", "./openapi-issue-21.json"]
}
blugavere commented 2 years ago

Oh I see... MDX supports importing files, so someone could want to import json, hence interfering with the plugin?

I think I would rather have a way to exclude the non-openapi-json than forcing _spec_.json.

Since OpenAPI definitions require an openapi key, we have a decent heuristic for throwing out invalid json/yaml.

On top of that, if the non-openapi-json does have an openapi key for some reason, maybe we could have some sort of ignore config or maybe ignore anything that starts with an underscore?

i.e. _example.json

Thoughts?

realize we replied at the same time -

what if you want to customize the sidebar label & not use the openspi spec title?

how would you differentiate between "I want to render the swagger spec in the folder in which it resides with custom mdx" vs "I want to render a subfolder with the OpenAPI spec title as the sidebar label"?

may still need a config file somewhere? hiding irrelevant json with an underscore makes sense

bourdakos1 commented 2 years ago

Oh so outside of the json collisions does _spec_.json act as like a "don't create a sub route for this"?

So with this:

api
β”œβ”€ a
β”‚  β”œβ”€ _catagory_.json
β”‚  β”œβ”€ pet-store.json
β”‚  └─ openapi-issue-21.json
└─ b
   β”œβ”€ _catagory_.json
   β”œβ”€ _spec_.json
   └─ introduction.mdx

You would get these routes:

/api/a/pet-store/thing1
/api/a/pet-store/thing2
/api/a/openapi-issue-21/thing1
/api/a/openapi-issue-21/thing2
/api/b/introduction
/api/b/thing1
/api/b/thing2
bourdakos1 commented 2 years ago

haha we did it again

bourdakos1 commented 2 years ago

I'm good with having a specific spec name for the "I want to render the swagger spec the folder in which it resides with custom mdx" case

bourdakos1 commented 2 years ago

I really like all of this.

Since this will probably be a rather large change, I will start by creating a branch off of main. That way, if this is something you are interested in working on, we could both open PRs contributing to that branch.

I might try to spend some time this weekend cleaning up the current plugin logic so this feature will be easier to add.

After the initial cleanup work, I think the first PR should be adding support for reading multiple specs from a directory. (no _catagory_.json, _spec_.json or markdown yet)

Once we have that merged, it should be much easier to divide up the rest of the work.

Let me know what you would like your involvement to be on this. I'm happy to give you full reins or we can work in tandem or I can do it all haha 😊

bourdakos1 commented 2 years ago

@blugavere Can we update the description of this issue to include a todo list to track tasks we still need to complete?

What I can think of off the top of my head: