whaleroute - is a C++17 header-only library for request routing. It is designed to bind handlers to HTTP requests,
but it can also be easily used with other protocols since the library is implemented as a generic class template.
If your incoming data processing function has a signature like void(const Request&, Response&)
or Response(const Request&)
and you need to perform
different actions based on a string value from the request object, whaleroute can be of great help.
Let's say that our Request and Response classes look like this:
struct Request{
std::string uri;
enum class Method{
GET,
POST
} method;
};
struct Response{
bool isSent = false;
void send(const std::string& data)
{
isSent = true;
std::cout << data;
}
};
To create the simplest router we must use Request and Response types as template arguments and implement two virtual functions:
virtual std::string getRequestPath(const TRequest&) = 0;
virtual void processUnmatchedRequest(const TRequest&, TResponse&) = 0;
#include <whaleroute/requestrouter.h>
class Router : public whaleroute::RequestRouter<Request, Response>{
std::string getRequestPath(const Request& request) final
{
return request.uri;
}
void processUnmatchedRequest(const Request&, Response& response) final
{
response.send("HTTP/1.1 404 Not Found\r\n\r\n");
}
};
Now our router can be used like this:
auto router = Router{};
router.route("/").process([](const Request& request, Response& response){
response.send("HTTP/1.1 200 OK\r\n\r\n");
});
//...
router.process(request, response);
The process
method accepts any callable that can be invoked with the registered request and response objects.
Therefore, in addition to lambdas, it is possible to use free functions and function objects. It is also possible to
specify the type of the invocable class, allowing whaleroute to instantiate and take ownership of the request
processor
object.
struct Responder{
void operator()(const Request&, Response& response)
{
response.send("HTTP/1.1 200 OK\r\n\r\n");
}
};
auto router = Router{};
router.route("/").process<Responder>();
router.process(request, response);
To simplify the setup of routes, it is possible to set the response value directly instead of registering a
request processor object. To achieve this, it is necessary to register a response converter that sets the passed value
to the response object. To do this, provide a callable structure with the following
signature: void(Response& response, const TValue& value)
, and pass it as the 3rd template argument
to whaleroute::RequestRouter
.
#include <whaleroute/requestrouter.h>
struct ResponseSetter{
void operator()(Response& response, const std::string& value)
{
response.send(value);
}
};
class Router : public whaleroute::RequestRouter<Request, Response, ResponseSetter>{
std::string getRequestPath(const TRequest& request) final
{
return request.uri;
}
void processUnmatchedRequest(const Request&, Response& response)
{
response.send("HTTP/1.1 418 I'm a teapot\r\n\r\n");
}
};
Now routes have the set
method available:
auto router = Router{};
router.route("/").set("HTTP/1.1 200 OK\r\n\r\n");
//...
router.process(request, response);
When a response converter is set, it is also possible to use request processors that return response values instead of taking a reference to the response object.
struct Responder{
std::string operator()(const Request&)
{
return "HTTP/1.1 200 OK\r\n\r\n";
}
};
auto router = Router{};
router.route("/").process<Responder>();
router.process(request, response);
By default, route processing stops at the first registered route that matches the request's path and any specified matchers. This behavior can be controlled by using the router's virtual function
isRouteProcessingFinished(const TRequest&, TResponse& response)
.
It's invoked for each matched route after the registered handler, and if the result is false, the processing continues
and tries to match the subsequent routes.
A practical example is to stop route processing only after the response has been sent. This allows you to register
request processors that perform some preparation work before generating the response.
class Router : public whaleroute::RequestRouter<Request, Response>{
std::string getRequestPath(const Request& request) final
{
return request.uri;
}
void processUnmatchedRequest(const Request&, Response& response) final
{
response.send("HTTP/1.1 404 Not Found\r\n\r\n");
}
bool isRouteProcessingFinished(const Request&, Response& response) final
{
return response.isSent;
}
};
auto router = Router{};
router.route(whaleroute::rx{".*"}, Request::Method::GET).process(const Request& request, Response&)){
log(request);
});
router.route("/").process([](const Request&, Response& response)){
response.send("HTTP/1.1 200 OK\r\n\r\n");
});
To make the matching of multiple routes more useful, it is possible to share data between route processors. This can be achieved by registering the route context class as the last template parameter of the whaleroute::RequestRouter template. Once registered, it is then possible to take a reference to the context object in the last request processor parameter.
#include <whaleroute/requestrouter.h>
struct Context{
bool isAuthorized = false;
};
class Router : public whaleroute::RequestRouter<Request, Response, whaleroute::_, Context>{
//...
};
void authorize(const Request& request, Response&, Context& ctx)
{
ctx.isAuthorized = true;
}
router.route(whaleroute::rx{".*"}, Request::Method::POST).process(authorize);
router.route("/").process([](const Request&, Response& response, const Context& ctx)){
if (ctx.isAuthorized)
response.send("HTTP/1.1 200 OK\r\n\r\n");
else
response.send("HTTP/1.1 401 Unauthorized\r\n\r\n");
});
Notice how it's possible to skip using the response converter in this example by passing the empty type whaleroute::_
in
its place.
By default, routes are matched based on request paths. To include other matching attributes, you can register them by
providing a specialization of the whaleroute::config::RouteMatcher
class template. The operator()
function within
the specialization should take a matcher's value and the request object, and return a boolean result of comparing
the matcher with some property of the request object.
#include <whaleroute/routematcher.h>
template<>
struct RouteMatcher<Request::Method> {
bool operator()(Request::Method value, const Request& request) const
{
return value == request.method;
}
};
}
Now Request::Method
can be specified in routes:
auto router = Router{};
router.route("/", Request::Method::GET).process([](const Request& request, Response& response)){
response.send("HTTP/1.1 200 OK\r\n\r\n");
});
When router has a registered context, it must be present in the route matcher specialization:
template<>
struct RouteMatcher<Request::Method, Context> {
bool operator()(Request::Method value, const Request& request, const Context& ctx) const
{
return value == request.method;
}
};
The route
method of the Router can accept a regular expression instead of a string to specify the path of the route:
router.route(whaleroute::rx{"/.*"}, Request::Method::GET).set("HTTP/1.1 200 OK\r\n\r\n");
Currently, the regular expressions use the standard C++ library with ECMAScript grammar.
When using regular expressions, request processors can accept additional parameters to capture the values of expression capturing groups.
void showPage(int pageNumber, const Request&, Response& response)
{
response.send("page" + std::to_string(pageNumber));
}
router.route(whaleroute::rx{"/page/(\\d+)"}, Request::Method::GET).process(showPage);
The conversion of strings from the capturing groups to the parameters of the request processor is performed using the
standard std::stringstream
stream. If the conversion is not possible, a runtime error will be raised. To support the
conversion of user-defined types, you can use the specialization of whaleroute::config::StringConverter
.
struct PageNumber{
int value;
};
template<>
struct whaleroute::config::StringConverter<PageNumber> {
static std::optional<PageNumber> fromString(const std::string& data)
{
return PageNumber{std::stoi(data)};
}
};
void showPage(PageNumber pageNumber, const Request&, Response& response)
{
response.send("page" + std::to_string(pageNumber.value));
}
router.route(whaleroute::rx{"/page/(\\d+)"}, Request::Method::GET).process(showPage);
When the regular expression of a route is set dynamically, you may need to capture an arbitrary number of parameters. In
such cases, you can use the whaleroute::RouteParameters<>
structure, which stores the values of capturing groups in a
vector of strings.
void showBook(const RouteParameters<>& bookIds, const Request&, Response& response)
{
if (bookIds.value.size() == 1)
response.send("book" + std::to_string(bookIds.value.at(0)));
if (bookIds.value.size() == 2)
response.send("book" + std::to_string(bookIds.value.at(0)) + std::to_string(bookIds.value.at(1)));
}
router.route(whaleroute::rx{"/book/(\\d+)/(\\d+)"}, Request::Method::GET).process(showBook);
router.route(whaleroute::rx{"/book/(\\d+)"}, Request::Method::GET).process(showBook);
If capturing the string array is more suitable for your request processor, you can use RouteParameters
with a specific
number of capturing groups that must be present in the regular expression:
void showPage(const RouteParameters<1>& pageNumber, const Request&, Response& response)
{
response.send("page" + pageNumber.value().at(0));
}
router.route(whaleroute::rx{"/page/(\\d+)"}, Request::Method::GET).process(showPage);
By default, whaleroute treats trailing slashes in requests and route paths as optional. For example, /path
and /path/
are considered equal.
This behavior can be changed by using the setTrailingSlashMode
method of whaleroute::RequestRouter
and providing the
value whaleroute::TrailingSlashMode::Strict
.
Using the route
method without arguments registers a processor for requests that do not match any existing routes.
This is an alternative to using the processUnmatchedRequest
virtual method, which won't be called if you use route()
instead.
router.route("/", Request::Method::GET).set("HTTP/1.1 200 OK\r\n\r\n");
router.route().set("HTTP/1.1 418 I'm a teapot\r\n\r\n");
If you check the implementation of the process
method in whaleroute::RequestRouter
, you'll see that it simply
creates a whale::RequestProcessorQueue
object and forwards the request processing by calling its launch
method:
auto queue = makeRequestProcessorQueue(request, response);
queue.launch();
whaleroute::RequestProcessorQueue
is a sequence of all matched route processors that can be launched and stopped by
calling its launch
and stop
methods. It's available in the public interface, allowing you to create and
use RequestProcessorQueue
directly without using the RequestRouter::process
method. This can be especially useful in
an asynchronous environment, where route processing can be delayed by stopping the queue and resumed in the request
handler's callback using a captured copy of the queue.
Otherwise, you can disregard this information and simply use the RequestRouter::process
method.
Download and link the library from your project's CMakeLists.txt:
include(FetchContent)
FetchContent_Declare(whaleroute
GIT_REPOSITORY "https://github.com/kamchatka-volcano/whaleroute.git"
GIT_TAG "origin/master"
)
#uncomment if you need to install whaleroot with your target
#set(INSTALL_WHALEROOT ON)
FetchContent_MakeAvailable(whaleroute)
add_executable(${PROJECT_NAME})
target_link_libraries(${PROJECT_NAME} PRIVATE whaleroute::whaleroute)
To install the library system-wide, use the following commands:
git clone https://github.com/kamchatka-volcano/whaleroute.git
cd whaleroute
cmake -S . -B build
cmake --build build
cmake --install build
After installation, you can use the find_package() command to make the installed library available inside your project:
find_package(whaleroute 1.0.0 REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE whaleroute::whaleroute)
cd whaleroute
cmake -S . -B build -DENABLE_TESTS=ON
cmake --build build
cd build/tests && ctest
whaleroute is licensed under the MS-PL license