rikitiki
v0.1.67
Build C++ web server modules that allow easy routing and deployment.
|
Modules can be anything with an appropriate registration function to register their handlers to the server. This registration function will only be called once per server object, and it's state is persisted for the lifetime of the server – so be aware that different handlers might fire at the same time depending on web traffic.
The registration function has this signature:
In this function, you should set up whatever state is required, and add the appropriate handlers to the server
Handlers are a common theme for these kinds of applications. The meat of the handler functionality is the
function, and predictably, it is this function that processes each request. The return value indicates whether the handler actually handled the request (true) or not (false).
Handlers are tried in the order they are added in. Once one handler handles the request, the rest are not executed.
Routes are implementations of handlers that check the incoming request against a route string, and decide whether or not they should handle the request. They then delegate out the actual processing of the request to a user specified function.
One of the convenient features of routes in rikitiki is that they provide functionality for basic parsing of the url, much like scanf, and pass the parsed values to the user provided function.
As a matter of convenience, we want to let C++ figure out as many types as it can. With this in mind, there is a set of helper functions that create routes for us. These calls tend to look like this:
Note that you can pass an instance of anything in, it doesn't have to be a module or have any signature other than the one you specified for the route to call.
This function take an optional fourth argument, with which you can specify the http request method type. The default is to accept any request method, but it can often times be useful to specify POST or GET – for instance, you could set up two handlers with the same route, but where one function is meant specifically to handle form POST requests.
An overload exists for which you don't have to specify the function. If you use this overload, it defaults to using 'operator()' as the function to route to.
Do note that the object you pass as the first argument will not be copied, and that reference is expected to be around as long as the server is active.
The second argument that CreateRoute::With takes is the actual mapped route that will call the route function. Remember to have the preceding '/' for most routes. Tokens like {.*} will be matched to whichever type they line up to in the templated argument list. For instance:
when matched against
"/books/23/romeoandjuliet/4.99"
will effectively call the function as '(*this)(ctx, 23, "romeoandjuliet", 4.99)'.
The first argument is the connection context, which is required and passed in automatically. The rest are extracted from the requested URL.
NOTE: The names within the brackets are optional, and for documentation purposes for the programmer only. They do not need to match up with the name of the arguments in the function themselves, nor should you expect them to be treated as a mapping. It will attempt to apply the arguments in the order you defined them and the order they were encountered in in the route. While it would be great for this automatic mapping to happen, C++ simply does not allow for that level of reflection.
That being said, it is a good practice to keep the names matched up for readability.
You can specify as many arguments into CreateRoute as you want, and they can be any type that you want. However, it isn't always completely obvious what function declaration they require.
The first argument is always a reference to a ConnContext object. This is the object you need to write to return information to the client, and so it makes sense for it to be a default. The arguments that follow are the ones specified by CreateRoute, with one caveat: non fundamental types AddPreprocessors to are passed in as const references for performance reasons.
So from the example above, where we had
it is going to want a function defined as such:
void operator()(rikitiki::ConnContext&, int, const std::string&, float)
Under the hood, the routing engine uses the '>>' stream operator to read in the tokens. So out of the box, anything datatype that can be extracted with the '>>' operator. Do note that a specialized case exists for string, in which case it only pulls out a single word and not the rest of the line, as '>>' does by default.
If you require a custom structure to be mapped, create an appropriate '>>' overload for it and a default constructor. If overloading '>>' is not a viable solution for you, there is also a more specific specialization you can make on 'rikitiki::extract' that is only used for routing. Consult the source and feel free to post to the message board if you have any trouble with this.
The 'ConnContext' object provides all the information about the request, as well as being the object to manipulate.
The main way to populate the response is to use the stream operator '<<' into the ConnContext object. It supports all the data types that streams accept, as well as 'ContentType' and ctemplate dictionaries.
The '>>' operator provides access to the payload content of the request. Even with no extensions:
will load the request contents into the payload variable.
As well as normal outputs, you can also use the stream operator to pass in:
On top of the basic payload extraction mechanism is a more helpful system in place which allows you to automatically marshall from a raw context, into a custom class definition. You could overload the stream operators to ConnContext yourself, however then you have to either add support for reading the 'accept' header and adhering to that, as well as sending the appropriate content type header back.
The recommended approach is to seperate out the content that you are sending (ie, your custom class data) with the content type handler. To define a new content type handler, you must provide an input and output overload, as well as a type trait defining what content types it covers. JsonCpp is already set up with sensible defaults:
From jsoncpp\connContext_ext.h:
Note that providing an overload for << to Response enables a useful overload to operator<< on ConnContext. This is the operator you should normally overload. This is enough to write and read the content type handler from the connection context.
The other piece you need in place is a conversion from your custom class to all the content type handlers you want to support. For instance, from examples\restAdv.h:
This is a fairly trivial implementation on how to convert back and forth from the Book class to a json object. We also need to provide a type trait to Book that specifies what handlers it supports:
And from that you can do things like:
Which reads in a book and immediately echos it back out.
This gives direction on both how to extract the book type as well as write it to the underlying stream.
This has a number of benefits over just manually writing to the stream. First, since you can specify multiple content formats, rikitiki will check the request headers and send back the appropriate response based. If the request headers don't specify a request that your object supports, the proper error code is returned indicating that the types it asked for where unacceptable.
Secondly, it provides automatic support for container types if the output format supports it. For instance,
will echo out an inputed list of books.
It will also make the translation code more resistant to underlying interface changes in ConnContext. Some pseudo-reflection methods are currently being investigated so you can define a minimal metaclass for a given type, and different output formats will know how to process your type automatically; although these methods will likely only ever apply to simpler classes.