Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal on how to store more info in the openapi.json #13

Merged
merged 4 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions django_api_decorator/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,28 @@

def get_resolved_url_patterns(
base_patterns: Sequence[URLResolver | URLPattern],
) -> list[tuple[URLPattern, str]]:
) -> list[tuple[URLPattern, str, str]]:
"""
Given a list of base URL patterns, this function digs down into the URL hierarchy
and evaluates the full URLs of all django views that have a simple path (e.g. no
regex). Returns a list of tuples with each RoutePattern and its full URL.
"""

unresolved_patterns: list[tuple[URLResolver | URLPattern, str]] = [
(url_pattern, "/") for url_pattern in base_patterns
def combine_path(existing: str, append: str | None) -> str:
if not append:
return existing
elif existing == "":
return append
else:
return existing + ":" + append

unresolved_patterns: list[tuple[URLResolver | URLPattern, str, str]] = [
(url_pattern, "/", "") for url_pattern in base_patterns
]
resolved_urls: list[tuple[URLPattern, str]] = []
resolved_urls: list[tuple[URLPattern, str, str]] = []

while len(unresolved_patterns):
url_pattern, url_prefix = unresolved_patterns.pop()
url_pattern, url_prefix, reverse_path = unresolved_patterns.pop()

if not isinstance(url_pattern.pattern, RoutePattern):
logger.debug("Skipping URL that is not simple (e.g. regex or locale url)")
Expand All @@ -44,10 +52,13 @@ def get_resolved_url_patterns(
# If we are dealing with a URL Resolver we should dig further down.
if isinstance(url_pattern, URLResolver):
unresolved_patterns += [
(child_pattern, url) for child_pattern in url_pattern.url_patterns
(child_pattern, url, combine_path(reverse_path, url_pattern.namespace))
for child_pattern in url_pattern.url_patterns
]
else:
resolved_urls.append((url_pattern, url))
resolved_urls.append(
(url_pattern, url, combine_path(reverse_path, url_pattern.name))
)

return resolved_urls

Expand Down Expand Up @@ -89,7 +100,11 @@ def replacer(match: re.Match[str]) -> str:


def paths_and_types_for_view(
*, view_name: str, callback: Callable[..., HttpResponse], resolved_url: str
*,
view_name: str,
callback: Callable[..., HttpResponse],
resolved_url: str,
reverse_path: str,
) -> tuple[dict[str, Any], dict[str, Any]]:
api_meta: ApiMeta | None = getattr(callback, "_api_meta", None)

Expand Down Expand Up @@ -162,6 +177,7 @@ def to_ref_if_object(schema: dict[str, Any]) -> dict[str, Any]:
"description": textwrap.dedent(callback.__doc__ or "").strip(),
# Tags are useful for grouping operations in codegen
"tags": [app_name],
"x-reverse-path": reverse_path,
"parameters": parameters,
**request_body,
"responses": {
Expand Down Expand Up @@ -189,14 +205,15 @@ class OpenApiOperation:
callback: Callable[..., HttpResponse]
name: str
url: str
reverse_path: str

all_urls = get_resolved_url_patterns(urlpatterns)

operations = []

# Iterate through all django views within the url patterns and generate specs for
# them
for pattern, resolved_url in all_urls:
for pattern, resolved_url, reverse_path in all_urls:
if pattern.callback is None:
continue

Expand Down Expand Up @@ -226,6 +243,7 @@ class OpenApiOperation:
f"{pattern.name or pattern.callback.__name__}"
),
url=resolved_url,
reverse_path=reverse_path,
)
)

Expand All @@ -235,6 +253,7 @@ class OpenApiOperation:
callback=pattern.callback,
name=pattern.name or pattern.callback.__name__,
url=resolved_url,
reverse_path=reverse_path,
)
)

Expand All @@ -251,6 +270,7 @@ class OpenApiOperation:
view_name=operation.name,
callback=operation.callback,
resolved_url=operation.url,
reverse_path=operation.reverse_path,
)
api_components.update(components)

