RESTFUL API Router using C++, Functional Programming, and CRTP
Introduction
Originally, this blog post was going to be specifically about Curiously Recurring Template Patterns (CRTP) in C++ and give a concrete example of when to use it. I decided that implementing an HTTP request router would be a good place to start but the amount of design decisions grew quite large.
So, this blog post now contains a ground up implementation of an HTTP request router that walks you through all of the design decisions you may make when implementing it yourself. We will use functional programming ala Haskell to parse the plaintext HTTP request. Then we will dive into Object Oriented Programming and Inheritence to try to make handling multiple endpoints more maintainable. Finally, we will use CRTP to avoid runtime polymorphism and allow us to run boilerplate code before and after our actual endpoint implementation.
You will need to be fairly comfortable with C++ but the final implementation includes a version you can copy/paste.
Parse an HTTP request using functional C++
We will simulate filling the buffer through a socket by reading a file from the disk.
Read the HTTP request
Create the HTTP request file and name it “request.http”.
GET / HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
Host: www.mydomain.com
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
Read the file
We will create a function that uses std::ifstream
and std::istreambuf_iterator
to read the file into an std::vector<char>
. In Haskell this function is named readFile
. It takes a filename as input and returns an std::vector<char>
.
#include <string>
#include <fstream>
#include <iterator>
// readFile :: std::string -> std::vector<char>
auto readFile( std::string const& filePath ) -> std::vector<char>
{
std::ifstream file( filePath, std::ios_base::in );
return std::vector<char>{ std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>() };
}
Break Buffer into lines
An HTTP request breaks up its data by lines. We first need to separate each line into its own std::vector<char>
. In Haskell this function is named lines
. It takes a list as input and returns a list of lists. In our case it returns a list of lines.
// ... previous code
// lines :: std::vector<char> -> [std::vector<char>]
auto lines( std::vector<char> const& str ) -> std::vector<std::vector<char>>
{
std::vector<std::vector<char>> out{};
auto a = std::begin( str );
auto b = std::end( str );
auto it = a;
// Leap frog iterator a and iterator it
for ( ; it != b ; std::advance( it, 1 ) )
{
if ( *it == '\n' )
{
out.push_back( std::vector<char>{ a, it } );
a = it + 1;
}
}
return out;
}
Break each line up by words
We now want each line in our request to be broken up by white space into multiple std::vector<char>
so our final container will be an std::vector<std::vector<std::vector<char>>>
. In Haskell, breaking up a line into words is called words
. It takes a list as input and returns a list of lists. In our case it returns a list of words.
// ... previous code
// words :: std::vector<char> -> [std::vector<char>]
auto words( std::vector<char> const& xs ) -> std::vector<std::vector<char>>
{
std::vector<std::vector<char>> out{};
auto a = std::begin( xs );
auto b = std::end( xs );
auto it = a;
// Leap frog iterator a and iterator it
for ( ; it != b ; std::advance( it, 1 ) )
{
if ( *it == ' ' )
{
out.push_back( std::vector<char>{ a, it } );
a = it + 1;
}
}
return out;
}
Since our list is currently a list of lines, we will need to loop through our structure. Fortunately, Haskell calls this fmap
.
It takes a function and a list as arguments and returns a list where
the function is applied to each item. We can use this to apply words
to each line.
// ... previous code
// fmap :: ( S -> T ) -> [S] -> [T]
template <typename T, typename S, template <typename> typename Cont, typename F>
auto fmap( F fct, Cont<S> const& xs ) -> Cont<T>
{
Cont<T> out{};
out.reserve( xs.size() );
for ( auto const& x : xs )
out.push_back( fct( x ) );
return out;
}
By making typename T
the first
template parameter, we can specify the type of each item after it has
been passed through the function and allow the compiler to deduce the
remaining types.
To convert our HTTP request from a plaintext file to something we can use, we can pass the filename to readFile
, pass that output to lines
, then apply words
to each line.
// ... previous code
int main()
{
auto request = fmap<std::vector<std::vector<char>>>( words, lines( readFile( "http.request" ) ) );
}
Now that we have the request in a useable format, we can grab the HTTP request verb (e.g. GET, POST, PUT, UPDATE, DELETE) and the uri (e.g. /, /name). Since indexing into std::vector
is built-in, there is no need to write functional helpers to grab the correct words from the request.
// ... previous code
int main()
{
auto request = fmap<std::vector<std::vector<char>>>( words, lines( readFile( "http.request" ) ) );
// verb :: std::vector<char>
auto verb = request.at( 0 ).at( 0 );
// uri :: std::vector<char>
auto uri = request.at( 0 ).at( 1 );
}
Creating a RESTFUL API Handler
For each endpoint in our api, we need to be able to perform some tasks. Our first thought is to write a series of if/else if statements to run some code based on the uri and verb. This will get tedious very quickly as each endpoint could potentially handle many verbs. A better starting point would be to use an std::unordered_map<std::string, std::function<void(void)>>
to hold the endpoint and callback pairs. We will deal with handling multiple verbs per endpoint later.
A First Attempt toward a RESTFUL API
By utilizing lambdas, we can make our code concise and easy to understand.
// ... previous code
#include <unordered_map>
#include <functional>
#include <iostream>
int main()
{
// ... previous code
std::unordered_map<std::string, std::function<void(void)>> routes =
{
{ "/", [](){ std::cout << "Do something\n"; } }
};
routes.at( "/" )();
}
This isn’t so flexible if we want to start adding features like logging, middleware, and error handling. We would need to call these same functions in every lambda. This creates lots of repeated code. Let’s think about a way to call a function for every endpoint where each endpoint could handle multiple HTTP request verbs.
Using Polymorphism to handle RESTFUL API Endpoints
If we expose functions through a base class, we can override those functions to handle each endpoint’s code within a single class. This also allows us to hold all of the endpoint classes in our std::unordered_map
container. First we will define our classes. Then we will create a factory class to create std::unique_ptr
’s to our endpoint classes. Finally, we will hold each endpoint-class pair using a function pointer to the factory function.
#include <memory>
#include <unordered_map>
#include <string>
#include <iostream>
class Base
{
public:
virtual void get()
{
std::cout << "Base::get\n";
}
};
class Child : public Base
{
public:
void get() override
{
Base::get();
std::cout << "Child::get\n";
}
};
template <typename EndPoint>
std::unique_ptr<Base> factory()
{
return std::make_unique<EndPoint>();
}
int main()
{
// Store the endpoint-function pointer pairs
std::unordered_map<std::string, std::unique_ptr<Base>(*)(void)> routes =
{
{ "/", &factory<Child> }
};
routes.at( "/" )()->get();
}
This allows us to complete boilerplate tasks before each individual endpoint task but only before our end point task. We are also required to explicitly call the base class endpoint function. One other downfall is the overhead incurred from using runtime polymorphism (i.e. virtual table lookup).
A Second Attempt toward a RESTFUL API
We can remove the use of the vtable completely by using compile time polymorphism. This means we need a way to determine what child class member function to call at compile time. Using CRTP, we can accomplish this task. We can pass the child class name to the base class as a template parameter and cast the base class to the passed in type.
#include <iostream>
template <typename Child_Class>
class Base
{
public:
void get()
{
std::cout << "Setup\n";
static_cast<Child_Class*>(this)->get_impl();
std::cout << "Cleanup\n";
}
};
class Child : public Base<Child>
{
public:
void get_impl()
{
std::cout << "Child::get\n";
}
};
int main()
{
Child ch{};
ch.get();
}
Awesome! Now we can do setup and cleanup in the base class for every subsequent endpoint class we create but we’ve lost the ability to hold these classes in a container using a factory. This is because every child class we create has a different base class. We could create an interface for the base class but this will return the overhead we incur from runtime polymorphism. We need something that never changes so we can hold it in our std::unordered_map
.
Using Functions Again
If we create functions in our base class that all have the same signature, we can create handles to them using std::function
. This allows us to hold the endpoint and function pairs like we did in our first attempt using lambdas.
#include <functional>
#include <unordered_map>
#include <iostream>
#include <string>
template <typename Child_Class>
class Base
{
public:
void get()
{
std::cout << "Setup\n";
static_cast<Child_Class>(*this).get_impl();
std::cout << "Cleanup\n";
}
};
class Child : public Base<Child>
{
public:
void get_impl()
{
std::cout << "Child::get\n";
}
};
int main()
{
std::unordered_map<std::string, std::function<void(void)>> routes =
{
{ "/", Child::get } // compilation error, invalid use of non-static member function
};
routes.at( "/" )();
}
Uh oh! We can’t call this function without having instantiated an object. It seems like we’re back to the problem we had when we couldn’t hold different classes in our std::unordered_map
.
A Breakthrough Using Static Functions
If we use static functions, we can get around the need to instantiate the correct object (by the way, inventing a scheme to do this with the previous code is non-trivial). The only caveat to this method is that we need to first instantiate an object with the type Child_Class
in our base class.
#include <functional>
#include <unordered_map>
#include <iostream>
#include <string>
template <typename Child_Class>
class Base
{
public:
static void get()
{
std::cout << "Setup\n";
Child_Class{}.get_impl();
std::cout << "Cleanup\n";
}
};
class Child : public Base<Child>
{
public:
void get_impl()
{
std::cout << "Child::get\n";
}
};
int main()
{
std::unordered_map<std::string, std::function<void(void)>> routes =
{
{ "/", Child::get }
};
routes.at( "/" )();
}
Using this method allows us to get around using the vtable at the expense of a single stack frame from the static method. This is a necessary but still worthwhile trade off.
Default HTTP Request Verb Implementations
Before we deal with handling endpoints with multiple HTTP Request verbs, we should think about how we will handle default implementations for every verb so that our child classes only need to implement the bare minimum. We will do this in our base class but let’s also handle how we will expose those methods to the client.
#include <functional>
#include <unordered_map>
#include <iostream>
#include <string>
template <typename Child_Class>
class Base
{
public:
static void get()
{
std::cout << "Setup\n";
Child_Class{}.get_impl();
std::cout << "Cleanup\n";
}
static void pst()
{
std::cout << "Setup\n";
Child_Class{}.pst_impl();
std::cout << "Cleanup\n";
}
private:
void get_impl()
{
std::cout << "Endpoint does not exist for GET\n";
}
void pst_impl()
{
std::cout << "Endpoint does not exist for POST\n";
}
};
class Child : public Base<Child>
{
friend Base<Child>;
protected:
void get_impl()
{
std::cout << "Child::get\n";
}
};
int main()
{
std::unordered_map<std::string, std::function<void(void)>> routes =
{
{ "/get", Child::get },
{ "/pst", Child::pst }
};
routes.at( "/get" )();
routes.at( "/pst" )();
}
We have protected the child class member functions from being called by client code and only exposed the static methods in the base class. In order for the base class to then call the implementation member functions in the child class, we must make the base class a friend of the child class. We could easily define a macro to take care of the boilerplate.
Passing the Request Data
But how do we handle multiple verbs for each endpoint? This seems like we’ll need yet another std::unordered_map
to switch on but how will we tell which child class to use? Since our API is entirely implemented in the base class, this seems like a good place to handle each verb. So when we add a verb, we will need to handle it then implement it’s public interface with a static member function and its default implementation in the base class. This means the base class will have to have some knowledge about the HTTP request. So why don’t we just pass it in the constructor so every child class can utilize it?
#include <functional>
#include <unordered_map>
#include <iostream>
#include <string>
struct Request
{
std::vector<char> verb{};
std::vector<char> uri{};
};
template <typename Child_Class>
class Base
{
public:
// Remove the default constructor for Base and its children
// This is because Base requires knowledge of the Request
Base() = delete;
// Must always instatiate Base (and Child) with a Request
Base( Request req ) : request{ req } {}
static void get( Request const& req )
{
std::cout << "Setup\n";
Child_Class{ req }.get_impl();
std::cout << "Cleanup\n";
}
static void pst( Request const& req )
{
std::cout << "Setup\n";
Child_Class{ req }.pst_impl();
std::cout << "Cleanup\n";
}
private:
Request request{};
void get_impl()
{
std::cout << "Endpoint does not exist for GET\n";
}
void pst_impl()
{
std::cout << "Endpoint does not exist for POST\n";
}
};
class Child : public Base<Child>
{
friend Base<Child>;
protected:
void get_impl()
{
std::cout << "Child::get\n";
}
};
int main()
{
std::unordered_map<std::string, std::function<void(Request const&)>> routes =
{
{ "/get", Child::get },
{ "/pst", Child::pst }
};
routes.at( "/get" )( Request{ {'G','E','T'}, {'/'} } );
routes.at( "/pst" )( Request{ {'P','O','S','T'}, {'/'} } );
}
If you’re lucky enough to be using C++17, this code will compile and run. If you aren’t, then you’ll need to inherit the constructors from the base class with a using Base<Child>::Base;
in the public section of your child class.
I’ve been speaking about handling the HTTP request verbs but as of yet have not considered an implementation. For endpoints that do utilize any number of the possible verbs, we need to point our original routes
endpoint to a function that will determine which static member function (i.e. get, pst) to call.
Request Verb Switching Implementation using std::unordered_map
Inside this switchboard looking function we would use if
statements to match on the std::vector<char>
or we could create a helper function to convert the verbs from std::vector<char>
to enums then use a switch
statement, but these don’t seem to scale well. Let’s take the easy road and use another std::unordered_map
to handle these verbs but we will make it static as we only need one instantiation of it for each child class.
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <functional>
#include <fstream>
class Request;
enum class ErrorStatus
{
Success,
NotFound,
UnauthAccess,
InvalidVerb
};
using Char = char;
template <typename T>
using Vec = std::vector<T>;
using StdString = std::string;
using String = Vec<Char>;
using EndPoint = std::function< ErrorStatus ( Request const& ) >;
using RouteMap = std::unordered_map< StdString, EndPoint >;
/********************************************************************
* HELPER
*******************************************************************/
// operator "" _s :: Char const* -> std::size_t -> String
auto operator "" _s( Char const* x, std::size_t s ) -> String
{
return String{ x, x + s };
}
// toStdStr :: String -> StdString
auto toStdStr( String const& xs ) -> StdString
{
return StdString{ begin( xs ), end( xs ) };
}
// toStr :: StdString -> String
auto toStr( StdString const& x ) -> String
{
return String{ begin( x ), end( x ) };
}
// readFile :: StdString -> StdString
auto readFile( StdString const& filePath ) -> StdString
{
std::ifstream file( filePath, std::ios_base::in );
return StdString{ std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>() };
}
// fmap :: ( S -> T ) -> [S] -> [T]
template <typename T, typename S, template <typename> typename Cont, typename F>
auto fmap( F fct, Cont<S> const& xs ) -> Cont<T>
{
Cont<T> out{};
out.reserve( xs.size() );
for ( auto const& x : xs )
out.push_back( fct( x ) );
return out;
}
// lines :: String -> [String]
auto lines( String const& str ) -> Vec<String>
{
Vec<String> out{};
auto a = begin( str );
auto b = end( str );
auto it = a;
for ( ; it != b ; advance( it, 1 ) )
{
if ( *it == '\n' )
{
out.push_back( String{ a, it } );
a = it + 1;
}
}
auto last = String{ a, b };
if ( ! last.empty() )
out.push_back( String{ a, b } );
return out;
}
// words :: String -> [String]
auto words( String const& xs ) -> Vec<String>
{
Vec<String> out{};
auto a = begin( xs );
auto b = end( xs );
auto it = a;
for ( ; it != b ; advance( it, 1 ) )
{
if ( *it == ' ' )
{
out.push_back( String{ a, it } );
a = it + 1;
}
}
auto last = String{ a, b };
if ( ! last.empty() )
out.push_back( String{ a, b } );
return out;
}
struct Request
{
String verb{};
String uri{};
String data{};
};
// The base class
template <typename Child>
class Route_Base
{
public:
/******************** Constructors ********************/
Route_Base() = delete;
Route_Base( Request const& req ) : request{ req } {}
/******************** Public API ********************/
static auto all( Request const& req ) -> ErrorStatus
{
if ( req.verb == "GET"_s )
return Child::get( req );
else if ( req.verb == "POST"_s )
return Child::pst( req );
else if ( req.verb == "PUT"_s )
return Child::put( req );
else if ( req.verb == "UPDATE"_s )
return Child::upd( req );
else if ( req.verb == "DELETE"_s )
return Child::del( req );
return ErrorStatus::InvalidVerb;
}
static auto get( Request const& req ) -> ErrorStatus
{
std::cout << "Route_Base::get: Setup\n";
auto ret = Child{ req }.get_impl();
std::cout << "Route_Base::get: Cleanup\n\n";
return ret;
}
static auto put( Request const& req ) -> ErrorStatus
{
std::cout << "Route_Base::put: Setup\n";
auto ret = Child{ req }.put_impl();
std::cout << "Route_Base::put: Cleanup\n\n";
return ret;
}
static auto pst( Request const& req ) -> ErrorStatus
{
std::cout << "Route_Base::pst: Setup\n";
auto ret = Child{ req }.pst_impl();
std::cout << "Route_Base::pst: Cleanup\n\n";
return ret;
}
static auto upd( Request const& req ) -> ErrorStatus
{
std::cout << "Route_Base::upd: Setup\n";
auto ret = Child{ req }.upd_impl();
std::cout << "Route_Base::upd: Cleanup\n\n";
return ret;
}
static auto del( Request const& req ) -> ErrorStatus
{
std::cout << "Route_Base::del: Setup\n";
auto ret = Child{ req }.del_impl();
std::cout << "Route_Base::del: Cleanup\n\n";
return ret;
}
protected:
Request request{};
private:
/******************** Default API ********************/
auto error_msg( char const* val ) -> ErrorStatus
{
std::cout << " Endpoint doesn't exist: " << val << "\n";
return ErrorStatus::NotFound;
}
auto get_impl() -> ErrorStatus
{
error_msg( "get" );
return ErrorStatus::NotFound;
}
auto put_impl() -> ErrorStatus
{
error_msg( "put" );
return ErrorStatus::NotFound;
}
auto pst_impl() -> ErrorStatus
{
error_msg( "pst" );
return ErrorStatus::NotFound;
}
auto upd_impl() -> ErrorStatus
{
error_msg( "upd" );
return ErrorStatus::NotFound;
}
auto del_impl() -> ErrorStatus
{
error_msg( "del" );
return ErrorStatus::NotFound;
}
};
// The child class
class Route_Index : public Route_Base<Route_Index>
{
friend Route_Base<Route_Index>;
protected:
auto get_impl() -> ErrorStatus
{
std::cout << " Route_Index::get_impl()\n";
std::cout << " verb: " << toStdStr( request.verb ) << "\n";
std::cout << " uri: " << toStdStr( request.uri ) << "\n";
std::cout << " data: " << toStdStr( request.data ) << "\n";
return ErrorStatus::Success;
}
};
// Helper functions for the route map
class Route
{
public:
static RouteMap route_map;
static auto add( StdString const& uri, EndPoint endpt ) -> void
{
route_map.emplace( uri, endpt );
}
static auto dispatch( StdString const& uri ) -> EndPoint
{
return route_map.at( uri );
}
};
RouteMap Route::route_map{};
int main()
{
Route::add( "/", Route_Index::all );
StdString a = "http.request";
String file_str = toStr( readFile( a ) );
auto file_lines = lines( file_str );
auto request = fmap<Vec<String>>( words, file_lines );
// auto request = fmap<Vec<Vec<Char>>( words, lines( toStr( readFile( a ) ) ) );
auto verb = request.at( 0 ).at( 0 );
auto uri = request.at( 0 ).at( 1 );
Route::dispatch( toStdStr( uri ) )( Request{ verb, uri, ""_s } );
Route::dispatch( toStdStr( uri ) )( Request{ "PUT"_s, "/"_s, ""_s } );
Route::dispatch( toStdStr( uri ) )( Request{ "POST"_s, "/"_s, ""_s } );
Route::dispatch( toStdStr( uri ) )( Request{ "UPDATE"_s, "/"_s, ""_s } );
Route::dispatch( toStdStr( uri ) )( Request{ "DELETE"_s, "/"_s, ""_s } );
}
So what does this all cost?
For each child class we have:
- An
std::unordered_map
for the wildcard endpoints - A static function for each endpoint. That’s 5 (i.e. get, post, put, update, and delete )
- A member function for the default implementation of each endpoint verb.
- A member function for each implemented endpoint verb
But look at what we’ve gained. Look at how compact the child class is. In the child class, we only need to implement the verbs we want to use, then add it to our routes map. The bulk of the work is in the base class, but we only need to write that once. If there is any work to be done to our API, we can do this all in the base class so that each child class can continue implementing only the necessary request verbs.
Request Verb Switching Implementation using Enum and std::array
If the std::unordered_map
is too memory heavy for your application, you can convert this into an std::array
that holds the std::functions
. You could then switch on an enum.
#include <array>
// ...previous code
// operator "" _s :: Char const* -> std::size_t -> String
auto operator "" _s( Char const* x, std::size_t s ) -> String
{
return String{ x, x + s };
}
enum RequestVerb
{
GET,
POST,
PUT,
UPDATE,
DELETE,
UNKNOWN
};
auto toReqVerb( String verb ) -> RequestVerb
{
if ( verb == "GET"_s )
return RequestVerb::GET;
else if ( verb == "POST"_s )
return RequestVerb::POST;
else if ( verb == "PUT"_s )
return RequestVerb::PUT;
else if ( verb == "UPDATE"_s )
return RequestVerb::UPDATE;
else if ( verb == "DELETE"_s )
return RequestVerb::DELETE;
return RequestVerb::UNKNOWN;
}
template <typename Child_Class>
class Base
{
// ...previous code
public:
static void all( Request const& req )
{
static std::array<std::function<void(Request const&)>, 5> verbArr =
{
Route_Base<Child>::get,
Route_Base<Child>::put,
Route_Base<Child>::pst,
Route_Base<Child>::upd,
Route_Base<Child>::del
};
auto verb = toReqVerb( req.verb );
if ( verb >= RequestVerb::UNKNOWN )
return;
verbArr.at( verb )( req );
}
};
This trades CPU cycles when converting an std::vector<char>
to enum inside toReqVerb()
to gain a little memory by using an std::array
instead of an std::unordered_map
. Unfortunately, we still need to utilize memory to hold these static member functions. If we were trying to restrict our memory use further, we could just switch on req.verb
using if statements but we would then incur lots of memory use due to code duplication. What’s a good middle ground? What if we convert the Base::all
member function into a helper function?
// ...previous code
template <typename Child>
void all( Request const& req )
{
if ( req.verb == "GET"_s )
Child::get( req );
else if ( req.verb == "POST"_s )
Child::pst( req );
}
// ...previous code
int main()
{
std::unordered_map<std::string, std::function<void(Request const&)> routes =
{
{ "/", all<Child> }
};
routes.at( "/" )( Request{ "GET"_s, "/"_, ""_s } );
routes.at( "/" )( Request{ "POST"_s, "/"_, ""_s } );
}
Excellent! We’ve removed the bloated std::unordered_map
and the clumsy enum + std::array
code. We will now need to keep this in sync with our API inside the base class but if we keep this inside the same file as our base class, this should be easy.
But, we just ended up right were we started! The all()
function gets as many instances as there are child classes so we might as well place it back in the base class!
Request Verb Switching Implementation using Plain Old If Statements
So, if the number of request verbs starts to grow too large due to a need to invent new ones, then moving back to the std::unordered_map
implementation might be more maintainable – though the code will hog quite a bit of memory. You could remove the static keyword from the std::unordered_map
and the memory will be freed when the child class is destroyed but creating the std::unordered_map
each time is very time consuming. Sigh. For now, since we only have a handful of request verbs, let’s stick with the last implementation but move it into the base class to keep the code tidy.
A Final Implementation
Let’s put it all together. This code is a more robust version that implements everything that has been discussed. It adds return values for each endpoint member function and easier to understand using
declarations. Most of the code is now boilerplate where the only maintenance will be on the base class called Route_Base
. This maintenance will only be necessary if the number of request verbs increases.
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <functional>
#include <fstream>
class Request;
enum class ErrorStatus
{
Success,
NotFound,
UnauthAccess,
InvalidVerb
};
using Char = char;
template <typename T>
using Vec = std::vector<T>;
using StdString = std::string;
using String = Vec<Char>;
using EndPoint = std::function< ErrorStatus ( Request const& ) >;
using RouteMap = std::unordered_map< StdString, EndPoint >;
/********************************************************************
* HELPER
*******************************************************************/
// operator "" _s :: Char const* -> std::size_t -> String
auto operator "" _s( Char const* x, std::size_t s ) -> String
{
return String{ x, x + s };
}
// toStdStr :: String -> StdString
auto toStdStr( String const& xs ) -> StdString
{
return StdString{ begin( xs ), end( xs ) };
}
// toStr :: StdString -> String
auto toStr( StdString const& x ) -> String
{
return String{ begin( x ), end( x ) };
}
// fmap :: ( S -> T ) -> [S] -> [T]
template <typename T, typename S, template <typename> typename Cont, typename F>
auto fmap( F fct, Cont<S> const& xs ) -> Cont<T>
{
Cont<T> out{};
out.reserve( xs.size() );
for ( auto const& x : xs )
out.push_back( fct( x ) );
return out;
}
// lines :: String -> [String]
auto lines( String const& str ) -> Vec<String>
{
Vec<String> out{};
auto a = begin( str );
auto b = end( str );
auto it = a;
for ( ; it != b ; advance( it, 1 ) )
{
if ( *it == '\n' )
{
out.push_back( String{ a, it } );
a = it + 1;
}
}
auto last = String{ a, b };
if ( ! last.empty() )
out.push_back( String{ a, b } );
return out;
}
// words :: String -> [String]
auto words( String const& xs ) -> Vec<String>
{
Vec<String> out{};
auto a = begin( xs );
auto b = end( xs );
auto it = a;
for ( ; it != b ; advance( it, 1 ) )
{
if ( *it == ' ' )
{
out.push_back( String{ a, it } );
a = it + 1;
}
}
auto last = String{ a, b };
if ( ! last.empty() )
out.push_back( String{ a, b } );
return out;
}
// readFile :: std::string -> std::vector<char>
auto readFile( std::string const& filePath ) -> std::vector<char>
{
std::ifstream file( filePath, std::ios_base::in );
return std::vector<char>{ std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>() };
}
struct Request
{
String verb{};
String uri{};
String data{};
};
// The base class
template <typename Child>
class Route_Base
{
public:
/******************** Constructors ********************/
Route_Base() = delete;
Route_Base( Request const& req ) : request{ req } {}
/******************** Public API ********************/
static auto all( Request const& req ) -> ErrorStatus
{
if ( req.verb == "GET"_s )
return Child::get( req );
else if ( req.verb == "POST"_s )
return Child::pst( req );
else if ( req.verb == "PUT"_s )
return Child::put( req );
else if ( req.verb == "UPDATE"_s )
return Child::upd( req );
else if ( req.verb == "DELETE"_s )
return Child::del( req );
return ErrorStatus::InvalidVerb;
}
static auto get( Request const& req ) -> ErrorStatus
{
std::cout << "Route_Base::get: Setup\n";
auto ret = Child{ req }.get_impl();
std::cout << "Route_Base::get: Cleanup\n\n";
return ret;
}
static auto put( Request const& req ) -> ErrorStatus
{
std::cout << "Route_Base::put: Setup\n";
auto ret = Child{ req }.put_impl();
std::cout << "Route_Base::put: Cleanup\n\n";
return ret;
}
static auto pst( Request const& req ) -> ErrorStatus
{
std::cout << "Route_Base::pst: Setup\n";
auto ret = Child{ req }.pst_impl();
std::cout << "Route_Base::pst: Cleanup\n\n";
return ret;
}
static auto upd( Request const& req ) -> ErrorStatus
{
std::cout << "Route_Base::upd: Setup\n";
auto ret = Child{ req }.upd_impl();
std::cout << "Route_Base::upd: Cleanup\n\n";
return ret;
}
static auto del( Request const& req ) -> ErrorStatus
{
std::cout << "Route_Base::del: Setup\n";
auto ret = Child{ req }.del_impl();
std::cout << "Route_Base::del: Cleanup\n\n";
return ret;
}
protected:
Request request{};
private:
/******************** Default API ********************/
auto error_msg( char const* val ) -> ErrorStatus
{
std::cout << " Endpoint doesn't exist: " << val << "\n";
return ErrorStatus::NotFound;
}
auto get_impl() -> ErrorStatus
{
error_msg( "get" );
return ErrorStatus::NotFound;
}
auto put_impl() -> ErrorStatus
{
error_msg( "put" );
return ErrorStatus::NotFound;
}
auto pst_impl() -> ErrorStatus
{
error_msg( "pst" );
return ErrorStatus::NotFound;
}
auto upd_impl() -> ErrorStatus
{
error_msg( "upd" );
return ErrorStatus::NotFound;
}
auto del_impl() -> ErrorStatus
{
error_msg( "del" );
return ErrorStatus::NotFound;
}
};
// The child class
class Route_Index : public Route_Base<Route_Index>
{
friend Route_Base<Route_Index>;
protected:
auto get_impl() -> ErrorStatus
{
std::cout << " Route_Index::get_impl()\n";
std::cout << " verb: " << toStdStr( request.verb ) << "\n";
std::cout << " uri: " << toStdStr( request.uri ) << "\n";
std::cout << " data: " << toStdStr( request.data ) << "\n";
return ErrorStatus::Success;
}
};
// Helper functions for the route map
class Route
{
public:
static RouteMap route_map;
static auto add( StdString const& uri, EndPoint endpt ) -> void
{
route_map.emplace( uri, endpt );
}
static auto dispatch( StdString const& uri ) -> EndPoint
{
return route_map.at( uri );
}
};
RouteMap Route::route_map{};
int main()
{
Route::add( "/", Route_Index::all );
auto request = fmap<std::vector<std::vector<char>>>( words, lines( readFile( "http.request" ) ) );
auto verb = request.at( 0 ).at( 0 );
auto uri = request.at( 0 ).at( 1 );
Route::dispatch( toStdStr(uri) )( Request{ verb, uri, ""_s } );
Route::dispatch( toStdStr(uri) )( Request{ "PUT"_s, "/"_s, ""_s } );
Route::dispatch( toStdStr(uri) )( Request{ "POST"_s, "/"_s, ""_s } );
Route::dispatch( toStdStr(uri) )( Request{ "UPDATE"_s, "/"_s, ""_s } );
Route::dispatch( toStdStr(uri) )( Request{ "DELETE"_s, "/"_s, ""_s } );
}
Things to think about
Versioning
Most APIs having versioning as it grows. Our endpoints could grow without any need for versioning as we will only add on child classes. But the request verbs could grow and this would require versioning. We could create a new base class that inherits from the original base class and switch on a version number given in the request uri. Each new child class will then inherit from this new base class while the new base class only implements the needed extras.
Variables in the URI
Most HTTP routers allow the client to define variables in the uri so the client doesn’t need to pass variables using the question mark syntax. This also gives us a clean API that is easy to bookmark and cache. Implementing this requires us to manually order our endpoints and hold them in an std::vector
instead of an std::unordered_map
. We would then need to write each uri as a regex and test if the given request uri matches. When we’ve found a match we call the correct child member function by index number (the two std::vector
’s must be synced’. We can create a class similar to Route
above to handle this) then split up the request uri and utilize the data given.
Authentication
This can be handled with API tokens but is outside the scope of this blog post. Fortunately, we’ve implemented this in such a way that middleware is easy to implement on a case by case basis so we can leave some endpoints open. We just need to add an std::vector
to the base class to hold request verbs that require authentication (i.e. POST). In the child class we fill it with the correct verbs and implement the authentication in the static member functions in the base class.
Response
Above we’ve only implemented error codes as responses. Ideally our application would return useful information like HTML or JSON. Currently we have all the building blocks, but this is out of scope for this blog post. In general, you would build up an HTTP response (which is just plaintext like the HTTP request), then use the original socket to return the information. Browsers and clients would then interpret this data depending on how you crafted the HTTP response.