feat(Request): Expose the URI template that was matched for the request (#889)
This has been requested by several members of the community who would like a way to access the URI template in middleware, etc. (e.g., for logging). Fixes #619
This commit is contained in:
committed by
Fran Fitzpatrick
parent
18beacf2a8
commit
c986dd1aa5
@@ -24,14 +24,15 @@ A custom router is any class that implements the following interface:
|
||||
"""
|
||||
|
||||
def find(self, uri):
|
||||
"""Search for a route that matches the given URI.
|
||||
"""Search for a route that matches the given partial URI.
|
||||
|
||||
Args:
|
||||
uri (str): Request URI to match to a route.
|
||||
uri(str): The requested path to route
|
||||
|
||||
Returns:
|
||||
tuple: A 3-member tuple composed of (resource, method_map, params)
|
||||
or ``None`` if no route is found.
|
||||
tuple: A 4-member tuple composed of (resource, method_map,
|
||||
params, uri_template), or ``None`` if no route matches
|
||||
the requested path
|
||||
"""
|
||||
|
||||
A custom routing engine may be specified when instantiating
|
||||
|
||||
@@ -189,7 +189,7 @@ class API(object):
|
||||
# next-hop child resource. In that case, the object
|
||||
# being asked to dispatch to its child will raise an
|
||||
# HTTP exception signalling the problem, e.g. a 404.
|
||||
responder, params, resource = self._get_responder(req)
|
||||
responder, params, resource, req.uri_template = self._get_responder(req)
|
||||
except Exception as ex:
|
||||
if not self._handle_exception(ex, req, resp, params):
|
||||
raise
|
||||
@@ -508,11 +508,18 @@ class API(object):
|
||||
|
||||
path = req.path
|
||||
method = req.method
|
||||
uri_template = None
|
||||
|
||||
route = self._router.find(path)
|
||||
|
||||
if route is not None:
|
||||
resource, method_map, params = route
|
||||
try:
|
||||
resource, method_map, params, uri_template = route
|
||||
except ValueError:
|
||||
# NOTE(kgriffs): Older routers may not return the
|
||||
# template. But for performance reasons they should at
|
||||
# least return None if they don't support it.
|
||||
resource, method_map, params = route
|
||||
else:
|
||||
# NOTE(kgriffs): Older routers may indicate that no route
|
||||
# was found by returning (None, None, None). Therefore, we
|
||||
@@ -538,7 +545,7 @@ class API(object):
|
||||
else:
|
||||
responder = falcon.responders.path_not_found
|
||||
|
||||
return (responder, params, resource)
|
||||
return (responder, params, resource, uri_template)
|
||||
|
||||
def _compose_status_response(self, req, resp, http_status):
|
||||
"""Composes a response for the given HTTPStatus instance."""
|
||||
|
||||
@@ -68,6 +68,8 @@ class Request(object):
|
||||
Args:
|
||||
env (dict): A WSGI environment dict passed in from the server. See
|
||||
also PEP-3333.
|
||||
|
||||
Keyword Arguments
|
||||
options (dict): Set of global options passed from the API handler.
|
||||
|
||||
Attributes:
|
||||
@@ -140,7 +142,6 @@ class Request(object):
|
||||
opposed to a class), the function is called like a method of
|
||||
the current Request instance. Therefore the first argument is
|
||||
the Request instance itself (self).
|
||||
|
||||
uri (str): The fully-qualified URI for the request.
|
||||
url (str): alias for `uri`.
|
||||
relative_uri (str): The path + query string portion of the full URI.
|
||||
@@ -148,6 +149,13 @@ class Request(object):
|
||||
string).
|
||||
query_string (str): Query string portion of the request URL, without
|
||||
the preceding '?' character.
|
||||
uri_template (str): The template for the route that was matched for
|
||||
this request. May be ``None`` if the request has not yet been
|
||||
routed, as would be the case for `process_request()` middleware
|
||||
methods. May also be ``None`` if your app uses a custom routing
|
||||
engine and the engine does not provide the URI template when
|
||||
resolving a route.
|
||||
|
||||
user_agent (str): Value of the User-Agent header, or ``None`` if the
|
||||
header is missing.
|
||||
accept (str): Value of the Accept header, or '*/*' if the header is
|
||||
@@ -244,6 +252,7 @@ class Request(object):
|
||||
'_cookies',
|
||||
'_cached_access_route',
|
||||
'__dict__',
|
||||
'uri_template',
|
||||
)
|
||||
|
||||
# Child classes may override this
|
||||
@@ -259,6 +268,8 @@ class Request(object):
|
||||
self.stream = env['wsgi.input']
|
||||
self.method = env['REQUEST_METHOD']
|
||||
|
||||
self.uri_template = None
|
||||
|
||||
# Normalize path
|
||||
path = env['PATH_INFO']
|
||||
if path:
|
||||
|
||||
@@ -43,7 +43,15 @@ class CompiledRouter(object):
|
||||
self._return_values = None
|
||||
|
||||
def add_route(self, uri_template, method_map, resource):
|
||||
"""Adds a route between URI path template and resource."""
|
||||
"""Adds a route between a URI path template and a resource.
|
||||
|
||||
Args:
|
||||
uri_template (str): A URI template to use for the route
|
||||
method_map (dict): A mapping of HTTP methods (e.g., 'GET',
|
||||
'POST') to methods of a resource object.
|
||||
resource (object): The resource instance to associate with
|
||||
the URI template.
|
||||
"""
|
||||
|
||||
if re.search('\s', uri_template):
|
||||
raise ValueError('URI templates may not include whitespace.')
|
||||
@@ -67,6 +75,7 @@ class CompiledRouter(object):
|
||||
# NOTE(kgriffs): Override previous node
|
||||
node.method_map = method_map
|
||||
node.resource = resource
|
||||
node.uri_template = uri_template
|
||||
else:
|
||||
insert(node.children, path_index)
|
||||
|
||||
@@ -85,6 +94,7 @@ class CompiledRouter(object):
|
||||
if path_index == len(path) - 1:
|
||||
new_node.method_map = method_map
|
||||
new_node.resource = resource
|
||||
new_node.uri_template = uri_template
|
||||
else:
|
||||
insert(new_node.children, path_index + 1)
|
||||
|
||||
@@ -92,13 +102,23 @@ class CompiledRouter(object):
|
||||
self._find = self._compile()
|
||||
|
||||
def find(self, uri):
|
||||
"""Finds resource and method map for a URI, or returns None."""
|
||||
"""Search for a route that matches the given partial URI.
|
||||
|
||||
Args:
|
||||
uri(str): The requested path to route
|
||||
|
||||
Returns:
|
||||
tuple: A 4-member tuple composed of (resource, method_map,
|
||||
params, uri_template), or ``None`` if no route matches
|
||||
the requested path
|
||||
"""
|
||||
|
||||
path = uri.lstrip('/').split('/')
|
||||
params = {}
|
||||
node = self._find(path, self._return_values, self._expressions, params)
|
||||
|
||||
if node is not None:
|
||||
return node.resource, node.method_map, params
|
||||
return node.resource, node.method_map, params, node.uri_template
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -234,12 +254,14 @@ class CompiledRouterNode(object):
|
||||
|
||||
_regex_vars = re.compile('{([-_a-zA-Z0-9]+)}')
|
||||
|
||||
def __init__(self, raw_segment, method_map=None, resource=None):
|
||||
def __init__(self, raw_segment,
|
||||
method_map=None, resource=None, uri_template=None):
|
||||
self.children = []
|
||||
|
||||
self.raw_segment = raw_segment
|
||||
self.method_map = method_map
|
||||
self.resource = resource
|
||||
self.uri_template = uri_template
|
||||
|
||||
self.is_var = False
|
||||
self.is_complex = False
|
||||
|
||||
@@ -21,14 +21,20 @@ class TestCustomRouter(testing.TestBase):
|
||||
def test_custom_router_find_should_be_used(self):
|
||||
|
||||
def resource(req, resp, **kwargs):
|
||||
resp.body = '{"status": "ok"}'
|
||||
resp.body = '{{"uri_template": "{0}"}}'.format(req.uri_template)
|
||||
|
||||
class CustomRouter(object):
|
||||
def __init__(self):
|
||||
self.reached_backwards_compat = False
|
||||
|
||||
def find(self, uri):
|
||||
if uri == '/test':
|
||||
if uri == '/test/42':
|
||||
return resource, {'GET': resource}, {}, '/test/{id}'
|
||||
|
||||
if uri == '/test/42/no-uri-template':
|
||||
return resource, {'GET': resource}, {}, None
|
||||
|
||||
if uri == '/test/42/uri-template/backwards-compat':
|
||||
return resource, {'GET': resource}, {}
|
||||
|
||||
if uri == '/404/backwards-compat':
|
||||
@@ -39,8 +45,14 @@ class TestCustomRouter(testing.TestBase):
|
||||
|
||||
router = CustomRouter()
|
||||
self.api = falcon.API(router=router)
|
||||
body = self.simulate_request('/test')
|
||||
self.assertEqual(body, [b'{"status": "ok"}'])
|
||||
body = self.simulate_request('/test/42')
|
||||
self.assertEqual(body, [b'{"uri_template": "/test/{id}"}'])
|
||||
|
||||
body = self.simulate_request('/test/42/no-uri-template')
|
||||
self.assertEqual(body, [b'{"uri_template": "None"}'])
|
||||
|
||||
body = self.simulate_request('/test/42/uri-template/backwards-compat')
|
||||
self.assertEqual(body, [b'{"uri_template": "None"}'])
|
||||
|
||||
for uri in ('/404', '/404/backwards-compat'):
|
||||
body = self.simulate_request(uri)
|
||||
|
||||
@@ -24,18 +24,18 @@ class TestRegressionCases(testing.TestBase):
|
||||
def test_versioned_url(self):
|
||||
self.router.add_route('/{version}/messages', {}, ResourceWithId(2))
|
||||
|
||||
resource, method_map, params = self.router.find('/v2/messages')
|
||||
resource, __, __, __ = self.router.find('/v2/messages')
|
||||
self.assertEqual(resource.resource_id, 2)
|
||||
|
||||
self.router.add_route('/v2', {}, ResourceWithId(1))
|
||||
|
||||
resource, method_map, params = self.router.find('/v2')
|
||||
resource, __, __, __ = self.router.find('/v2')
|
||||
self.assertEqual(resource.resource_id, 1)
|
||||
|
||||
resource, method_map, params = self.router.find('/v2/messages')
|
||||
resource, __, __, __ = self.router.find('/v2/messages')
|
||||
self.assertEqual(resource.resource_id, 2)
|
||||
|
||||
resource, method_map, params = self.router.find('/v1/messages')
|
||||
resource, __, __, __ = self.router.find('/v1/messages')
|
||||
self.assertEqual(resource.resource_id, 2)
|
||||
|
||||
route = self.router.find('/v1')
|
||||
@@ -47,10 +47,10 @@ class TestRegressionCases(testing.TestBase):
|
||||
self.router.add_route(
|
||||
'/recipes/baking', {}, ResourceWithId(2))
|
||||
|
||||
resource, method_map, params = self.router.find('/recipes/baking/4242')
|
||||
resource, __, __, __ = self.router.find('/recipes/baking/4242')
|
||||
self.assertEqual(resource.resource_id, 1)
|
||||
|
||||
resource, method_map, params = self.router.find('/recipes/baking')
|
||||
resource, __, __, __ = self.router.find('/recipes/baking')
|
||||
self.assertEqual(resource.resource_id, 2)
|
||||
|
||||
route = self.router.find('/recipes/grilling')
|
||||
@@ -165,20 +165,20 @@ class TestComplexRouting(testing.TestBase):
|
||||
def test_override(self):
|
||||
self.router.add_route('/emojis/signs/0', {}, ResourceWithId(-1))
|
||||
|
||||
resource, method_map, params = self.router.find('/emojis/signs/0')
|
||||
resource, __, __, __ = self.router.find('/emojis/signs/0')
|
||||
self.assertEqual(resource.resource_id, -1)
|
||||
|
||||
def test_literal_segment(self):
|
||||
resource, method_map, params = self.router.find('/emojis/signs/0')
|
||||
resource, __, __, __ = self.router.find('/emojis/signs/0')
|
||||
self.assertEqual(resource.resource_id, 12)
|
||||
|
||||
resource, method_map, params = self.router.find('/emojis/signs/1')
|
||||
resource, __, __, __ = self.router.find('/emojis/signs/1')
|
||||
self.assertEqual(resource.resource_id, 13)
|
||||
|
||||
resource, method_map, params = self.router.find('/emojis/signs/42')
|
||||
resource, __, __, __ = self.router.find('/emojis/signs/42')
|
||||
self.assertEqual(resource.resource_id, 14)
|
||||
|
||||
resource, method_map, params = self.router.find('/emojis/signs/42/small')
|
||||
resource, __, __, __ = self.router.find('/emojis/signs/42/small')
|
||||
self.assertEqual(resource.resource_id, 14.1)
|
||||
|
||||
route = self.router.find('/emojis/signs/1/small')
|
||||
@@ -204,18 +204,18 @@ class TestComplexRouting(testing.TestBase):
|
||||
self.assertIs(route, None)
|
||||
|
||||
def test_literal(self):
|
||||
resource, method_map, params = self.router.find('/user/memberships')
|
||||
resource, __, __, __ = self.router.find('/user/memberships')
|
||||
self.assertEqual(resource.resource_id, 8)
|
||||
|
||||
def test_variable(self):
|
||||
resource, method_map, params = self.router.find('/teams/42')
|
||||
resource, __, params, __ = self.router.find('/teams/42')
|
||||
self.assertEqual(resource.resource_id, 6)
|
||||
self.assertEqual(params, {'id': '42'})
|
||||
|
||||
resource, method_map, params = self.router.find('/emojis/signs/stop')
|
||||
__, __, params, __ = self.router.find('/emojis/signs/stop')
|
||||
self.assertEqual(params, {'id': 'stop'})
|
||||
|
||||
resource, method_map, params = self.router.find('/gists/42/raw')
|
||||
__, __, params, __ = self.router.find('/gists/42/raw')
|
||||
self.assertEqual(params, {'id': '42'})
|
||||
|
||||
@ddt.data(
|
||||
@@ -232,7 +232,7 @@ class TestComplexRouting(testing.TestBase):
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_literal_vs_variable(self, path, expected_id):
|
||||
resource, method_map, params = self.router.find(path)
|
||||
resource, __, __, __ = self.router.find(path)
|
||||
self.assertEqual(resource.resource_id, expected_id)
|
||||
|
||||
@ddt.data(
|
||||
@@ -271,12 +271,12 @@ class TestComplexRouting(testing.TestBase):
|
||||
self.assertIs(route, None)
|
||||
|
||||
def test_multivar(self):
|
||||
resource, method_map, params = self.router.find(
|
||||
resource, __, params, __ = self.router.find(
|
||||
'/repos/racker/falcon/commits')
|
||||
self.assertEqual(resource.resource_id, 4)
|
||||
self.assertEqual(params, {'org': 'racker', 'repo': 'falcon'})
|
||||
|
||||
resource, method_map, params = self.router.find(
|
||||
resource, __, params, __ = self.router.find(
|
||||
'/repos/racker/falcon/compare/all')
|
||||
self.assertEqual(resource.resource_id, 11)
|
||||
self.assertEqual(params, {'org': 'racker', 'repo': 'falcon'})
|
||||
@@ -285,7 +285,7 @@ class TestComplexRouting(testing.TestBase):
|
||||
@ddt.unpack
|
||||
def test_complex(self, url_postfix, resource_id):
|
||||
uri = '/repos/racker/falcon/compare/johndoe:master...janedoe:dev'
|
||||
resource, method_map, params = self.router.find(uri + url_postfix)
|
||||
resource, __, params, __ = self.router.find(uri + url_postfix)
|
||||
|
||||
self.assertEqual(resource.resource_id, resource_id)
|
||||
self.assertEqual(params, {
|
||||
@@ -297,11 +297,14 @@ class TestComplexRouting(testing.TestBase):
|
||||
'branch1': 'dev',
|
||||
})
|
||||
|
||||
@ddt.data(('', 16), ('/full', 17))
|
||||
@ddt.data(
|
||||
('', 16, '/repos/{org}/{repo}/compare/{usr0}:{branch0}'),
|
||||
('/full', 17, '/repos/{org}/{repo}/compare/{usr0}:{branch0}/full')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_complex_alt(self, url_postfix, resource_id):
|
||||
uri = '/repos/falconry/falcon/compare/johndoe:master'
|
||||
resource, method_map, params = self.router.find(uri + url_postfix)
|
||||
def test_complex_alt(self, url_postfix, resource_id, expected_template):
|
||||
uri = '/repos/falconry/falcon/compare/johndoe:master' + url_postfix
|
||||
resource, __, params, uri_template = self.router.find(uri)
|
||||
|
||||
self.assertEqual(resource.resource_id, resource_id)
|
||||
self.assertEqual(params, {
|
||||
@@ -310,3 +313,4 @@ class TestComplexRouting(testing.TestBase):
|
||||
'usr0': 'johndoe',
|
||||
'branch0': 'master',
|
||||
})
|
||||
self.assertEqual(uri_template, expected_template)
|
||||
|
||||
Reference in New Issue
Block a user