lithium icon indicating copy to clipboard operation
lithium copied to clipboard

Compile-time routes

Open stiff opened this issue 5 years ago • 4 comments

With recent additions in C++20 it is possible to write type-level compile-time routes that will accept only parameters of valid type, like this:

using AllPosts = Route< Slug<"blog">, Slug<"posts"> >;
using SinglePost = Route< Slug<"blog">, Slug<"posts">, Capture<int> >;

std::cout << "allPosts = " << AllPosts::toString() << std::endl;
// -> /blog/posts

// std::cout << "singlePost = " << SinglePost::toString() << std::endl;
// invalid path, compilation error

std::cout << "singlePost = " << SinglePost::toString(42) << std::endl;
// -> /blog/posts/42

Here is the proof of concept: https://gist.github.com/stiff/27607ddeb4da590d915423ba6779f18e . Sorry for some obvious room for optimisation :)

Think it's also possible to parse incoming requests in similar manner, is there a chance that'll get into Lithium, so we could define all routes in one place and get compile-time validation that they all genereated and handled just as specified?

stiff avatar Oct 26 '20 22:10 stiff

Hi Stiff

Thanks for your gist. It is similar to what I have implemented in another webframework : http://siliconframework.org/docs/apis.html but I dropped it for simplicity.

It has one advantage that the current version:

  • it declares the place of the url parameter and it's type in the same place.

The current url parameter reading are also typed checked but the types are provided when reading parameters:

// If the route is
"/blog/post/{{id}}"
[..]
// and if the handler read parameters as follow
auto params = url_parameters(s::id = int())
// this will throw if the targeter url does not provide a valid int.

However, if you do a typo in {{id}} or in s::id, the handler is invalid but still compiles.

Your example has one disadvantage over the current version: it is not using named parameters, so if your handler takes 4 integers as parameters it won't be trivial to remember which one is what in the handler. I guess this could be fixed using metamaps.

Another disadvantage is the readability of the code. This makes defining a route a lot more complex than just writing a plain strings, while not really providing a big advantage: in the current version, if a very simple test would fail and report any mismatch between s::id and {{id}}.

This would also require a lots of change in the core of the framework (including lots of templates, slowing down compilation): right now, all handlers have the same type. It we want to forward the static types of the route to the handler, we would have to switch to templated handlers, which would contaminate lots of the code.

Silicon http://siliconframework.org was actually using this approach but was 5x slower to compile and it's codebase was more complex. So I when I rewrote it, I decided to strip down all the complex meta programming stuff to get something that compiles faster and with a code base accessible to more c++ devs.

matt-42 avatar Oct 27 '20 08:10 matt-42

Sure each approach has it's own trade offs different from others, that's why I asked about the possibility of Lithium going in this direction.

Silicon is different from my proposed approach in that Silicon invents it's own syntax, and first glance of a person who only knows C++ will definitely raise questions like what's POST, what's _about, _id[int] - is it an array of size int), instead of using plain old c++20.

To simplify writing routes it should be possible to get rid of Slug and use FixedString right away like using SinglePost = Route< "blog", "posts", Capture<int> >;, but I haven't managed to get it compiled yet.

Naming arguments is a bit more tricky, I totally agree that it's very helpful to have named arguments, but it's not clear if it should go into types. After all programmers are allowed to name instances of type as they like.

Regarding compilation time, since it's done once, I prefer to let the computer do more work for me and check as much as possible. And to handle all this my initial though was to use std::visit of std::variant.

Also having all routes in types opens possibilities to generate clients for same API, without writing it again by hand with typos and without compiler check.

stiff avatar Oct 27 '20 10:10 stiff

Route< "blog", "posts", Capture<int> > is better yes.

For named parameters, we can use symbols: Route< "blog", "posts", Capture<s::id, int>>

I'm thinking about a way to integrate this to the framework as an additional way to declare routes. What do you think of this ?

using singlePostRoute = Route<"blog", "posts", Capture<s::id, int>>;
api.get<singlePostRoute>([&](li::http_request& request, li::http_response& response) {
  auto url_params = request.url_parameters<singlePostRoute>();
  // do something with url_params.id
});

matt-42 avatar Oct 27 '20 11:10 matt-42

I think I would try this in my project :)

I really like how parameters passed to handler function in Servant (like in toString example), for easier testing without any dependencies to http requests and responses. But separating responsibilities of performing the business logic and rendering response is probably quite a lot of changes.

stiff avatar Oct 28 '20 08:10 stiff