I'm trying to implement a feature in my API where a part of a URL is optional. If supplied, I'd like to process it and stick some data in g. If not, I'd put some default info in g. Either way, I'd then remove it from the URL before Rules are mapped to endpoints. So I'd like the following two URLs to end up calling the same endpoint:
/bar/1 (I would fill in a default value for foo here)
/foo/32/bar/1
I want this same optional piece of URL in every endpoint I have. I think I could do this by brute force by decorating every endpoint but I have over 250 of them so I'd like something more elegant.
I'm using multiple Blueprints and I'd like to leave each endpoint as simple as I already have them if possible (the blueprints already have their own prefixes):
@blueprint1.route('/bar/<int:id>', methods=['GET'])
@blueprint2.route('/bar/<int:id>', methods=['GET'])
def get_foo():
I've tried the @url_defaults, @url_value_preprocessor, and @before_request decorators but it seems the Rule has already been mapped to the endpoint by then. Is there a hook to access the URL before mapping is done?
I made this work by subclassing the Flask class and overriding the create_url_adapter() function like so:
class MyFlask(Flask):
"""
MyFlask subclasses the standard Flask app class so that I can hook the URL parsing before the URL
is mapped to a Route. We do this so we can extract an optional "/foo/<int:foo_id>" prefix. If we
see this prefix, we store the int value for later processing and delete the prefix from the URL before
Flask maps it to a route.
"""
def _extract_optional_foo_id(self, path):
....
return path, foo_id # path has the optional part removed
def create_url_adapter(self, request):
"""
Augment the base class's implementation of create_url_adapter() by extracting an optional foo_id and modifying
the URL. The Flask function name is misleading: we aren't creating anything like an object. The "adapter" is
just this function call. We add our own behavior then call the super class's version of this function.
:param request: Flask's Request object
:return: the results of the Flask super class's create_url_adapter()
"""
# Must test for request. It is None when calling this for the app-wide scenario (instead of request-wide).
if request and request.environ:
(new_path, foo_id) = self._extract_optional_foo_id(request.environ['PATH_INFO'])
if foo_id is not None:
request.environ['PATH_INFO'] = new_path
request.foo_id_in_url = foo_id
return super(MyFlask, self).create_url_adapter(request)
Then in my app init code, instead of instantiating an instance of Flask, I do:
app = MyFlask( ... )