ninenines / cowboy

Small, fast, modern HTTP server for Erlang/OTP.
https://ninenines.eu
ISC License
7.27k stars 1.16k forks source link

Reverse routing in Cowboy? #816

Open chreke opened 9 years ago

chreke commented 9 years ago

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 commented 9 years ago

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

chreke commented 9 years ago

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 commented 9 years ago

Thanks for the swift reply! :) On May 4, 2015 18:27, "Loïc Hoguin" notifications@github.com 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 https://github.com/ninenines/cowboy/issues/816#issuecomment-98772043.

bullno1 commented 9 years ago

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

How should it be done?

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 commented 9 years ago

Here are some suggestions:

essen commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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

zuiderkwast commented 9 years ago

The user POSTs to http://myapi.example.org/blogposts/123/comments content such as {"user": "http://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 commented 9 years ago

Yep, sounds good. Will be considered.

essen commented 7 years ago

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 commented 7 years ago

Ok, thanks for the update! Looking forward to 2.0 ! 🙂 On Sat, 10 Jun 2017 at 13:05, Loïc Hoguin notifications@github.com wrote:

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.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/ninenines/cowboy/issues/816#issuecomment-307558443, or mute the thread https://github.com/notifications/unsubscribe-auth/ABMY9vdBny5D5XAIkFOKWGuSMvvMypv1ks5sCniEgaJpZM4EPcxH .

essen commented 7 years ago

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

essen commented 4 years ago

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.