Previously I wrote a very generic HTTP handler.
But in real life the server needs to be able to handle different functions on different paths. i.e. /rest/addUser
would add a user while /rest/getUser/45
would get user 45 etc.
So we need to add lambdas to a generic path that can pick up variables to from the path provided to the server. So this class allows you to register paths that embed sections that will provided to the lambda as variables.
Example: /rest/getUser/{id}
would be a path that contains /rest/getUser/
and has a suffix that is put in the variable id
for the lambda to use. Note the algorithm is generic so you can register multiple variable names in different sections. The variable will match against any character except '/'.
PathMatcher.h
#ifndef THORSANVIL_NISSE_NISSEHTTP_PATH_MATCHER_H
#define THORSANVIL_NISSE_NISSEHTTP_PATH_MATCHER_H
#include <map>
#include <vector>
#include <string>
#include <functional>
#include <regex>
namespace ThorsAnvil::Nisse::NisseHTTP
{
class Request;
class Response;
using Match = std::map<std::string, std::string>;
using Action = std::function<void(Match const&, Request&, Response&)>;
using NameList = std::vector<std::string>;
class PathMatcher
{
struct MatchInfo
{
std::regex test;
NameList names;
Action action;
};
std::vector<MatchInfo> paths;
public:
void addPath(std::string pathMatch, Action&& action);
bool findMatch(std::string const& path, Request& request, Response& response);
};
}
#endif
PathMatcher.cpp
#include "PathMatcher.h"
using namespace ThorsAnvil::Nisse::NisseHTTP;
void PathMatcher::addPath(std::string pathMatch, Action&& action)
{
// Variables to be built.
std::string expr; // Convert pathMatch into a regular expression.
NameList names; // Extract list of names from pathMatch.
// Search variables
std::smatch searchMatch;
std::regex pathNameExpr{"\\{[^}]*\\}"};
while (std::regex_search(pathMatch, searchMatch, pathNameExpr))
{
expr += pathMatch.substr(0, searchMatch.position());
expr += "([^/]*)";
std::string match = searchMatch.str();
names.emplace_back(match.substr(1, match.size() - 2));
pathMatch = searchMatch.suffix().str();
}
expr += pathMatch;
// Add the path information to the list.
paths.emplace_back(std::regex{expr}, std::move(names), std::move(action));
}
bool PathMatcher::findMatch(std::string const& path, Request& request, Response& response)
{
for (auto const& pathMatchInfo: paths)
{
std::smatch match{};
if (std::regex_match(path, match, pathMatchInfo.test))
{
Match result;
for (std::size_t loop = 0; loop < pathMatchInfo.names.size(); ++loop)
{
result.insert({pathMatchInfo.names[loop], match[loop+1].str()});
}
pathMatchInfo.action(result, request, response);
return true;
}
}
return false;
}
Test Case
TEST(PathMatcherTest, NameMatchMultiple)
{
PathMatcher pm;
int count = 0;
Match hit;
pm.addPath("/path1/{name}/{id}", [&count, &hit](Match const& match, Request&, Response&){++count;hit = match;});
std::stringstream ss{"GET /path1/path2/path3 HTTP/1.1\r\nhost: google.com\r\n\r\n"};
Request request("http", ss);
Response response(ss, Version::HTTP1_1);
pm.findMatch("/path1/path2/path3", request, response);
EXPECT_EQ(1, count);
EXPECT_EQ(2, hit.size());
EXPECT_EQ("name", (++hit.begin())->first);
EXPECT_EQ("path2", (++hit.begin())->second);
EXPECT_EQ("id", hit.begin()->first);
EXPECT_EQ("path3", hit.begin()->second);
}