diff --git a/swh/web/api/apidoc.py b/swh/web/api/apidoc.py --- a/swh/web/api/apidoc.py +++ b/swh/web/api/apidoc.py @@ -3,14 +3,17 @@ # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information -import docutils.nodes -import docutils.parsers.rst -import docutils.utils + import functools from functools import wraps import os import re import textwrap +from typing import List + +import docutils.nodes +import docutils.parsers.rst +import docutils.utils from rest_framework.decorators import api_view import sentry_sdk @@ -253,63 +256,79 @@ """ -def api_doc(route, noargs=False, need_params=False, tags=[], - handle_response=False, api_version='1'): +def api_doc(route: str, noargs: bool = False, need_params: bool = False, + tags: List[str] = [], handle_response: bool = False, + api_version: str = '1', post_only: bool = False): """ - Decorate an API function to register it in the API doc route index - and create the corresponding DRF route. + Decorator for an API endpoint implementation used to generate a dedicated + view displaying its HTML documentation. + + The documentation will be generated from the endpoint docstring based on + sphinxcontrib-httpdomain format. Args: - route (str): documentation page's route - noargs (boolean): set to True if the route has no arguments, and its + route: documentation page's route + noargs: set to True if the route has no arguments, and its result should be displayed anytime its documentation is requested. Default to False - need_params (boolean): specify the route requires query parameters + need_params: specify the route requires query parameters otherwise errors will occur. It enables to avoid displaying the invalid response in its HTML documentation. Default to False. - tags (list): Further information on api endpoints. Two values are + tags: Further information on api endpoints. Two values are possibly expected: * hidden: remove the entry points from the listing * upcoming: display the entry point but it is not followable - handle_response (boolean): indicate if the decorated function takes + handle_response: indicate if the decorated function takes care of creating the HTTP response or delegates that task to the apiresponse module - api_version (str): api version string + api_version: api version string + post_only: if the decorated route only accepts POST requests, set this + parameter to True in order to generate a dedicated doc view + accepting GET requests """ - urlpattern = '^' + api_version + route + '$' - tags = set(tags) + + tags_set = set(tags) # @api_doc() Decorator call def decorator(f): - # If the route is not hidden, add it to the index - if 'hidden' not in tags: - doc_data = get_doc_data(f, route, noargs) + if 'hidden' not in tags_set: + doc_data = get_doc_data(f, route) doc_desc = doc_data['description'] first_dot_pos = doc_desc.find('.') APIUrls.add_route(route, doc_desc[:first_dot_pos+1], - tags=tags) + api_version=api_version, + doc_route=post_only, tags=tags_set) - # If the decorated route has arguments, we create a specific - # documentation view - if not noargs: + # If the decorated route has arguments or if the route does not accept + # GET requests, we create a specific documentation view + if not noargs or post_only: @api_view(['GET', 'HEAD']) @wraps(f) def doc_view(request): - doc_data = get_doc_data(f, route, noargs) + doc_data = get_doc_data(f, route) return make_api_response(request, None, doc_data) - view_name = 'api-%s-%s' % \ - (api_version, route[1:-1].replace('/', '-')) + route_name = route[1:-1].replace('/', '-') + urlpattern = '^' + api_version + route + # if the decorated route does not accept GET requests, we need to + # register the doc view with a different url and name + if post_only: + route_name += '-doc' + urlpattern += 'doc/$' + else: + urlpattern += '$' + + view_name = 'api-%s-%s' % (api_version, route_name) APIUrls.add_url_pattern(urlpattern, doc_view, view_name) @wraps(f) def documented_view(request, **kwargs): - doc_data = get_doc_data(f, route, noargs) + doc_data = get_doc_data(f, route) try: response = f(request, **kwargs) @@ -332,7 +351,7 @@ @functools.lru_cache(maxsize=32) -def get_doc_data(f, route, noargs): +def get_doc_data(f, route): """ Build documentation data for the decorated api endpoint function """ @@ -349,7 +368,6 @@ 'status_codes': [], 'examples': [], 'route': route, - 'noargs': noargs } if not f.__doc__: diff --git a/swh/web/api/apiurls.py b/swh/web/api/apiurls.py --- a/swh/web/api/apiurls.py +++ b/swh/web/api/apiurls.py @@ -30,13 +30,18 @@ return cls._apidoc_routes @classmethod - def add_route(cls, route, docstring, **kwargs): + def add_route(cls, route, docstring, api_version='1', + doc_route=False, **kwargs): """ Add a route to the self-documenting API reference """ - route_view_name = 'api-1-%s' % route[1:-1].replace('/', '-') + route_name = route[1:-1].replace('/', '-') + if doc_route: + route_name += '-doc' + route_view_name = 'api-%s-%s' % (api_version, route_name) if route not in cls._apidoc_routes: d = {'docstring': docstring, + 'route': '/api/%s%s' % (api_version, route), 'route_view_name': route_view_name} for k, v in kwargs.items(): d[k] = v diff --git a/swh/web/templates/api/endpoints.html b/swh/web/templates/api/endpoints.html --- a/swh/web/templates/api/endpoints.html +++ b/swh/web/templates/api/endpoints.html @@ -53,7 +53,7 @@ {% else %}