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

Compile-time routes #38

Open
stiff opened this issue Oct 26, 2020 · 4 comments
Open

Compile-time routes #38

stiff opened this issue Oct 26, 2020 · 4 comments

Comments

@stiff
Copy link

stiff commented Oct 26, 2020

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?

@matt-42
Copy link
Owner

matt-42 commented Oct 27, 2020

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.

@stiff
Copy link
Author

stiff commented Oct 27, 2020

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.

@matt-42
Copy link
Owner

matt-42 commented Oct 27, 2020

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
});

@stiff
Copy link
Author

stiff commented Oct 28, 2020

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.

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

No branches or pull requests

2 participants