Expand Down
36 changes: 32 additions & 4 deletions tests/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from django.http import HttpRequest
from django.test.client import Client
from django.test.utils import override_settings
from django.urls import path
from django.urls import URLPattern, URLResolver, path
from django.urls.resolvers import RoutePattern
from pydantic import BaseModel

from django_api_decorator.decorators import api
from django_api_decorator.openapi import generate_api_spec
from django_api_decorator.openapi import generate_api_spec, get_resolved_url_patterns

urlpatterns = None

Expand Down Expand Up @@ -61,7 +62,7 @@ def view(
return Response(state=State.OK, num=3)

urls = [
path("<str:path_str>/<int:path_int>", view),
path("<str:path_str>/<int:path_int>", view, name="view_name"),
]

assert generate_api_spec(urls) == {
Expand All @@ -70,9 +71,10 @@ def view(
"paths": {
"/{path_str}/{path_int}": {
"post": {
"operationId": "view",
"operationId": "view_name",
"description": "",
"tags": ["test_openapi"],
"x-reverse-path": "view_name",
"parameters": [
{
"name": "path_str",
Expand Down Expand Up @@ -239,6 +241,7 @@ def view(request: HttpRequest) -> A | B | C:
"operationId": "view",
"description": "",
"tags": ["test_openapi"],
"x-reverse-path": "",
"parameters": [],
"responses": {
200: {
Expand Down Expand Up @@ -282,3 +285,28 @@ def view(request: HttpRequest) -> A | B | C:
}
},
}


def test_get_resolved_url_patterns() -> None:
child_pattern = URLPattern(
RoutePattern("child_pattern/nested/deep/"), lambda x: x, name="child_view"
)

base_patterns = [
URLResolver(
pattern=RoutePattern("toplevel_pattern/"),
urlconf_name=[
child_pattern,
],
namespace="top_namespace",
)
]

result = get_resolved_url_patterns(base_patterns)
assert result == [
(
child_pattern,
"/toplevel_pattern/child_pattern/nested/deep/",
"top_namespace:child_view",
)
]
7 changes: 7 additions & 0 deletions tests/test_response_encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def test_schema() -> None:
"operationId": "view_union",
"description": "",
"tags": ["test_response_encoding"],
"x-reverse-path": "",
"parameters": [],
"responses": {
200: {
Expand All @@ -136,6 +137,7 @@ def test_schema() -> None:
"operationId": "view_camel_case_pydantic_model",
"description": "",
"tags": ["test_response_encoding"],
"x-reverse-path": "",
"parameters": [],
"responses": {
200: {
Expand All @@ -156,6 +158,7 @@ def test_schema() -> None:
"operationId": "view_pydantic_model",
"description": "",
"tags": ["test_response_encoding"],
"x-reverse-path": "",
"parameters": [],
"responses": {
200: {
Expand All @@ -176,6 +179,7 @@ def test_schema() -> None:
"operationId": "view_bool",
"description": "",
"tags": ["test_response_encoding"],
"x-reverse-path": "",
"parameters": [],
"responses": {
200: {
Expand All @@ -192,6 +196,7 @@ def test_schema() -> None:
"operationId": "view_int",
"description": "",
"tags": ["test_response_encoding"],
"x-reverse-path": "",
"parameters": [],
"responses": {
200: {
Expand All @@ -208,6 +213,7 @@ def test_schema() -> None:
"operationId": "view_typed_dict",
"description": "",
"tags": ["test_response_encoding"],
"x-reverse-path": "",
"parameters": [],
"responses": {
200: {
Expand All @@ -228,6 +234,7 @@ def test_schema() -> None:
"operationId": "view_json_response",
"description": "",
"tags": ["test_response_encoding"],
"x-reverse-path": "",
"parameters": [],
"responses": {200: {"description": ""}},
}
Expand Down