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:
Kurt Griffiths
2016-09-14 10:49:32 -06:00
committed by Fran Fitzpatrick
parent 18beacf2a8
commit c986dd1aa5
6 changed files with 96 additions and 39 deletions

View File

@@ -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

View File

@@ -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."""

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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)