The design of the Silicon middlewares relies on several concepts:
A middleware is represented by one or two classes:
instantiate
method may take one or more middleware instances as argument if the instance stacks on other middlewares. The factory can also implement a initialize method if it relies on other middlewares for its initialization. For example, sqlite_session::initialize
relies on a sqlite_connection
to create the session table.Stateless factories: When a middleware factory does not have to maintain a state or data to provide instances, the instance class can directly implement instantiate as a static method.
Naming Convention: Given a middleware, for example a connection to a mysql database. The instance name should be mysql_connection
and the factory mysql_connection_factory
.
It is often usefull for a procedure to gain access to external data like access to a database. Procedures request a middleware by simply declaring it as a parameter.
The following example gets data from a sqlite database using the sqlite_connection
middleware:
#include <silicon/api.hh>
#include <silicon/mhd_serve.hh>
#include <silicon/sqlite.hh>
int main()
{
using namespace sl;
auto api = http_api(
GET / _get_user / _id[int()] = [] (auto p, sqlite_connection& c)
{
auto res = D(_name = std::string(), _age = int());
if (!(c("SELECT name, age from users where id = ?")(p.id)() >> res))
throw error::not_found("User with id ", p.id, " not found.");
return res;
}
);
auto middlewares = std::make_tuple(sqlite_connection_factory("db.sqlite"));
sl::mhd_json_serve(api, middlewares, 12345);
}
A sqlite_connection_factory
, the object responsible for the sqlite_connection
creation, is added to the api via bind_factories.
Procedures can take any number of middlewares as argument, in any order.
Middlewares can depend on each others, until there is no dependency cycle. For example, a sqlite session storage requires a cookie tracking id to identify a request and an access to a sqlite database.
struct sqlite_session_factory
{
sqlite_session_factory(const std::string& table_name)
: table_name_(table_name)
{
}
// Run once at the initialization of the server:
void initialize(sqlite_connection& c)
{
// Create the table if it does not exists
}
// Run for every procedure call taking session_data as argument.
session_data instantiate(tracking_cookie& cookie, sqlite_connection& con)
{
// Fetch the session belonging to cookie.id and using the connection con.
// Return it.
}
std::string table_name_;
};
Operations like logging the requests, monitoring the server load need to insert code before and after each procedure call. In Silicon, an API can implicitly attach a set of middlewares to each procedure call. The instantiate method of these middlewares will run before each request, and the destructor of the instance after.
To pass a set of global middleware to an API, simply wrap its procedures with the add_global_middlewares::to
routine.
Here is a example showing a very simple example logging the running time of each request:
#include <iostream>
#include <silicon/backends/mhd_serve.hh>
#include <silicon/api.hh>
using namespace sl;
struct request_logger
{
request_logger()
{
time = get_time();
std::cout << "Request start!" << std::endl;
}
~request_logger()
{
std::cout << "Request took " << (get_time() - time) << " microseconds." << std::endl;
}
inline double get_time()
{
timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return double(ts.tv_sec) * 1e6 + double(ts.tv_nsec) / 1e3;
}
static request_logger instantiate() { return request_logger(); }
private:
double time;
};
auto hello_api = http_api(GET / _test = [] () { return "hello world."; });
auto hello_api_ga = add_global_middlewares<request_logger>::to(hello_api);
int main()
{
sl::mhd_json_serve(hello_api_ga, 12345);
}
Note that request_logger
is a stateless factory.