mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-07 13:54:47 +03:00
Handle schema structure correctly when path prefixs exist. Closes #4401
This commit is contained in:
parent
5858168d55
commit
69b4acd88f
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
@ -237,18 +238,63 @@ class SchemaGenerator(object):
|
||||||
included in the API schema.
|
included in the API schema.
|
||||||
"""
|
"""
|
||||||
links = OrderedDict()
|
links = OrderedDict()
|
||||||
|
|
||||||
|
# Generate (path, method, view) given (path, method, callback).
|
||||||
|
paths = []
|
||||||
|
view_endpoints = []
|
||||||
for path, method, callback in self.endpoints:
|
for path, method, callback in self.endpoints:
|
||||||
view = self.create_view(callback, method, request)
|
view = self.create_view(callback, method, request)
|
||||||
|
if getattr(view, 'exclude_from_schema', False):
|
||||||
|
continue
|
||||||
path = self.coerce_path(path, method, view)
|
path = self.coerce_path(path, method, view)
|
||||||
if not self.should_include_view(path, method, view):
|
paths.append(path)
|
||||||
|
view_endpoints.append((path, method, view))
|
||||||
|
|
||||||
|
# Only generate the path prefix for paths that will be included
|
||||||
|
prefix = self.determine_path_prefix(paths)
|
||||||
|
|
||||||
|
for path, method, view in view_endpoints:
|
||||||
|
if not self.has_view_permissions(path, method, view):
|
||||||
continue
|
continue
|
||||||
link = self.get_link(path, method, view)
|
link = self.get_link(path, method, view)
|
||||||
keys = self.get_keys(path, method, view)
|
subpath = path[len(prefix):]
|
||||||
|
keys = self.get_keys(subpath, method, view)
|
||||||
insert_into(links, keys, link)
|
insert_into(links, keys, link)
|
||||||
return links
|
return links
|
||||||
|
|
||||||
# Methods used when we generate a view instance from the raw callback...
|
# Methods used when we generate a view instance from the raw callback...
|
||||||
|
|
||||||
|
def determine_path_prefix(self, paths):
|
||||||
|
"""
|
||||||
|
Given a list of all paths, return the common prefix which should be
|
||||||
|
discounted when generating a schema structure.
|
||||||
|
|
||||||
|
This will be the longest common string that does not include that last
|
||||||
|
component of the URL, or the last component before a path parameter.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
/api/v1/users/
|
||||||
|
/api/v1/users/{pk}/
|
||||||
|
|
||||||
|
The path prefix is '/api/v1/'
|
||||||
|
"""
|
||||||
|
prefixes = []
|
||||||
|
for path in paths:
|
||||||
|
components = path.strip('/').split('/')
|
||||||
|
initial_components = []
|
||||||
|
for component in components:
|
||||||
|
if '{' in component:
|
||||||
|
break
|
||||||
|
initial_components.append(component)
|
||||||
|
prefix = '/'.join(initial_components[:-1])
|
||||||
|
if not prefix:
|
||||||
|
# We can just break early in the case that there's at least
|
||||||
|
# one URL that doesn't have a path prefix.
|
||||||
|
return '/'
|
||||||
|
prefixes.append('/' + prefix + '/')
|
||||||
|
return os.path.commonprefix(prefixes)
|
||||||
|
|
||||||
def create_view(self, callback, method, request=None):
|
def create_view(self, callback, method, request=None):
|
||||||
"""
|
"""
|
||||||
Given a callback, return an actual view instance.
|
Given a callback, return an actual view instance.
|
||||||
|
@ -274,13 +320,10 @@ class SchemaGenerator(object):
|
||||||
|
|
||||||
return view
|
return view
|
||||||
|
|
||||||
def should_include_view(self, path, method, view):
|
def has_view_permissions(self, path, method, view):
|
||||||
"""
|
"""
|
||||||
Return `True` if the incoming request has the correct view permissions.
|
Return `True` if the incoming request has the correct view permissions.
|
||||||
"""
|
"""
|
||||||
if getattr(view, 'exclude_from_schema', False):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if view.request is None:
|
if view.request is None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -291,6 +334,11 @@ class SchemaGenerator(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def coerce_path(self, path, method, view):
|
def coerce_path(self, path, method, view):
|
||||||
|
"""
|
||||||
|
Coerce {pk} path arguments into the name of the model field,
|
||||||
|
where possible. This is cleaner for an external representation.
|
||||||
|
(Ie. "this is an identifier", not "this is a database primary key")
|
||||||
|
"""
|
||||||
if not self.coerce_pk or '{pk}' not in path:
|
if not self.coerce_pk or '{pk}' not in path:
|
||||||
return path
|
return path
|
||||||
model = getattr(getattr(view, 'queryset', None), 'model', None)
|
model = getattr(getattr(view, 'queryset', None), 'model', None)
|
||||||
|
@ -461,7 +509,7 @@ class SchemaGenerator(object):
|
||||||
|
|
||||||
# Method for generating the link layout....
|
# Method for generating the link layout....
|
||||||
|
|
||||||
def get_keys(self, path, method, view):
|
def get_keys(self, subpath, method, view):
|
||||||
"""
|
"""
|
||||||
Return a list of keys that should be used to layout a link within
|
Return a list of keys that should be used to layout a link within
|
||||||
the schema document.
|
the schema document.
|
||||||
|
@ -478,14 +526,14 @@ class SchemaGenerator(object):
|
||||||
action = view.action
|
action = view.action
|
||||||
else:
|
else:
|
||||||
# Views have no associated action, so we determine one from the method.
|
# Views have no associated action, so we determine one from the method.
|
||||||
if is_list_view(path, method, view):
|
if is_list_view(subpath, method, view):
|
||||||
action = 'list'
|
action = 'list'
|
||||||
else:
|
else:
|
||||||
action = self.default_mapping[method.lower()]
|
action = self.default_mapping[method.lower()]
|
||||||
|
|
||||||
named_path_components = [
|
named_path_components = [
|
||||||
component for component
|
component for component
|
||||||
in path.strip('/').split('/')
|
in subpath.strip('/').split('/')
|
||||||
if '{' not in component
|
if '{' not in component
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -280,3 +280,56 @@ class TestSchemaGenerator(TestCase):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(schema, expected)
|
self.assertEqual(schema, expected)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||||
|
class TestSchemaGeneratorNotAtRoot(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.patterns = [
|
||||||
|
url('^api/v1/example/?$', ExampleListView.as_view()),
|
||||||
|
url('^api/v1/example/(?P<pk>\d+)/?$', ExampleDetailView.as_view()),
|
||||||
|
url('^api/v1/example/(?P<pk>\d+)/sub/?$', ExampleDetailView.as_view()),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_schema_for_regular_views(self):
|
||||||
|
"""
|
||||||
|
Ensure that schema generation with an API that is not at the URL
|
||||||
|
root continues to use correct structure for link keys.
|
||||||
|
"""
|
||||||
|
generator = SchemaGenerator(title='Example API', patterns=self.patterns)
|
||||||
|
schema = generator.get_schema()
|
||||||
|
expected = coreapi.Document(
|
||||||
|
url='',
|
||||||
|
title='Example API',
|
||||||
|
content={
|
||||||
|
'example': {
|
||||||
|
'create': coreapi.Link(
|
||||||
|
url='/api/v1/example/',
|
||||||
|
action='post',
|
||||||
|
fields=[]
|
||||||
|
),
|
||||||
|
'list': coreapi.Link(
|
||||||
|
url='/api/v1/example/',
|
||||||
|
action='get',
|
||||||
|
fields=[]
|
||||||
|
),
|
||||||
|
'retrieve': coreapi.Link(
|
||||||
|
url='/api/v1/example/{id}/',
|
||||||
|
action='get',
|
||||||
|
fields=[
|
||||||
|
coreapi.Field('id', required=True, location='path')
|
||||||
|
]
|
||||||
|
),
|
||||||
|
'sub': {
|
||||||
|
'list': coreapi.Link(
|
||||||
|
url='/api/v1/example/{id}/sub/',
|
||||||
|
action='get',
|
||||||
|
fields=[
|
||||||
|
coreapi.Field('id', required=True, location='path')
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(schema, expected)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user