-
-
Notifications
You must be signed in to change notification settings - Fork 951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a helper to generate the URL for a resource #1264
Comments
(Please comment below re a design proposal if you are interested in taking this on, thanks!) |
Much of the way that The basic premise within that code is to utilize the I can understand that people do want it, but why do people want it? Is generating a URL based on resource a required competency of an API framework? Flask has Jinjna2 bundled for generating HTML so A reason I could be confused is that, "inter-resource delegation"/"to facilitate one resource calling into another" and |
Places I miss
Without something like |
I agree. Perhaps this issue could be renamed if it is about generating the URLs for resources? Given how much weight Roy Fielding puts on link-driven APIs I'm surprised that a REST-focused framework like Falcon does not have a way to create a URL for a resource. E.g, for some hypothetical API, if I query edit: here is essentially how I'm doing it now: class ProductResource(object):
def on_get(self, req, resp):
urljoin = urllib.parse.urljoin
quote = urllib.parse.quote
data = [{'name': prod.name,
'url': urljoin(req.prefix, 'products/' + quote(prod.id))}
for prod in self.products]
resp.media = data
resp.status = falcon.HTTP_OK I'm not primarily a web-developer so I'm not sure how good that is. And what if |
Since #1228 speaks to delegating a response to another resource/route, I think it makes sense to rename this issue to focus solely on generating the hyperlink. |
I've noticed the url_for is on roadmap for 4.1.0. I have a working implementation of url_for in falcon and quite like it. It's used for links and html-endpoints (they are actually good despite the the json-focused nature of the falcon). If there is an interest, I could share some design points. The url_for is implemented by subclassing (ok, hacking) the stock CompiledRouter. |
I think it would be useful, sure! |
Ok, here it is. Will help me too to document the feature ) Requirements:
Implementation
class ThingsEndpoint:
def on_get(req, resp, *, thing_id: str, table_id: int): ...
things_ep = ThingsEndpoint()
router.add_route(Url("/api") / "things" / Arg("thing_id") / IntArg("table_id", max=100), things_ep)
def _set_route(responder: Callable, route: Url):
cls_inst = responder.__self__
unbound = responder.__func__
cls_inst._routes[unbound] = route
def _get_route(responder: Callable):
cls_inst = responder.__self__
unbound = responder.__func__
return cls_inst._routes[unbound]
def url_for(responder: Callable[Concatenate[Any, Any, P], None], *args: P.args, **kwargs: P.kwargs) -> str:
return _get_route(responder).interpolate(*args, **kwargs) This magic typing ensures the url_for arguments match the responder signature url_for(things_ep.on_get, thing_id='a', table_id=2)
>>> '/api/things/a/2'
url_for(things_ep.on_get, thing_id='a') # Missing table id
class MyRouter(CompiledRouter):
# note the Url instead of the string
def add_route(self, url: Url, resource: object, **kwargs: Any) -> None:
responders_map = map_http_methods(resource, kwargs.get("suffix"))
# store the urls at responders class instance
for r in responders_map.values():
_set_route(r, url)
# the url is stringified here for CompiledRouter
return super().add_route(str(url), resource, **kwargs) |
Interesting idea, thanks for sharing it! As an api for falcon I think we shouldn't force use users to use path segments as the url like in your implementation. Also the mapping instances - routes is likely better if stored in the app, so that we don't require or alter the instances of the responder classes provided by the users. |
Sure thing, the segments-as-objects would be a HUGE breaking change ) |
... regarding passing around the resource instances, there are ways to make it more convenient. class BaseEp:
def __init__(self, app: MyAppWithEpRegistryAndServices)
self.app = app
class ThingEp(BaseEp):
...
class OtherThingEp(BaseEp):
def on_get(...):
thing_id = self.app.db.fetch(...)
url = url_for(self.app.thing_ep.on_post, thing_id=thing_id)
class MyAppWithEpRegistryAndServices:
def __init__(self):
self.db = SqliteDb()
self.thing_ep = ThingEp(self)
self.other_thing_ep = OtherThingEp(self)
self.falcon = falcon.App(...)
self.falcon.add_route(Url(...), self.thing_ep)
self.falcon.add_route(Url(...), self.other_thing_ep)
app = MyAppWithEpRegistryAndServices()
make_server(app.falcon).serve_forever() The best thing is VSCode autocompletion here. Typing |
As a REST-focused framework, Falcon really should make it easier to generate a URL for a given resource. This will help encourage the use of hypermedia by lowering the barrier to entry.
Prior Art
The text was updated successfully, but these errors were encountered: