Unverified Commit 447cedbe authored by Dennis Jenkins's avatar Dennis Jenkins Committed by GitHub

Merge pull request #306 from Fylax/master

Improved routing algorithm and ability to add/remove routes at runtime
parents 3685d513 17a71a76
cmake_minimum_required (VERSION 3.0.2)
cmake_minimum_required (VERSION 3.8.2)
project (pistache)
include(CheckCXXCompilerFlag)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Wextra -Wno-missing-field-initializers")
......@@ -8,15 +7,16 @@ option(PISTACHE_BUILD_TESTS "build tests alongside the project" OFF)
option(PISTACHE_BUILD_EXAMPLES "build examples alongside the project" OFF)
option(PISTACHE_INSTALL "add pistache as install target (recommended)" ON)
CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)
CHECK_CXX_COMPILER_FLAG("-std=c++0x" COMPILER_SUPPORTS_CXX0X)
if(COMPILER_SUPPORTS_CXX11)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
elseif(COMPILER_SUPPORTS_CXX0X)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
# Clang exports C++17 in std::experimental namespace (tested on Clang 5 and 6).
# This gives an error on date.h external library.
# Following workaround forces Clang to compile at best with C++14
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(CMAKE_CXX_STANDARD 14)
else()
message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support. Please use a different C++ compiler.")
set(CMAKE_CXX_STANDARD 17)
endif()
set(CMAKE_CXX_STANDARD_REQUIRED OFF)
set(CMAKE_CXX_EXTENSIONS OFF)
include_directories (${CMAKE_CURRENT_SOURCE_DIR}/include)
add_subdirectory (src)
......
......@@ -6,20 +6,27 @@
#pragma once
#include <regex>
#include <string>
#include <tuple>
#include <unordered_map>
#include <memory>
#include <vector>
#include <pistache/http.h>
#include <pistache/http_defs.h>
#include <pistache/flags.h>
#include "pistache/string_view.h"
namespace Pistache {
namespace Rest {
class Description;
namespace details {
template<typename T> struct LexicalCast {
template<typename T>
struct LexicalCast {
static T cast(const std::string& value) {
std::istringstream iss(value);
T out;
......@@ -27,21 +34,21 @@ namespace details {
throw std::runtime_error("Bad lexical cast");
return out;
}
};
};
template<>
struct LexicalCast<std::string> {
template<>
struct LexicalCast<std::string> {
static std::string cast(const std::string& value) {
return value;
}
};
}
};
} // namespace details
class TypedParam {
public:
TypedParam(const std::string& name, const std::string& value)
: name_(name)
, value_(value)
TypedParam(std::string name, std::string value)
: name_(std::move(name))
, value_(std::move(value))
{ }
template<typename T>
......@@ -49,13 +56,13 @@ public:
return details::LexicalCast<T>::cast(value_);
}
std::string name() const {
const std::string& name() const {
return name_;
}
private:
std::string name_;
std::string value_;
const std::string name_;
const std::string value_;
};
class Request;
......@@ -63,95 +70,162 @@ class Request;
struct Route {
enum class Result { Ok, Failure };
typedef std::function<Result (const Request&, Http::ResponseWriter)> Handler;
Route(std::string resource, Http::Method method, Handler handler)
: resource_(std::move(resource))
, handler_(std::move(handler))
, fragments_(Fragment::fromUrl(resource_))
{
UNUSED(method)
}
enum class Status { Match, NotFound };
std::tuple<bool, std::vector<TypedParam>, std::vector<TypedParam>>
match(const Http::Request& req) const;
typedef std::function<Result(const Request, Http::ResponseWriter)> Handler;
std::tuple<bool, std::vector<TypedParam>, std::vector<TypedParam>>
match(const std::string& req) const;
explicit Route(Route::Handler handler) : handler_(std::move(handler)) { }
template<typename... Args>
void invokeHandler(Args&& ...args) const {
handler_(std::forward<Args>(args)...);
}
private:
struct Fragment {
explicit Fragment(std::string value);
bool match(const std::string& raw) const;
bool match(const Fragment& other) const;
bool isParameter() const;
bool isSplat() const;
bool isOptional() const;
std::string value() const {
return value_;
}
Handler handler_;
};
static std::vector<Fragment> fromUrl(const std::string& url);
namespace Private {
class RouterHandler;
}
private:
enum class Flag {
None = 0x0,
Fixed = 0x1,
Parameter = Fixed << 1,
Optional = Parameter << 1,
Splat = Optional << 1
/**
* A request URI is made of various path segments.
* Since all routes handled by a router are naturally
* represented as a tree, this class provides support for it.
* It is possible to perform tree-based routing search instead
* of linear one.
* This class holds all data for a given path segment, meaning
* that it holds the associated route handler (if any) and all
* next child routes (by means of fixed routes, parametric,
* optional parametric and splats).
* Each child is in turn a SegmentTreeNode.
*/
class SegmentTreeNode {
private:
enum class SegmentType {
Fixed, Param, Optional, Splat
};
void init(std::string value);
void checkInvariant() const;
Flags<Flag> flags;
std::string value_;
};
/**
* string_view are very efficient when working with the
* substring function (massively used for routing) but are
* non-owning. To let the content survive after it is firstly
* created, their content is stored in this reference pointer
* that is in charge of managing their lifecycle (create on add,
* reset on remove).
*/
std::shared_ptr<char> resource_ref_;
std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>> fixed_;
std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>> param_;
std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>> optional_;
std::shared_ptr<SegmentTreeNode> splat_;
std::shared_ptr<Route> route_;
/**
* Common web servers (nginx, httpd, IIS) collapse multiple
* forward slashes to a single one. This regex is used to
* obtain the same result.
*/
static std::regex multiple_slash;
static SegmentType getSegmentType(const std::string_view& fragment);
/**
* Fetches the route associated to a given path.
* \param[in] path Requested resource path. Must have no leading slash
* and no multiple slashes:
* eg:
* - auth/login is valid
* - /auth/login is invalid
* - auth//login is invalid
* \param[in,out] params Contains all the parameters parsed so far.
* \param[in,out] splats Contains all the splats parsed so far.
* \returns Tuple containing the route, the list of all parsed parameters
* and the list of all parsed splats.
* \throws std::runtime_error An empty path was given
*/
std::tuple<std::shared_ptr<Route>,
std::vector<TypedParam>, std::vector<TypedParam>>
findRoute(const std::string_view& path,
std::vector<TypedParam> &params,
std::vector<TypedParam> &splats) const;
std::string resource_;
Handler handler_;
/* @Performance: since we know that resource_ will live as long as the vector underneath,
* we would benefit from std::experimental::string_view to store fragments.
*
* We could use string_view instead of allocating strings everytime. However, string_view is
* only available in c++17, so I might have to come with my own lightweight implementation of
* it
public:
SegmentTreeNode();
explicit SegmentTreeNode(const std::shared_ptr<char> &resourceReference);
/**
* Sanitizes a resource URL by removing any duplicate slash, leading
* slash and trailing slash.
* @param path URL to sanitize.
* @return Sanitized URL.
*/
static std::string sanitizeResource(const std::string& path);
/**
* Associates a route handler to a given path.
* \param[in] path Requested resource path. Must have no leading and trailing
* slashes and no multiple slashes:
* eg:
* - auth/login is valid
* - /auth/login is invalid
* - auth/login/ is invalid
* - auth//login is invalid
* \param[in] handler Handler to associate to path.
* \param[in] resource_reference \see SegmentTreeNode::resource_ref_
* \throws std::runtime_error An empty path was given
*/
void addRoute(const std::string_view& path, const Route::Handler &handler,
const std::shared_ptr<char> &resource_reference);
/**
* Removes the route handler associated to a given path.
* \param[in] path Requested resource path. Must have no leading slash
* and no multiple slashes:
* eg:
* - auth/login is valid
* - /auth/login is invalid
* - auth//login is invalid
* \throws std::runtime_error An empty path was given
*/
std::vector<Fragment> fragments_;
bool removeRoute(const std::string_view& path);
/**
* Finds the correct route for the given path.
* \param[in] path Requested resource path. Must have no leading slash
* and no multiple slashes:
* eg:
* - auth/login is valid
* - /auth/login is invalid
* - auth//login is invalid
* \throws std::runtime_error An empty path was given
* \return Found route with its resolved parameters and splats (if no route
* is found, first element of the tuple is a null pointer).
*/
std::tuple<std::shared_ptr<Route>,
std::vector<TypedParam>, std::vector<TypedParam>>
findRoute(const std::string_view& path) const;
};
namespace Private {
class RouterHandler;
}
class Router {
public:
enum class Status { Match, NotFound };
static Router fromDescription(const Rest::Description& desc);
std::shared_ptr<Private::RouterHandler>
handler() const;
static std::shared_ptr<Private::RouterHandler>
handler(std::shared_ptr<Rest::Router> router);
void initFromDescription(const Rest::Description& desc);
void get(std::string resource, Route::Handler handler);
void post(std::string resource, Route::Handler handler);
void put(std::string resource, Route::Handler handler);
void patch(std::string resource, Route::Handler handler);
void del(std::string resource, Route::Handler handler);
void options(std::string resource, Route::Handler handler);
void get(const std::string& resource, Route::Handler handler);
void post(const std::string& resource, Route::Handler handler);
void put(const std::string& resource, Route::Handler handler);
void patch(const std::string& resource, Route::Handler handler);
void del(const std::string& resource, Route::Handler handler);
void options(const std::string& resource, Route::Handler handler);
void removeRoute(Http::Method method, const std::string& resource);
void addCustomHandler(Route::Handler handler);
......@@ -159,11 +233,14 @@ public:
inline bool hasNotFoundHandler() { return notFoundHandler != nullptr; }
void invokeNotFoundHandler(const Http::Request &req, Http::ResponseWriter resp) const;
Status route(const Http::Request& request, Http::ResponseWriter response);
Route::Status route(const Http::Request& request, Http::ResponseWriter response);
private:
void addRoute(Http::Method method, std::string resource, Route::Handler handler);
std::unordered_map<Http::Method, std::vector<Route>> routes;
void addRoute(Http::Method method, const std::string& resource,
Route::Handler handler);
std::unordered_map<Http::Method, SegmentTreeNode> routes;
std::vector<Route::Handler> customHandlers;
......@@ -174,18 +251,31 @@ namespace Private {
class RouterHandler : public Http::Handler {
public:
RouterHandler(const Rest::Router& router);
/**
* Used for immutable router. Useful if all the routes are
* defined at compile time (and for backward compatibility)
* \param[in] router Immutable router.
*/
explicit RouterHandler(const Rest::Router& router);
/**
* Used for mutable router. Useful if it is required to
* add/remove routes at runtime.
* \param[in] router Pointer to a (mutable) router.
*/
explicit RouterHandler(std::shared_ptr<Rest::Router> router);
void onRequest(
const Http::Request& req,
Http::ResponseWriter response);
private:
std::shared_ptr<Tcp::Handler> clone() const {
return std::make_shared<RouterHandler>(*this);
std::shared_ptr<Tcp::Handler> clone() const final {
return std::make_shared<RouterHandler>(router);
}
Rest::Router router;
std::shared_ptr<Rest::Router> router;
};
}
......@@ -212,12 +302,13 @@ private:
namespace Routes {
void Get(Router& router, std::string resource, Route::Handler handler);
void Post(Router& router, std::string resource, Route::Handler handler);
void Put(Router& router, std::string resource, Route::Handler handler);
void Patch(Router& router, std::string resource, Route::Handler handler);
void Delete(Router& router, std::string resource, Route::Handler handler);
void Options(Router& router, std::string resource, Route::Handler handler);
void Get(Router& router, const std::string& resource, Route::Handler handler);
void Post(Router& router, const std::string& resource, Route::Handler handler);
void Put(Router& router, const std::string& resource, Route::Handler handler);
void Patch(Router& router, const std::string& resource, Route::Handler handler);
void Delete(Router& router, const std::string& resource, Route::Handler handler);
void Options(Router& router, const std::string& resource, Route::Handler handler);
void Remove(Router& router, Http::Method method, const std::string& resource);
void NotFound(Router& router, Route::Handler handler);
......
#pragma once
#define CUSTOM_STRING_VIEW 1
#if __cplusplus >= 201703L
# if defined(__has_include)
# if __has_include(<string_view>)
# undef CUSTOM_STRING_VIEW
# include <string_view>
# elif __has_include(<experimental/string_view>)
# undef CUSTOM_STRING_VIEW
# include <experimental/string_view>
namespace std {
typedef experimental::string_view string_view;
}
# endif
# endif
#endif
#ifdef CUSTOM_STRING_VIEW
#include <endian.h>
#include <string>
#include <stdexcept>
#include <cstring>
typedef std::size_t size_type;
namespace std {
class string_view {
private:
const char *data_;
size_type size_;
public:
static constexpr size_type npos = size_type(-1);
constexpr string_view() noexcept: data_(nullptr), size_(0) { }
constexpr string_view(const string_view& other) noexcept = default;
constexpr string_view(const char *s, size_type count) : data_(s), size_(count) { }
string_view(const char *s) : data_(s), size_(strlen(s)) { }
string_view
substr(size_type pos, size_type count = npos) const {
if (pos > size_) {
throw std::out_of_range("Out of range.");
}
size_type rcount = std::min(count, size_ - pos);
return {data_ + pos, rcount};
}
constexpr size_type
size() const noexcept {
return size_;
}
constexpr size_type
length() const noexcept {
return size_;
}
constexpr const char operator[](size_type pos) const {
return data_[pos];
}
constexpr const char *data() const noexcept {
return data_;
}
bool
operator==(const string_view &rhs) const {
return (!std::lexicographical_compare(data_, data_ + size_, rhs.data_, rhs.data_ + rhs.size_) &&
!std::lexicographical_compare(rhs.data_, rhs.data_ + rhs.size_, data_, data_ + size_));
}
string_view &
operator=(const string_view &view) noexcept = default;
size_type find(string_view v, size_type pos = 0) const noexcept {
for(; size_ - pos >= v.size_; pos++) {
string_view compare = substr(pos, v.size_);
if (v == compare) {
return pos;
}
}
return npos;
}
size_type find(char ch, size_type pos = 0) const noexcept {
return find(string_view(&ch, 1), pos);
}
size_type find(const char *s, size_type pos, size_type count) const {
return find(string_view(s, count), pos);
}
size_type find(const char *s, size_type pos = 0) const {
return find(string_view(s), pos);
}
size_type rfind(string_view v, size_type pos = npos) const noexcept {
if (pos >= size_) {
pos = size_ - v.size_;
}
for(; pos >= 0 && pos != npos; pos--) {
string_view compare = substr(pos, v.size_);
if (v == compare) {
return pos;
}
}
return npos;
}
size_type rfind(char c, size_type pos = npos) const noexcept {
return rfind(string_view(&c, 1), pos);
}
size_type rfind(const char* s, size_type pos, size_type count) const {
return rfind(string_view(s, count), pos);
}
size_type rfind(const char* s, size_type pos = npos) const {
return rfind(string_view(s), pos);
}
constexpr bool empty() const noexcept {
return size_ == 0;
}
};
template<>
struct hash<string_view> {
//-----------------------------------------------------------------------------
// MurmurHash3 was written by Austin Appleby, and is placed in the public
// domain. The author hereby disclaims copyright to this source code.
private:
#ifdef __GNUC__
#define FORCE_INLINE __attribute__((always_inline)) inline
#else
#define FORCE_INLINE inline
#endif
static FORCE_INLINE std::uint32_t Rotl32(const std::uint32_t x,
const std::int8_t r) {
return (x << r) | (x >> (32 - r));
}
static FORCE_INLINE std::uint32_t Getblock(const std::uint32_t *p,
const int i) {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
return p[i];
#else
std::uint32_t temp_number = p[i];
uint8_t (&number)[4] = *reinterpret_cast<uint8_t(*)[4]>(&temp_number);
std::swap(number[0], number[3]);
std::swap(number[1], number[2]);
return temp_number;
#endif
}
static FORCE_INLINE uint32_t fmix32(uint32_t h) {
h ^= h >> 16;
h *= 0x85ebca6b;
h ^= h >> 13;
h *= 0xc2b2ae35;
h ^= h >> 16;
return h;
}
public:
size_type
operator()(const string_view &str) const {
const size_t len = str.length();
const uint8_t *data = reinterpret_cast<const uint8_t *>(str.data());
const int nblocks = static_cast<int>(len / 4);
uint32_t h1 = 0; // seed
const uint32_t c1 = 0xcc9e2d51U;
const uint32_t c2 = 0x1b873593U;
const uint32_t *blocks = reinterpret_cast<const uint32_t *>(data +
nblocks * 4);
for (auto i = -nblocks; i; ++i) {
uint32_t k1 = Getblock(blocks, i);
k1 *= c1;
k1 = Rotl32(k1, 15);
k1 *= c2;
h1 ^= k1;
h1 = Rotl32(h1, 13);
h1 = h1 * 5 + 0xe6546b64;
}
const uint8_t *tail = data + nblocks * 4;
uint32_t k1 = 0;
switch (len & 3) {
case 3:
k1 ^= tail[2] << 16;
case 2:
k1 ^= tail[1] << 8;
case 1:
k1 ^= tail[0];
k1 *= c1;
k1 = Rotl32(k1, 15);
k1 *= c2;
h1 ^= k1;
default:
break;
}
h1 ^= len;
h1 = fmix32(h1);
return hash<int>()(h1);
}
#undef FORCE_INLINE
};
}
#endif
......@@ -24,7 +24,8 @@ Request::Request(
bool
Request::hasParam(const std::string& name) const {
auto it = std::find_if(params_.begin(), params_.end(), [&](const TypedParam& param) {
auto it = std::find_if(params_.begin(), params_.end(),
[&](const TypedParam& param) {
return param.name() == name;
});
......@@ -33,7 +34,8 @@ Request::hasParam(const std::string& name) const {
TypedParam
Request::param(const std::string& name) const {
auto it = std::find_if(params_.begin(), params_.end(), [&](const TypedParam& param) {
auto it = std::find_if(params_.begin(), params_.end(),
[&](const TypedParam& param) {
return param.name() == name;
});
......@@ -49,7 +51,6 @@ Request::splatAt(size_t index) const {
if (index >= splats_.size()) {
throw std::out_of_range("Request splat index out of range");
}
return splats_[index];
}
......@@ -58,157 +59,259 @@ Request::splat() const {
return splats_;
}
Route::Fragment::Fragment(std::string value)
{
if (value.empty())
throw std::runtime_error("Invalid empty fragment");
std::regex SegmentTreeNode::multiple_slash = std::regex("//+",
std::regex_constants::optimize);
init(std::move(value));
SegmentTreeNode::SegmentTreeNode(): resource_ref_(),
fixed_(), param_(), optional_(), splat_(nullptr), route_(nullptr) {
std::shared_ptr<char> ptr(new char[0], std::default_delete<char[]>());
resource_ref_.swap(ptr);
}
bool
Route::Fragment::match(const std::string& raw) const {
if (flags.hasFlag(Flag::Fixed)) {
return raw == value_;
SegmentTreeNode::SegmentTreeNode(const std::shared_ptr<char> &resourceReference) :
resource_ref_(resourceReference), fixed_(), param_(),
optional_(), splat_(nullptr), route_(nullptr) { }
SegmentTreeNode::SegmentType
SegmentTreeNode::getSegmentType(const std::string_view& fragment) {
auto optpos = fragment.find('?');
if (fragment[0] == ':') {
if (optpos != std::string_view::npos) {
if (optpos != fragment.length() - 1) {
throw std::runtime_error("? should be at the end of the string");
}
else if (flags.hasFlag(Flag::Parameter) || flags.hasFlag(Flag::Splat)) {
return true;
return SegmentType::Optional;
}
return false;
}
bool
Route::Fragment::match(const Fragment& other) const {
return match(other.value());
}
void
Route::Fragment::init(std::string value) {
if (value[0] == ':')
flags.setFlag(Flag::Parameter);
else if (value[0] == '*') {
if (value.size() > 1)
return SegmentType::Param;
} else if (fragment[0] == '*') {
if (fragment.length() > 1) {
throw std::runtime_error("Invalid splat parameter");
flags.setFlag(Flag::Splat);
}
else {
flags.setFlag(Flag::Fixed);
return SegmentType::Splat;
}
// Let's search for any '?'
auto pos = value.find('?');
if (pos != std::string::npos) {
if (value[0] != ':')
if (optpos != std::string_view::npos) {
throw std::runtime_error("Only optional parameters are currently supported");
if (pos != value.size() - 1)
throw std::runtime_error("? should be at the end of the string");
value_ = value.substr(0, pos);
flags.setFlag(Flag::Optional);
} else {
value_ = std::move(value);
}
checkInvariant();
return SegmentType::Fixed;
}
void
Route::Fragment::checkInvariant() const {
auto check = [this](std::initializer_list<Flag> exclusiveFlags) {
for (auto flag: exclusiveFlags) {
if (!flags.hasFlag(flag)) return;
std::string SegmentTreeNode::sanitizeResource(const std::string& path) {
const auto& dup = std::regex_replace(path,
SegmentTreeNode::multiple_slash, "/");
if (dup[dup.length() - 1] == '/') {
return dup.substr(1, dup.length() - 2);
}
throw std::logic_error(
std::string("Invariant violated: invalid combination of flags for fragment ") + value_);
};
check({ Flag::Fixed, Flag::Optional });
check({ Flag::Fixed, Flag::Parameter });
check({ Flag::Splat, Flag::Fixed });
check({ Flag::Splat, Flag::Optional });
check({ Flag::Splat, Flag::Parameter });
return dup.substr(1);
}
std::vector<Route::Fragment>
Route::Fragment::fromUrl(const std::string& url) {
std::vector<Route::Fragment> fragments;
std::istringstream iss(url);
std::string p;
while (std::getline(iss, p, '/')) {
if (p.empty()) continue;
fragments.push_back(Fragment(std::move(p)));
void
SegmentTreeNode::addRoute(const std::string_view& path,
const Route::Handler &handler,
const std::shared_ptr<char> &resource_reference) {
// recursion to correct path segment
if (!path.empty()) {
const auto segment_delimiter = path.find('/');
// current segment value
auto current_segment = path.substr(0, segment_delimiter);
// complete child path (path without this segment)
// if no '/' was found, it means that it is a leaf resource
const auto lower_path = (segment_delimiter == std::string_view::npos) ?
std::string_view {nullptr, 0} :
path.substr(segment_delimiter + 1);
std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>> *collection;
const auto fragmentType = getSegmentType(current_segment);
switch (fragmentType) {
case SegmentType::Fixed:
collection = &fixed_;
break;
case SegmentType::Param:
collection = &param_;
break;
case SegmentType::Optional:
// remove the trailing question mark
current_segment = current_segment.substr(0,
current_segment.length() - 1);
collection = &optional_;
break;
case SegmentType::Splat:
if (splat_ == nullptr) {
splat_ = std::make_shared<SegmentTreeNode>(resource_reference);
}
splat_->addRoute(lower_path, handler, resource_reference);
return;
}
return fragments;
}
bool
Route::Fragment::isParameter() const {
return flags.hasFlag(Flag::Parameter);
}
bool
Route::Fragment::isOptional() const {
return isParameter() && flags.hasFlag(Flag::Optional);
}
bool
Route::Fragment::isSplat() const {
return flags.hasFlag(Flag::Splat);
}
std::tuple<bool, std::vector<TypedParam>, std::vector<TypedParam>>
Route::match(const Http::Request& req) const
{
return match(req.resource());
// if the segment tree nodes for the lower path does not exist
if (collection->count(current_segment) == 0) {
// first create it
collection->insert(std::make_pair(current_segment,
std::make_shared<SegmentTreeNode>(resource_reference)));
}
collection->at(current_segment)->addRoute(lower_path, handler,
resource_reference);
} else { // current path segment requested
if (route_ != nullptr)
throw std::runtime_error("Requested route already exist.");
route_ = std::make_shared<Route>(handler);
}
}
std::tuple<bool, std::vector<TypedParam>, std::vector<TypedParam>>
Route::match(const std::string& req) const
{
static const auto NoMatch = std::make_tuple(false, std::vector<TypedParam>(), std::vector<TypedParam>());
auto reqFragments = Fragment::fromUrl(req);
if (reqFragments.size() > fragments_.size())
return NoMatch;
bool Pistache::Rest::SegmentTreeNode::removeRoute(const std::string_view& path) {
// recursion to correct path segment
if (!path.empty()) {
const auto segment_delimiter = path.find('/');
// current segment value
auto current_segment = path.substr(0, segment_delimiter);
// complete child path (path without this segment)
// if no '/' was found, it means that it is a leaf resource
const auto lower_path = (segment_delimiter == std::string_view::npos) ?
std::string_view {nullptr, 0} :
path.substr(segment_delimiter + 1);
std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>> *collection;
auto fragmentType = getSegmentType(current_segment);
switch (fragmentType) {
case SegmentType::Fixed:
collection = &fixed_;
break;
case SegmentType::Param:
collection = &param_;
break;
case SegmentType::Optional:
// remove the trailing question mark
current_segment = current_segment.substr(0,
current_segment.length() - 1);
collection = &optional_;
break;
case SegmentType::Splat:
return splat_->removeRoute(lower_path);
}
std::vector<TypedParam> params;
std::vector<TypedParam> splats;
try {
const bool removable = collection->at(current_segment)->
removeRoute(lower_path);
if (removable) {
collection->erase(current_segment);
}
} catch (const std::out_of_range &) {
throw std::runtime_error("Requested does not exist.");
}
} else { // current leaf requested
route_.reset();
}
return fixed_.empty() && param_.empty() &&
optional_.empty() && splat_ == nullptr && route_ == nullptr;
}
std::tuple<std::shared_ptr<Route>,
std::vector<TypedParam>, std::vector<TypedParam>>
Pistache::Rest::SegmentTreeNode::findRoute(
const std::string_view& path,
std::vector<TypedParam>& params,
std::vector<TypedParam>& splats) const {
// recursion to correct path segment
if (!path.empty()) {
const auto segment_delimiter = path.find('/');
// current segment value
auto current_segment = path.substr(0, segment_delimiter);
// complete child path (path without this segment)
// if no '/' was found, it means that it is a leaf resource
const auto lower_path = (segment_delimiter == std::string_view::npos) ?
std::string_view {nullptr, 0} :
path.substr(segment_delimiter + 1);
// Check if it is a fixed route
if (fixed_.count(current_segment) != 0) {
auto result = fixed_.at(current_segment)->
findRoute(lower_path, params, splats);
auto route = std::get<0>(result);
if (route != nullptr) return result;
}
for (std::vector<Fragment>::size_type i = 0; i < fragments_.size(); ++i) {
const auto& fragment = fragments_[i];
if (i >= reqFragments.size()) {
if (fragment.isOptional())
continue;
// Check if it is a path param
for (const auto &param : param_) {
std::string para_name {param.first.data(), param.first.length()};
std::string para_val {current_segment.data(), current_segment.length()};
params.emplace_back(para_name, para_val);
auto result = param.second->
findRoute(lower_path, params, splats);
auto route = std::get<0>(result);
if (route != nullptr) return result;
params.pop_back();
}
return NoMatch;
// Check if it is an optional path param
for (const auto &optional: optional_) {
std::string opt_name {optional.first.data(), optional.first.length()};
std::string opt_val {current_segment.data(), current_segment.length()};
params.emplace_back(opt_name, opt_val);
auto result = optional.second->
findRoute(lower_path, params, splats);
auto route = std::get<0>(result);
if (route != nullptr) return result;
params.pop_back();
// try to find a route for lower path assuming that
// this optional path param is not present
result = optional.second->
findRoute(lower_path, params, splats);
route = std::get<0>(result);
if (route != nullptr) return result;
}
const auto& reqFragment = reqFragments[i];
if (!fragment.match(reqFragment))
return NoMatch;
// Check if it is a splat
if (splat_ != nullptr) {
std::string splat {current_segment.data(), current_segment.length()};
splats.emplace_back(splat, splat);
auto result = splat_->
findRoute(lower_path, params, splats);
if (fragment.isParameter()) {
params.push_back(TypedParam(fragment.value(), reqFragment.value()));
} else if (fragment.isSplat()) {
splats.push_back(TypedParam(reqFragment.value(), reqFragment.value()));
auto route = std::get<0>(result);
if (route != nullptr) return result;
splats.pop_back();
}
// Requested route does not exists
return std::make_tuple(nullptr, std::vector<TypedParam>(),
std::vector<TypedParam>());
} else { // current leaf requested, or empty final optional
if (!optional_.empty()) {
// in case of more than one optional at this point, as it is an
// ambuiguity, it is resolved by using the first optional
auto optional = optional_.begin();
std::string opt {optional->first.data(), optional->first.length()};
return optional->second->findRoute(path, params, splats);
} else if (route_ == nullptr) {
// if we are here but route is null, we reached this point
// trying to parse an optional, that was missing
return std::make_tuple(nullptr, std::vector<TypedParam>(),
std::vector<TypedParam>());
} else {
return std::make_tuple(route_, std::move(params), std::move(splats));
}
}
}
return make_tuple(true, std::move(params), std::move(splats));
std::tuple<std::shared_ptr<Route>, std::vector<TypedParam>,
std::vector<TypedParam>>
Pistache::Rest::SegmentTreeNode::findRoute(const std::string_view& path) const {
std::vector<TypedParam> params;
std::vector<TypedParam> splats;
return findRoute(path, params, splats);
}
namespace Private {
RouterHandler::RouterHandler(const Rest::Router& router)
: router(router)
: router(std::make_shared<Rest::Router>(router))
{
}
RouterHandler::RouterHandler(std::shared_ptr<Rest::Router> router)
: router(std::move(router))
{
}
......@@ -218,15 +321,15 @@ RouterHandler::onRequest(
Http::ResponseWriter response)
{
auto resp = response.clone();
auto result = router.route(req, std::move(resp));
auto result = router->route(req, std::move(resp));
/* @Feature: add support for a custom NotFound handler */
if (result == Router::Status::NotFound)
if (result == Route::Status::NotFound)
{
if (router.hasNotFoundHandler())
if (router->hasNotFoundHandler())
{
auto resp2 = response.clone();
router.invokeNotFoundHandler(req, std::move(resp2));
router->invokeNotFoundHandler(req, std::move(resp2));
}
else
response.send(Http::Code::Not_Found, "Could not find a matching route");
......@@ -247,6 +350,11 @@ Router::handler() const {
return std::make_shared<Private::RouterHandler>(*this);
}
std::shared_ptr<Private::RouterHandler>
Router::handler(std::shared_ptr<Rest::Router> router) {
return std::make_shared<Private::RouterHandler>(router);
}
void
Router::initFromDescription(const Rest::Description& desc) {
auto paths = desc.rawPaths();
......@@ -258,39 +366,49 @@ Router::initFromDescription(const Rest::Description& desc) {
oss << "Path '" << path.value << "' is not bound";
throw std::runtime_error(oss.str());
}
addRoute(path.method, std::move(path.value), std::move(path.handler));
addRoute(path.method, path.value, path.handler);
}
}
}
void
Router::get(std::string resource, Route::Handler handler) {
addRoute(Http::Method::Get, std::move(resource), std::move(handler));
Router::get(const std::string& resource, Route::Handler handler) {
addRoute(Http::Method::Get, resource, std::move(handler));
}
void
Router::post(std::string resource, Route::Handler handler) {
addRoute(Http::Method::Post, std::move(resource), std::move(handler));
Router::post(const std::string& resource, Route::Handler handler) {
addRoute(Http::Method::Post, resource, std::move(handler));
}
void
Router::put(std::string resource, Route::Handler handler) {
addRoute(Http::Method::Put, std::move(resource), std::move(handler));
Router::put(const std::string& resource, Route::Handler handler) {
addRoute(Http::Method::Put, resource, std::move(handler));
}
void
Router::patch(std::string resource, Route::Handler handler) {
addRoute(Http::Method::Patch, std::move(resource), std::move(handler));
Router::patch(const std::string& resource, Route::Handler handler) {
addRoute(Http::Method::Patch, resource, std::move(handler));
}
void
Router::del(std::string resource, Route::Handler handler) {
addRoute(Http::Method::Delete, std::move(resource), std::move(handler));
Router::del(const std::string& resource, Route::Handler handler) {
addRoute(Http::Method::Delete, resource, std::move(handler));
}
void
Router::options(std::string resource, Route::Handler handler) {
addRoute(Http::Method::Options, std::move(resource), std::move(handler));
Router::options(const std::string& resource, Route::Handler handler) {
addRoute(Http::Method::Options, resource, handler);
}
void
Router::removeRoute(Http::Method method, const std::string& resource) {
if (resource.empty()) throw std::runtime_error("Invalid zero-length URL.");
auto& r = routes[method];
const auto sanitized = SegmentTreeNode::sanitizeResource(resource);
const std::string_view path {sanitized.data(), sanitized.size()};
r.removeRoute(path);
}
void
......@@ -309,61 +427,76 @@ Router::invokeNotFoundHandler(const Http::Request &req, Http::ResponseWriter res
notFoundHandler(Rest::Request(std::move(req), std::vector<TypedParam>(), std::vector<TypedParam>()), std::move(resp));
}
Router::Status
Route::Status
Router::route(const Http::Request& req, Http::ResponseWriter response) {
const auto resource = req.resource();
if (resource.empty()) throw std::runtime_error("Invalid zero-length URL.");
auto& r = routes[req.method()];
for (const auto& route: r) {
bool match;
std::vector<TypedParam> params;
std::vector<TypedParam> splats;
std::tie(match, params, splats) = route.match(req);
if (match) {
route.invokeHandler(Request(req, std::move(params), std::move(splats)), std::move(response));
return Router::Status::Match;
}
const auto sanitized = SegmentTreeNode::sanitizeResource(resource);
const std::string_view path {sanitized.data(), sanitized.size()};
auto result = r.findRoute(path);
auto route = std::get<0>(result);
if (route != nullptr) {
auto params = std::get<1>(result);
auto splats = std::get<2>(result);
route->invokeHandler(Request(req, std::move(params), std::move(splats)),
std::move(response));
return Route::Status::Match;
}
for (const auto& handler: customHandlers) {
auto resp = response.clone();
auto result = handler(Request(req, std::vector<TypedParam>(), std::vector<TypedParam>()), std::move(resp));
if (result == Route::Result::Ok) return Router::Status::Match;
auto handler1 = handler(Request(req, std::vector<TypedParam>(),
std::vector<TypedParam>()), std::move(resp));
if (handler1 == Route::Result::Ok) return Route::Status::Match;
}
return Router::Status::NotFound;
if (hasNotFoundHandler()) invokeNotFoundHandler(req, std::move(response));
return Route::Status::NotFound;
}
void
Router::addRoute(Http::Method method, std::string resource, Route::Handler handler)
{
void Router::addRoute(Http::Method method,
const std::string& resource, Route::Handler handler) {
if (resource.empty()) throw std::runtime_error("Invalid zero-length URL.");
auto& r = routes[method];
r.push_back(Route(std::move(resource), method, std::move(handler)));
const auto sanitized = SegmentTreeNode::sanitizeResource(resource);
std::shared_ptr<char> ptr(new char[sanitized.length()],
std::default_delete<char[]>());
memcpy(ptr.get(), sanitized.data(), sanitized.length());
const std::string_view path {ptr.get(), sanitized.length()};
r.addRoute(path, handler, ptr);
}
namespace Routes {
void Get(Router& router, std::string resource, Route::Handler handler) {
router.get(std::move(resource), std::move(handler));
void Get(Router& router, const std::string& resource, Route::Handler handler) {
router.get(resource, std::move(handler));
}
void Post(Router& router, const std::string& resource, Route::Handler handler) {
router.post(resource, std::move(handler));
}
void Post(Router& router, std::string resource, Route::Handler handler) {
router.post(std::move(resource), std::move(handler));
void Put(Router& router, const std::string& resource, Route::Handler handler) {
router.put(resource, std::move(handler));
}
void Put(Router& router, std::string resource, Route::Handler handler) {
router.put(std::move(resource), std::move(handler));
void Patch(Router& router, const std::string& resource, Route::Handler handler) {
router.patch(resource, std::move(handler));
}
void Patch(Router& router, std::string resource, Route::Handler handler) {
router.patch(std::move(resource), std::move(handler));
void Delete(Router& router, const std::string& resource, Route::Handler handler) {
router.del(resource, std::move(handler));
}
void Delete(Router& router, std::string resource, Route::Handler handler) {
router.del(std::move(resource), std::move(handler));
void Options(Router& router, const std::string& resource, Route::Handler handler) {
router.options(resource, std::move(handler));
}
void Options(Router& router, std::string resource, Route::Handler handler) {
router.options(std::move(resource), std::move(handler));
void Remove(Router& router, Http::Method method, const std::string& resource) {
router.removeRoute(method, resource);
}
void NotFound(Router& router, Route::Handler handler) {
......
......@@ -21,3 +21,4 @@ pistache_test(net_test)
pistache_test(listener_test)
pistache_test(payload_test)
pistache_test(rest_server_test)
pistache_test(string_view_test)
\ No newline at end of file
......@@ -10,119 +10,137 @@
#include <pistache/router.h>
using namespace Pistache;
using namespace Pistache::Rest;
bool match(const Rest::Route& route, const std::string& req) {
return std::get<0>(route.match(req));
bool match(const SegmentTreeNode& routes, const std::string& req) {
const auto& s = SegmentTreeNode::sanitizeResource(req);
std::shared_ptr<Route> route;
std::tie(route, std::ignore, std::ignore) = routes.findRoute({s.data(), s.size()});
return route != nullptr;
}
bool matchParams(
const Rest::Route& route, const std::string& req,
std::initializer_list<std::pair<std::string, std::string>> list)
{
bool ok;
std::vector<Rest::TypedParam> params;
std::tie(ok, params, std::ignore) = route.match(req);
const SegmentTreeNode& routes, const std::string& req,
std::initializer_list<std::pair<std::string, std::string>> list) {
if (!ok) return false;
const auto& s = SegmentTreeNode::sanitizeResource(req);
std::shared_ptr<Route> route;
std::vector<TypedParam> params;
std::string_view sv {s.data(), s.length()};
std::tie(route, params, std::ignore) = routes.findRoute(sv);
if (route == nullptr) return false;
for (const auto& p: list) {
auto it = std::find_if(params.begin(), params.end(), [&](const Rest::TypedParam& param) {
auto it = std::find_if(params.begin(), params.end(),
[&](const TypedParam& param) {
return param.name() == p.first;
});
if (it == std::end(params)) {
std::cerr << "Did not find param '" << p.first << "'" << std::endl;
return false;
}
if (it->as<std::string>() != p.second) {
std::cerr << "Param '" << p.first << "' mismatched ("
<< p.second << " != " << it->as<std::string>() << ")" << std::endl;
return false;
}
if (it == std::end(params)) return false;
if (it->as<std::string>() != p.second) return false;
}
return true;
}
bool matchSplat(
const Rest::Route& route, const std::string& req,
std::initializer_list<std::string> list)
{
bool ok;
std::vector<Rest::TypedParam> splats;
std::tie(ok, std::ignore, splats) = route.match(req);
if (!ok) return false;
if (list.size() != splats.size()) {
std::cerr << "Size mismatch (" << list.size() << " != " << splats.size() << ")"
<< std::endl;
return false;
}
const SegmentTreeNode& routes, const std::string& req,
std::initializer_list<std::string> list) {
const auto& s = SegmentTreeNode::sanitizeResource(req);
std::shared_ptr<Route> route;
std::vector<TypedParam> splats;
std::string_view sv {s.data(), s.length()};
std::tie(route, std::ignore, splats) = routes.findRoute(sv);
if (route == nullptr) return false;
if (list.size() != splats.size()) return false;
size_t i = 0;
for (const auto& s: list) {
for (const auto& s : list) {
auto splat = splats[i].as<std::string>();
if (splat != s) {
std::cerr << "Splat number " << i << " did not match ("
<< splat << " != " << s << ")" << std::endl;
return false;
}
if (splat != s) return false;
++i;
}
return true;
}
Rest::Route
makeRoute(std::string value) {
auto noop = [](const Http::Request&, Http::Response) { return Rest::Route::Result::Ok; };
return Rest::Route(value, Http::Method::Get, noop);
}
TEST(router_test, test_fixed_routes) {
auto r1 = makeRoute("/v1/hello");
ASSERT_TRUE(match(r1, "/v1/hello"));
ASSERT_FALSE(match(r1, "/v2/hello"));
ASSERT_FALSE(match(r1, "/v1/hell0"));
SegmentTreeNode routes;
auto s = SegmentTreeNode::sanitizeResource("/v1/hello");
routes.addRoute(std::string_view {s.data(), s.length()}, nullptr, nullptr);
ASSERT_TRUE(match(routes, "/v1/hello"));
ASSERT_FALSE(match(routes, "/v2/hello"));
ASSERT_FALSE(match(routes, "/v1/hell0"));
auto r2 = makeRoute("/a/b/c");
ASSERT_TRUE(match(r2, "/a/b/c"));
s = SegmentTreeNode::sanitizeResource("/a/b/c");
routes.addRoute(std::string_view {s.data(), s.length()}, nullptr, nullptr);
ASSERT_TRUE(match(routes, "/a/b/c"));
}
TEST(router_test, test_parameters) {
auto r1 = makeRoute("/v1/hello/:name");
ASSERT_TRUE(matchParams(r1, "/v1/hello/joe", {
SegmentTreeNode routes;
const auto& s = SegmentTreeNode::sanitizeResource("/v1/hello/:name/");
routes.addRoute(std::string_view {s.data(), s.length()}, nullptr, nullptr);
ASSERT_TRUE(matchParams(routes, "/v1/hello/joe", {
{ ":name", "joe" }
}));
auto r2 = makeRoute("/greetings/:from/:to");
ASSERT_TRUE(matchParams(r2, "/greetings/foo/bar", {
const auto& p = SegmentTreeNode::sanitizeResource("/greetings/:from/:to");
routes.addRoute(std::string_view {p.data(), p.length()}, nullptr, nullptr);
ASSERT_TRUE(matchParams(routes, "/greetings/foo/bar", {
{ ":from", "foo" },
{ ":to" , "bar" }
}));
}
TEST(router_test, test_optional) {
auto r1 = makeRoute("/get/:key?");
ASSERT_TRUE(match(r1, "/get"));
ASSERT_TRUE(match(r1, "/get/"));
ASSERT_TRUE(matchParams(r1, "/get/foo", {
{ ":key", "foo" }
SegmentTreeNode routes;
auto s = SegmentTreeNode::sanitizeResource("/get/:key?/bar");
routes.addRoute(std::string_view {s.data(), s.length()}, nullptr, nullptr);
ASSERT_FALSE(matchParams(routes, "/get/bar", {
{ ":key", "whatever" }
}));
ASSERT_TRUE(matchParams(r1, "/get/foo/", {
ASSERT_TRUE(matchParams(routes, "/get/foo/bar", {
{ ":key", "foo" }
}));
ASSERT_FALSE(match(r1, "/get/foo/bar"));
}
TEST(router_test, test_splat) {
auto r1 = makeRoute("/say/*/to/*");
ASSERT_TRUE(match(r1, "/say/hello/to/user"));
ASSERT_FALSE(match(r1, "/say/hello/to"));
ASSERT_FALSE(match(r1, "/say/hello/to/user/please"));
SegmentTreeNode routes;
auto s = SegmentTreeNode::sanitizeResource("/say/*/to/*");
routes.addRoute(std::string_view {s.data(), s.length()}, nullptr, nullptr);
ASSERT_TRUE(match(routes, "/say/hello/to/user"));
ASSERT_FALSE(match(routes, "/say/hello/to"));
ASSERT_FALSE(match(routes, "/say/hello/to/user/please"));
ASSERT_TRUE(matchSplat(routes, "/say/hello/to/user", { "hello", "user" }));
ASSERT_TRUE(matchSplat(routes, "/say/hello/to/user/", { "hello", "user" }));
}
TEST(router_test, test_sanitize) {
SegmentTreeNode routes;
auto s = SegmentTreeNode::sanitizeResource("//v1//hello/");
routes.addRoute(std::string_view {s.data(), s.length()}, nullptr, nullptr);
ASSERT_TRUE(match(routes, "/v1/hello////"));
}
TEST(router_test, test_mixed) {
SegmentTreeNode routes;
auto s = SegmentTreeNode::sanitizeResource("/hello");
auto p = SegmentTreeNode::sanitizeResource("/*");
routes.addRoute(std::string_view {s.data(), s.length()}, nullptr, nullptr);
routes.addRoute(std::string_view {p.data(), p.length()}, nullptr, nullptr);
ASSERT_TRUE(match(routes, "/hello"));
ASSERT_TRUE(match(routes, "/hi"));
ASSERT_TRUE(matchSplat(r1, "/say/hello/to/user", { "hello", "user" }));
ASSERT_TRUE(matchSplat(r1, "/say/hello/to/user/", { "hello", "user" }));
ASSERT_FALSE(matchSplat(routes, "/hello", { "hello" }));
ASSERT_TRUE(matchSplat(routes, "/hi", { "hi" }));
}
#include "gtest/gtest.h"
#include <pistache/string_view.h>
TEST(string_view_test, substr_test) {
std::string_view orig ("test");
std::string_view targ ("est");
std::string_view sub = orig.substr(1);
ASSERT_EQ(sub, targ);
sub = orig.substr(1, 10);
ASSERT_EQ(sub, targ);
ASSERT_THROW(orig.substr(6), std::out_of_range);
}
TEST(string_view_test, find_test) {
std::string_view orig ("test");
std::string_view find ("est");
ASSERT_EQ(orig.find(find), 1);
ASSERT_EQ(orig.find(find, 1), 1);
ASSERT_EQ(orig.find(find, 2), std::size_t(-1));
ASSERT_EQ(orig.find('e'), 1);
ASSERT_EQ(orig.find('e', 1), 1);
ASSERT_EQ(orig.find('e', 2), std::size_t(-1));
ASSERT_EQ(orig.find('1'), std::size_t(-1));
ASSERT_EQ(orig.find("est"), 1);
ASSERT_EQ(orig.find("est", 1), 1);
ASSERT_EQ(orig.find("est", 1, 2), 1);
ASSERT_EQ(orig.find("set"), std::size_t(-1));
ASSERT_EQ(orig.find("est", 2), std::size_t(-1));
ASSERT_EQ(orig.find("est", 2, 2), std::size_t(-1));
}
TEST(string_view_test, rfind_test) {
std::string_view orig ("test");
std::string_view find ("est");
ASSERT_EQ(orig.rfind(find), 1);
ASSERT_EQ(orig.rfind(find, 1), 1);
ASSERT_EQ(orig.rfind('e'), 1);
ASSERT_EQ(orig.rfind('e', 1), 1);
ASSERT_EQ(orig.rfind('q'), std::size_t(-1));
ASSERT_EQ(orig.rfind("est"), 1);
ASSERT_EQ(orig.rfind("est", 1), 1);
ASSERT_EQ(orig.rfind("est", 1, 2), 1);
ASSERT_EQ(orig.rfind("set"), std::size_t(-1));
}
TEST(string_view_test, emptiness) {
std::string_view e1;
std::string_view e2 ("");
std::string_view e3 ("test", 0);
std::string_view ne ("test");
ASSERT_TRUE(e1.empty());
ASSERT_TRUE(e2.empty());
ASSERT_TRUE(e3.empty());
ASSERT_FALSE(ne.empty());
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment