Changeset View
Changeset View
Standalone View
Standalone View
swh/web/api/v2/urls.py
- This file was added.
# Copyright (C) 2020 The Software Heritage developers | |||||
# See the AUTHORS file at the top-level directory of this distribution | |||||
# License: GNU Affero General Public License version 3, or any later version | |||||
# See top-level LICENSE file for more information | |||||
import os | |||||
from typing import Any, Callable | |||||
from openapi_core import create_spec | |||||
from openapi_core.contrib.django import DjangoOpenAPIRequest, DjangoOpenAPIResponse | |||||
from openapi_core.validation.request.datatypes import RequestParameters | |||||
from openapi_core.validation.request.validators import RequestValidator | |||||
from openapi_core.validation.response.validators import ResponseValidator | |||||
from prance import ResolvingParser | |||||
from django.conf.urls import url | |||||
from django.http import HttpRequest, HttpResponse | |||||
from django.shortcuts import render | |||||
from rest_framework.decorators import api_view, renderer_classes | |||||
from rest_framework.renderers import JSONRenderer | |||||
from rest_framework.request import Request | |||||
from rest_framework.response import Response | |||||
from swh.web.api.renderers import BinaryRenderer, YAMLRenderer | |||||
from swh.web.config import get_config | |||||
# root openapi.yml file path | |||||
spec_filepath = os.path.join(os.path.dirname(__file__), "spec/openapi.yml") | |||||
# parse and resolve OpenAPI specification | |||||
parser = ResolvingParser(spec_filepath) | |||||
# OpenAPI specification dictionary | |||||
spec_dict = parser.specification | |||||
# create OpenAPI request and response validators | |||||
spec = create_spec(spec_dict) | |||||
request_validator = RequestValidator(spec) | |||||
response_validator = ResponseValidator(spec) | |||||
@api_view(["GET"]) | |||||
@renderer_classes([JSONRenderer, YAMLRenderer]) | |||||
def _api_schema(request: Request) -> Response: | |||||
""" | |||||
Endpoint returning full dereferenced OpenAPI specification. | |||||
""" | |||||
spec_dict["servers"] = [ | |||||
{ | |||||
"url": request.build_absolute_uri("/api/2"), | |||||
"description": "Software Heritage API v2 server", | |||||
} | |||||
] | |||||
return Response(spec_dict) | |||||
def _api_doc(request: HttpRequest) -> HttpResponse: | |||||
""" | |||||
API documentation based on Swagger UI. | |||||
""" | |||||
return render(request, template_name="api/swagger-ui.html") | |||||
def _import_view_function( | |||||
module: str, function_name: str | |||||
) -> Callable[[Request, RequestParameters], Any]: | |||||
""" | |||||
Import view implementation logic from the function defined in | |||||
OpenAPI path specification (under the operationId property). | |||||
""" | |||||
temp = __import__(module, fromlist=[function_name]) | |||||
return getattr(temp, function_name) | |||||
def _get_api_view( | |||||
http_method: str, view_function: Callable[[Request, RequestParameters], Any] | |||||
): | |||||
""" | |||||
Create Django REST framework view by wrapping its implementation | |||||
logic. | |||||
""" | |||||
@api_view([http_method.upper()]) | |||||
@renderer_classes([JSONRenderer, YAMLRenderer, BinaryRenderer]) | |||||
def _api_view(request: Request, *args, **kwargs) -> HttpResponse: | |||||
# wrap django request | |||||
openapi_request = DjangoOpenAPIRequest(request) | |||||
# fix parsed url pattern still containing regexp special chars | |||||
full_url_pattern = openapi_request.full_url_pattern | |||||
full_url_pattern = full_url_pattern.replace("^", "").replace("$", "") | |||||
openapi_request.full_url_pattern = full_url_pattern | |||||
# validate request parameters | |||||
result = request_validator.validate(openapi_request) | |||||
result.raise_for_errors() | |||||
# call view implementation logic | |||||
data = view_function(request, result.parameters) | |||||
# create HTTP response | |||||
response = Response(data) | |||||
# validate response format in debug mode | |||||
if get_config()["debug"] and type(data) != bytes: | |||||
response.accepted_renderer = JSONRenderer() # type: ignore | |||||
response.accepted_media_type = "application/json" # type: ignore | |||||
response.renderer_context = {} # type: ignore | |||||
response.render() | |||||
openapi_response = DjangoOpenAPIResponse(response) | |||||
result = response_validator.validate(openapi_request, openapi_response) | |||||
result.raise_for_errors() | |||||
response._is_rendered = False # type: ignore | |||||
return response | |||||
return _api_view | |||||
urlpatterns = [ | |||||
url(r"2/schema/$", _api_schema, name="api-2-schema"), | |||||
url(r"2/doc/$", _api_doc, name="api-2-doc"), | |||||
] | |||||
# iterate on OpenAPI paths specification and register associated | |||||
# Django REST framework routes | |||||
paths = spec_dict["paths"] | |||||
for path in paths: | |||||
# create view name from its path components (omitting parameters) | |||||
base_view_name = "api-2" | |||||
for path_part in path.split("/"): | |||||
if path_part and not path_part.startswith("{"): | |||||
base_view_name = f"{base_view_name}-{path_part}" | |||||
nb_http_methods = len(paths[path]) | |||||
# add a route for each supported HTTP methods | |||||
for http_method, path_info in paths[path].items(): | |||||
# get view logic implementation function | |||||
view_path = path_info["operationId"] | |||||
view_module = view_path[: view_path.rfind(".")] | |||||
view_function_name = view_path[view_path.rfind(".") + 1 :] | |||||
view_function = _import_view_function(view_module, view_function_name) | |||||
# turn OpenAPI path into a Django URLPattern | |||||
if "parameters" in path_info: | |||||
for parameter in path_info["parameters"]: | |||||
if parameter["in"] != "path": | |||||
continue | |||||
path = path.replace( | |||||
f"{{{parameter['name']}}}", f"(?P<{parameter['name']}>.*)" | |||||
) | |||||
# suffixed view name by the HTTP method if multiples are supported | |||||
# for a same path | |||||
view_name = base_view_name | |||||
if nb_http_methods > 1: | |||||
view_name += f"-{http_method}" | |||||
# register the DRF route | |||||
urlpatterns.append( | |||||
url(f"2{path}$", _get_api_view(http_method, view_function), name=view_name) | |||||
) |