generators.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. """
  2. generators.py # Top-down schema generation
  3. See schemas.__init__.py for package overview.
  4. """
  5. import re
  6. from importlib import import_module
  7. from django.conf import settings
  8. from django.contrib.admindocs.views import simplify_regex
  9. from django.core.exceptions import PermissionDenied
  10. from django.http import Http404
  11. from rest_framework import exceptions
  12. from rest_framework.compat import URLPattern, URLResolver, get_original_route
  13. from rest_framework.request import clone_request
  14. from rest_framework.settings import api_settings
  15. from rest_framework.utils.model_meta import _get_pk
  16. def get_pk_name(model):
  17. meta = model._meta.concrete_model._meta
  18. return _get_pk(meta).name
  19. def is_api_view(callback):
  20. """
  21. Return `True` if the given view callback is a REST framework view/viewset.
  22. """
  23. # Avoid import cycle on APIView
  24. from rest_framework.views import APIView
  25. cls = getattr(callback, 'cls', None)
  26. return (cls is not None) and issubclass(cls, APIView)
  27. def endpoint_ordering(endpoint):
  28. path, method, callback = endpoint
  29. method_priority = {
  30. 'GET': 0,
  31. 'POST': 1,
  32. 'PUT': 2,
  33. 'PATCH': 3,
  34. 'DELETE': 4
  35. }.get(method, 5)
  36. return (method_priority,)
  37. _PATH_PARAMETER_COMPONENT_RE = re.compile(
  38. r'<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>'
  39. )
  40. class EndpointEnumerator:
  41. """
  42. A class to determine the available API endpoints that a project exposes.
  43. """
  44. def __init__(self, patterns=None, urlconf=None):
  45. if patterns is None:
  46. if urlconf is None:
  47. # Use the default Django URL conf
  48. urlconf = settings.ROOT_URLCONF
  49. # Load the given URLconf module
  50. if isinstance(urlconf, str):
  51. urls = import_module(urlconf)
  52. else:
  53. urls = urlconf
  54. patterns = urls.urlpatterns
  55. self.patterns = patterns
  56. def get_api_endpoints(self, patterns=None, prefix=''):
  57. """
  58. Return a list of all available API endpoints by inspecting the URL conf.
  59. """
  60. if patterns is None:
  61. patterns = self.patterns
  62. api_endpoints = []
  63. for pattern in patterns:
  64. path_regex = prefix + get_original_route(pattern)
  65. if isinstance(pattern, URLPattern):
  66. path = self.get_path_from_regex(path_regex)
  67. callback = pattern.callback
  68. if self.should_include_endpoint(path, callback):
  69. for method in self.get_allowed_methods(callback):
  70. endpoint = (path, method, callback)
  71. api_endpoints.append(endpoint)
  72. elif isinstance(pattern, URLResolver):
  73. nested_endpoints = self.get_api_endpoints(
  74. patterns=pattern.url_patterns,
  75. prefix=path_regex
  76. )
  77. api_endpoints.extend(nested_endpoints)
  78. return sorted(api_endpoints, key=endpoint_ordering)
  79. def get_path_from_regex(self, path_regex):
  80. """
  81. Given a URL conf regex, return a URI template string.
  82. """
  83. # ???: Would it be feasible to adjust this such that we generate the
  84. # path, plus the kwargs, plus the type from the convertor, such that we
  85. # could feed that straight into the parameter schema object?
  86. path = simplify_regex(path_regex)
  87. # Strip Django 2.0 convertors as they are incompatible with uritemplate format
  88. return re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g<parameter>}', path)
  89. def should_include_endpoint(self, path, callback):
  90. """
  91. Return `True` if the given endpoint should be included.
  92. """
  93. if not is_api_view(callback):
  94. return False # Ignore anything except REST framework views.
  95. if callback.cls.schema is None:
  96. return False
  97. if 'schema' in callback.initkwargs:
  98. if callback.initkwargs['schema'] is None:
  99. return False
  100. if path.endswith('.{format}') or path.endswith('.{format}/'):
  101. return False # Ignore .json style URLs.
  102. return True
  103. def get_allowed_methods(self, callback):
  104. """
  105. Return a list of the valid HTTP methods for this endpoint.
  106. """
  107. if hasattr(callback, 'actions'):
  108. actions = set(callback.actions)
  109. http_method_names = set(callback.cls.http_method_names)
  110. methods = [method.upper() for method in actions & http_method_names]
  111. else:
  112. methods = callback.cls().allowed_methods
  113. return [method for method in methods if method not in ('OPTIONS', 'HEAD')]
  114. class BaseSchemaGenerator(object):
  115. endpoint_inspector_cls = EndpointEnumerator
  116. # 'pk' isn't great as an externally exposed name for an identifier,
  117. # so by default we prefer to use the actual model field name for schemas.
  118. # Set by 'SCHEMA_COERCE_PATH_PK'.
  119. coerce_path_pk = None
  120. def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None, version=None):
  121. if url and not url.endswith('/'):
  122. url += '/'
  123. self.coerce_path_pk = api_settings.SCHEMA_COERCE_PATH_PK
  124. self.patterns = patterns
  125. self.urlconf = urlconf
  126. self.title = title
  127. self.description = description
  128. self.version = version
  129. self.url = url
  130. self.endpoints = None
  131. def _initialise_endpoints(self):
  132. if self.endpoints is None:
  133. inspector = self.endpoint_inspector_cls(self.patterns, self.urlconf)
  134. self.endpoints = inspector.get_api_endpoints()
  135. def _get_paths_and_endpoints(self, request):
  136. """
  137. Generate (path, method, view) given (path, method, callback) for paths.
  138. """
  139. paths = []
  140. view_endpoints = []
  141. for path, method, callback in self.endpoints:
  142. view = self.create_view(callback, method, request)
  143. path = self.coerce_path(path, method, view)
  144. paths.append(path)
  145. view_endpoints.append((path, method, view))
  146. return paths, view_endpoints
  147. def create_view(self, callback, method, request=None):
  148. """
  149. Given a callback, return an actual view instance.
  150. """
  151. view = callback.cls(**getattr(callback, 'initkwargs', {}))
  152. view.args = ()
  153. view.kwargs = {}
  154. view.format_kwarg = None
  155. view.request = None
  156. view.action_map = getattr(callback, 'actions', None)
  157. actions = getattr(callback, 'actions', None)
  158. if actions is not None:
  159. if method == 'OPTIONS':
  160. view.action = 'metadata'
  161. else:
  162. view.action = actions.get(method.lower())
  163. if request is not None:
  164. view.request = clone_request(request, method)
  165. return view
  166. def coerce_path(self, path, method, view):
  167. """
  168. Coerce {pk} path arguments into the name of the model field,
  169. where possible. This is cleaner for an external representation.
  170. (Ie. "this is an identifier", not "this is a database primary key")
  171. """
  172. if not self.coerce_path_pk or '{pk}' not in path:
  173. return path
  174. model = getattr(getattr(view, 'queryset', None), 'model', None)
  175. if model:
  176. field_name = get_pk_name(model)
  177. else:
  178. field_name = 'id'
  179. return path.replace('{pk}', '{%s}' % field_name)
  180. def get_schema(self, request=None, public=False):
  181. raise NotImplementedError(".get_schema() must be implemented in subclasses.")
  182. def has_view_permissions(self, path, method, view):
  183. """
  184. Return `True` if the incoming request has the correct view permissions.
  185. """
  186. if view.request is None:
  187. return True
  188. try:
  189. view.check_permissions(view.request)
  190. except (exceptions.APIException, Http404, PermissionDenied):
  191. return False
  192. return True