How can I create an API index using multiple DRF routers?

133 Views Asked by At

I'm trying to build an API using DRF with the following structure (example):

api/
├── v1/
│   ├── foo/
│   │   ├── bar/
│   │   │   └── urls.py # There's one `rest_framework.routers.DefaultRouter` here
│   │   ├── bar2/
│   │   │   └── urls.py # There's one `rest_framework.routers.DefaultRouter` here
│   │   ├── __init__.py
│   │   └── urls.py
│   ├── foo2/
│   │   ├── bar3/
│   │   │   └── urls.py # There's one `rest_framework.routers.DefaultRouter` here
│   │   ├── bar4/
│   │   │   └── urls.py # There's one `rest_framework.routers.DefaultRouter` here
│   │   ├── __init__.py
│   │   └── urls.py
│   ├── __init__.py
│   └── urls.py
├── __init__.py
└── urls.py

Intuitively, my endpoints would be

https://api.example.com/v1/foo/bar/...
https://api.example.com/v1/foo/bar2/...
https://api.example.com/v1/foo2/bar3/...
https://api.example.com/v1/foo2/bar4/...

But I want that Api Root web page to be available from the https://api.example.com/v1 level. For example, when I ran curl https://api.example.com/v1 it would show me

{"foo":"https://api.example.com/v1/foo/","foo2":"https://api.example.com/v1/foo2/"}

and so on.

That being said, I guess that the way to do it was to somehow "merge" those DefaultRouters.

I'm aware that I could just router.registry.extend(some_other_router.registry), but that would make it all be at the same level and I explicitly needed it to be multi-level, as shown above.

1

There are 1 best solutions below

0
On

I ended up writing an IndexRouter class, which then can be used as the following:

  • Example 1: reusing your urlpatterns:
your_old_urlpatterns = []
router = IndexRouter(urlpatterns=your_old_urlpatterns)
urlpatterns = router.to_urlpatterns()
  • Example 2: using other DRF routers:
from my_app.urls import router as my_app_router

router = IndexRouter(routers={"my_app": my_app_router})

You can also mix them. Besides, there are other useful parameters, such as:

  • name: set the page name for breadcrumb navigation
  • deprecated_func: a function that tells whether this endpoint is deprecated or not (drf-yasg compatible)
  • swagger_operation_description: drf-yasg compatible, see their docs
  • swagger_operation_summary: drf-yasg compatible, see their docs

The implementation itself, although not really elegant, is the following:

from collections import OrderedDict
from typing import Any, Callable, Dict, List, Union

from django.urls import include, path, URLPattern, URLResolver
from drf_yasg.utils import swagger_auto_schema
from rest_framework.response import Response
from rest_framework.routers import BaseRouter
from rest_framework.views import APIView


class IndexView(APIView):
    deprecated_func: Callable[[], bool] = None
    ignore_admin: bool = None
    router_names: List[str] = None
    swagger_operation_description: str = (None,)
    swagger_operation_summary: str = (None,)
    urlpatterns: List[Union[URLPattern, URLResolver]] = None
    view_name: str = None

    def __init__(
        self,
        *,
        deprecated_func: Callable[[], bool] = lambda: False,
        ignore_admin: bool = True,
        router_names: List[str] = None,
        swagger_operation_description: str = None,
        swagger_operation_summary: str = None,
        urlpatterns: List[Union[URLPattern, URLResolver]] = None,
        view_name: str = "Index",
        **kwargs,
    ):
        super().__init__(**kwargs)
        self.deprecated_func = deprecated_func
        self.ignore_admin = ignore_admin
        self.router_names = router_names or []
        self.swagger_operation_description = swagger_operation_description
        self.swagger_operation_summary = swagger_operation_summary
        self.urlpatterns = urlpatterns or []
        self.view_name = view_name

        @swagger_auto_schema(
            deprecated=self.deprecated_func(),
            operation_summary=self.swagger_operation_summary,
            operation_description=self.swagger_operation_description,
        )
        def get(request, format=None):
            current_url = request.build_absolute_uri().rstrip("/")
            data = OrderedDict()
            for router_name in self.router_names:
                data[router_name] = f"{current_url}/{router_name}/"
            for urlpatterns in self.urlpatterns:
                if isinstance(urlpatterns, URLResolver):
                    pattern = str(urlpatterns.pattern).rstrip("/")
                    if self.ignore_admin and pattern == "admin":
                        continue
                    data[
                        str(urlpatterns.pattern).rstrip("/")
                    ] = f"{current_url}/{str(urlpatterns.pattern)}"
                elif isinstance(urlpatterns, URLPattern):
                    # Parse the URLPattern to something readable
                    base_pattern = str(urlpatterns.pattern).rstrip("$")
                    base_pattern = base_pattern.rstrip("/")
                    base_pattern = base_pattern.replace("^", "")
                    base_pattern = base_pattern.replace("<", "{")
                    base_pattern = base_pattern.replace(">", "}")
                    # If base_pattern is empty, skip it
                    if not base_pattern:
                        continue
                    # If we want to skip admin, skip it
                    if self.ignore_admin and base_pattern == "admin":
                        continue
                    # If we have a regex group, skip it
                    if "(" in base_pattern or ")" in base_pattern:
                        continue
                    data[base_pattern] = f"{current_url}/{base_pattern}/"
            return Response(data)

        self.get = get

    def get_view_name(self):
        return self.view_name

class IndexRouter:
    def __init__(
        self,
        routers: Dict[str, BaseRouter] = None,
        urlpatterns: List[Any] = None,
        name: str = "Index",
        deprecated_func: Callable[[], bool] = lambda: False,
        swagger_operation_description: str = None,
        swagger_operation_summary: str = None,
    ):
        self.routers = routers or {}
        self.urlpatterns = urlpatterns or []
        self.name = name
        self.deprecated_func = deprecated_func
        self.swagger_operation_description = swagger_operation_description
        self.swagger_operation_summary = swagger_operation_summary
        self._index = IndexView(
            router_names=list(self.routers.keys()),
            urlpatterns=self.urlpatterns,
            view_name=self.name,
            deprecated_func=self.deprecated_func,
            swagger_operation_description=self.swagger_operation_description,
            swagger_operation_summary=self.swagger_operation_summary,
        )

    def to_urlpatterns(self):
        urlpatterns = self.urlpatterns
        urlpatterns.append(
            path(
                "",
                self._index.as_view(
                    router_names=list(self.routers.keys()),
                    urlpatterns=self.urlpatterns,
                    view_name=self.name,
                    deprecated_func=self.deprecated_func,
                    swagger_operation_description=self.swagger_operation_description,
                    swagger_operation_summary=self.swagger_operation_summary,
                ),
                name="index",
            )
        )
        for name, router in self.routers.items():
            urlpatterns.append(path(name + "/", include(router.urls)))
        return urlpatterns