Skip to content
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

Reverse routing in Cowboy? #816

Open
chreke opened this issue May 4, 2015 · 16 comments
Open

Reverse routing in Cowboy? #816

chreke opened this issue May 4, 2015 · 16 comments

Comments

@chreke
Copy link

chreke commented May 4, 2015

Is it possible to do "reverse routing" in Cowboy, i.e. to generate a URL given a handler + bindings?

For example, given a handler (e.g. 'user_handler') and a set of URL bindings (e.g. [{user_id, 1234}]) is it possible to generate a URL that will resolve to the given handler + bindings? (e.g. "/users/1234“)

@essen
Copy link
Member

essen commented May 4, 2015

There is nothing for that at this point but I suppose it could be added. Sounds interesting.

@chreke
Copy link
Author

chreke commented May 4, 2015

A situation where this would be useful is when you're creating a REST API with HATEOAS, eg resources contain links to themselves and related resources.

@chreke
Copy link
Author

chreke commented May 4, 2015

Thanks for the swift reply! :)
On May 4, 2015 18:27, "Loïc Hoguin" [email protected] wrote:

There is nothing for that at this point but I suppose it could be added.
Sounds interesting.


Reply to this email directly or view it on GitHub
#816 (comment).

@bullno1
Copy link

bullno1 commented May 5, 2015

This is interesting, I'll see if I can help.

