Neoteroi / BlackSheep

Fast ASGI web framework for Python
https://www.neoteroi.dev/blacksheep/
MIT License
1.8k stars 75 forks source link

Can operations in OpenAPI Specification be sorted? #327

Closed RobertoPrevato closed 1 year ago

RobertoPrevato commented 1 year ago

Can operations be sorted alphabetically?

image

tyzhnenko commented 1 year ago

@RobertoPrevato I've put an example of implementation in the linked PR

RobertoPrevato commented 1 year ago

@tyzhnenko in this specific case, what I really had in mind was simply to sort alphabetically the list of operations in the auto-generated OpenAPI Specification file. I need to look into your PR and think a bit about it.

tyzhnenko commented 1 year ago

@RobertoPrevato as I understand Swagger UI doesn't have a proper way to set the ordering type. By default it shows tags like they are saved in tags attribute in case this attribute is set. So, to make alphabetically sorting by default we have to sort tags themself and fill tags attribute. I'll think about how it can be handled and update the PR

RobertoPrevato commented 1 year ago

@tyzhnenko your recommendation and PR are good input and I thank you. I wasn't aware of the possibility to sort tags that way, it's a good idea using that.

Let me also explain better what I was thinking. If you consider this example:

from dataclasses import dataclass

from blacksheep import Application
from blacksheep.server.controllers import APIController, get, post
from blacksheep.server.openapi.v3 import OpenAPIHandler
from openapidocs.v3 import Info

app = Application()

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))
docs.bind_app(app)

@dataclass
class Cat:
    pass

@dataclass
class Dog:
    pass

@dataclass
class Parrot:
    pass

class Cats(APIController):
    @get()
    def get_cats(self) -> list[Cat]:
        """Return the list of configured cats."""

    @post()
    def create_cat(self, cat: Cat) -> None:
        """Add a Cat to the system."""

class Dogs(APIController):
    @get()
    def get_dogs(self) -> list[Dog]:
        """Return the list of configured dogs."""

    @post()
    def create_dog(self, dog: Dog) -> None:
        """Add a Dog to the system."""

class Parrots(APIController):
    @get()
    def get_parrots(self) -> list[Parrot]:
        """Return the list of configured Parrots."""

    @post()
    def create_parrot(self, parrot: Parrot) -> None:
        """Add a Parrot to the system."""

In this case tags are obtained automatically from controller names, and the OpenAPI Specification is sorted in the same order in which request handlers are registered.

image

If I simply move around in code the Dogs class declaration before Cats, operations are not sorted properly anymore alphabetically, because they are sorted by handler definition. This indicates that Swagger UI is respecting the order in which tags appear in the definition file.

image

When the user doesn't want to use Controller, BlackSheep lets specific tags using the @docs decorator.

app = Application()

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))
docs.bind_app(app)

@docs(tags=["Mouses"])
@app.router.post("/api/mouses")
async def create_mouse():
    """Add a Mouse to the system."""

image

In conclusion, I like very much the idea of documenting tags like you proposed! I want to merge your PR. In addition to that, I would still like operations to be sorted automatically by tag, even if the user doesn't describe tags explicitly in the instance of OpenAPIHandler.

tyzhnenko commented 1 year ago

@RobertoPrevato Yep, got it. Don't merge the PR until I push changes that provide the auto sorting feature. I have some ideas on how to make it and I need some time to verify them.

RobertoPrevato commented 1 year ago

@tyzhnenko I prepared an example in the meanwhile.

Example:

  1. adding these private methods to the class (with related imports, Iterable is missing)

    def _iter_tags(self, paths: Dict[str, PathItem]) -> Iterable[str]:
        methods = "get post put delete options head patch trace".split()

        for path in paths.values():
            for method in methods:
                prop: Optional[Operation] = getattr(path, method)

                if prop is not None:
                    yield from prop.tags

    def _tags_from_paths(self, paths: Dict[str, PathItem]) -> List[Tag]:
        unique_tags = set(self._iter_tags(paths))
        return [Tag(name) for name in sorted(unique_tags)]
  1. modify the generate_documentation method to use tags if the user specified them explicitly, like in your example, otherwise auto-generate from the tags obtained from paths, sorted alphabetically
    def generate_documentation(self, app: Application) -> OpenAPI:
        paths = self.get_paths(app)
        return OpenAPI(
            info=self.info,
            paths=paths,
            components=self.components,
            tags=self._tags if self._tags else self._tags_from_paths(paths),
        )

This from my point of view has two positive sides:

  1. if users of the framework wants to control the tags order, they can
  2. if users don't specify tags config explicitly (but they are obtained from Controllers or @docs decorator), they are sorted automatically