How should it be done?

  • cowboy_router:construct_uri("/users/:id", #{id => 30}) is nice but routes have to be reparsed.
  • cowboy_router:construct_uri(Req, user_route, #{id => 30}) might be faster, but some compiled routes passed to cowboy_router must have id attached to them. The "routing table" must be attached to the Req as a meta. This one is also better since a resource can be identified in multiple URIs but there is only one "canonical". Moreover, by changing a route in the routing list, you will change the returned URIs too, so there's no need to duplicate the change e.g: change how to route request to user_handler in cowboy's option and change the code that construct a route to user_handler.

Another problem is constraint. User-defined constraints can convert bindings from binary to some other type (even pids if you are doing websocket). It would be nice to let binding values be of the converted type too e.g: cowboy_router:construct_uri("/user/:id/:feed_ws", #{id => 30, feed_ws => Pid}). This would mean user-defined constraints have to be a pair of functions, one for binary->type and another for type->binary. Currently, cowboy allows one to specify only one constraint. What should be done when user wants to construct a constraint? throw(badarg)? throw(badconstraint)?

@chreke
Copy link
Author

chreke commented May 5, 2015

Here are some suggestions:

  • I suggest we give the function a name that builds on established conventions, something like url_for (from the Rails world) or reverse (in Django).
  • IMHO it makes the most sense to pass an atom representing a handler to the function, not a URL string. E.g. cowboy_router:reverse(user_handler, #{id => 1234}) and not cowboy_router:reverse("/users/:id", #{id => 1234}). This way, if we change the URL for user_handler we don't need to update the calls to reverse.
  • If the reverse function is passed a request, maybe we could return a fully qualified URL, e.g. https://www.myapi.com/users/1234 instead of just /users/1234?
  • As for handling constraints, maybe the best way is to only let the reverse function accept binaries, at least initially?
  • I would say it's acceptable to have the function fail for some routes, for example routes with wildcards. This is the same behaviour as Django / Rails.

@essen
Copy link
Member

essen commented May 5, 2015

Doing it is one thing, doing it efficiently is another.

Perhaps it would make sense to make the router compile to an ets table. But this means we wouldn't be able to update the dispatch dynamically in middlewares (so edge case I doubt anyone does it anyway).

With an ets table, we can use match or select functions to get only the routes that might fit, and avoid copying the whole dispatch onto all connection processes.

If we have this ets table then we can also use it when we want to do "reverse routing" without having to keep track of the dispatch in Req, saving memory again. The match function would be a little different but the idea is the same: we want all routes that give a handler and then we find the right one from there.

Nothing prevents us from making constraint functions accept different types. For example the constraint 'int' would say true for integers and for binaries containing integer values, and false otherwise. Though it's probably not that easy, especially since we want to use constraints in other places too like the new cowboy_req:match functions.

Food for thoughts.

@bullno1
Copy link

bullno1 commented May 5, 2015

I didn't know you can change route dynamically. I guess it's possible since it's just an environment. But IIRC the cowboy doc already said that one should use ranch:set_protocol_option/2 to update route (globally) anyway so I guess that should be the only way.

I still don't think having one function for both direction of conversion is a good idea. Say if I have a function that convert base64 to raw binary (twitter uuid for example), there wouldn't be an easy way to differentiate. Maybe we can use a pair of function like {Forward, Reverse} while still accepting a single function but throws on reverse. Built-in constraints like int should of course just work for both directions. Once could tag the parameter on reversal like: {reverse, 3} and have the function deals with it but that's effectively two functions and old constraints would mysteriously crash.

On fully qualified URL: I did have to return a host name different from the one in the request once. Nothing in HTTP or REST say you can't create a resource somewhere else anyway so I guess it should be optional.

ets sounds nice but I'm not sure how one would match a concrete piece of data against a list of patterns. Usually it's the other way around with ets, unless you are talking about just matching against the handler atom.

@essen
Copy link
Member

essen commented May 5, 2015

You can change the route but that's a hack. The proper way is as you say, so it should be fine.

Yes my idea is that we simply match against the handler atom and then have some logic to do the rest. What that logic is remains to be determined, of course.

As for constraints, you're right. Perhaps we need a function that receives 2 arguments instead of 1, and we can use the second argument to give context ie say that we are in 'normal' or 'reverse' mode or something. Not sure, this is a tough one.

@zuiderkwast
Copy link
Contributor

A related thing of interest for HATEOAS is parsing URLs using the router rules without doing actual routing. Exporting cowboy_router:match/3 would do. Any plans for this?

@essen
Copy link
Member

essen commented May 22, 2015

What's the use case for that? What do you do with the info?

@zuiderkwast
Copy link
Contributor

The user POSTs to https://myapi.example.org/blogposts/123/comments content such as {"user": "https://myapi.example.org/users/42", "comment": "Bla bla bla."}. I want to parse the "user" URL to get the ID number 42 to look it up in a database. The idea of HATEOAS is to URLs like this in the API instead of the internal IDs.

@essen
Copy link
Member

essen commented May 22, 2015

Yep, sounds good. Will be considered.

@essen essen modified the milestone: 2.0.0 Aug 18, 2015
@essen essen modified the milestones: 2.0.0, 2.0 Breaking Changes Feb 3, 2017
@essen
Copy link
Member

essen commented Jun 10, 2017

I think the functionality of reverse routing would be too limited, if implemented. The problem is that we can't go from "handler module + bindings" to getting the host (unless it's used exactly once), that wildcards would cause problems, that reusing handlers for more than one path would not be resolvable (for example if you have generic handlers and reuse them for many different types of resources). I do not think it wise to implement something that leaves so much uncertainty in its behavior, and only works well if routes are defined a certain way.

On the other hand exposing cowboy_router:match is still a consideration and will happen, perhaps not in 2.0 but soon after.

@chreke
Copy link
Author

chreke commented Jun 11, 2017 via email

@essen
Copy link
Member

essen commented Jun 28, 2017

I've done both reverse constraints in c221730. Feedback welcome!

@essen essen removed this from the 2.0 New Features milestone Oct 2, 2017
@essen
Copy link
Member

essen commented Dec 19, 2019

I've implemented URI templates in Cowlib https://github.com/ninenines/cowlib/blob/master/src/cow_uri_template.erl

It is one step in the reverse routing direction. I am considering moving the router to use the URI template syntax + constraints, either in 3.0 or optionally for now. Then the template can always be expanded, perhaps via a cowboy_req function so that the host/scheme/.. can be added if it's not present in the template (similar to what cowboy_req:uri is already doing).

Anyway it's still a long way off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants