From 033f92d123357364f2bf9608cf27a5b386393db6 Mon Sep 17 00:00:00 2001 From: helintongh Date: Wed, 10 Jan 2024 11:22:31 +0800 Subject: [PATCH] feat: add radix tree router and regex router for restful api (#475) --- include/cinatra/coro_http_connection.hpp | 65 +++- include/cinatra/coro_http_request.hpp | 7 + include/cinatra/coro_http_router.hpp | 103 +++++- include/cinatra/coro_radix_tree.hpp | 409 +++++++++++++++++++++++ tests/test_coro_http_server.cpp | 145 ++++++++ 5 files changed, 711 insertions(+), 18 deletions(-) create mode 100644 include/cinatra/coro_radix_tree.hpp diff --git a/include/cinatra/coro_http_connection.hpp b/include/cinatra/coro_http_connection.hpp index 688261a6..04ed714f 100644 --- a/include/cinatra/coro_http_connection.hpp +++ b/include/cinatra/coro_http_connection.hpp @@ -184,8 +184,69 @@ class coro_http_connection co_await router_.route_coro(coro_handler, request_, response_, key); } else { - // not found - response_.set_status(status_type::not_found); + bool is_exist = false; + std::function + handler; + std::string method_str; + method_str.assign(parser_.method().data(), parser_.method().length()); + std::string url_path; + url_path.assign(parser_.url().data(), parser_.url().length()); + std::tie(is_exist, handler, request_.params_) = + router_.get_router_tree()->get(url_path, method_str); + if (is_exist) { + (handler)(request_, response_); + } + else { + bool is_coro_exist = false; + std::function( + coro_http_request & req, coro_http_response & resp)> + coro_handler; + + std::tie(is_coro_exist, coro_handler, request_.params_) = + router_.get_coro_router_tree()->get_coro(url_path, method_str); + + if (is_coro_exist) { + co_await (coro_handler)(request_, response_); + } + else { + bool is_matched_regex_router = false; + // coro regex router + auto coro_regex_handlers = router_.get_coro_regex_handlers(); + if (coro_regex_handlers.size() != 0) { + for (auto &pair : coro_regex_handlers) { + std::string coro_regex_key; + coro_regex_key.assign(key.data(), key.size()); + + if (std::regex_match(coro_regex_key, request_.matches_, + std::get<0>(pair))) { + auto coro_handler = std::get<1>(pair); + co_await (coro_handler)(request_, response_); + is_matched_regex_router = true; + } + } + } + // regex router + if (!is_matched_regex_router) { + auto regex_handlers = router_.get_regex_handlers(); + if (regex_handlers.size() != 0) { + for (auto &pair : regex_handlers) { + std::string regex_key; + regex_key.assign(key.data(), key.size()); + if (std::regex_match(regex_key, request_.matches_, + std::get<0>(pair))) { + auto handler = std::get<1>(pair); + (handler)(request_, response_); + is_matched_regex_router = true; + } + } + } + } + // not found + if (!is_matched_regex_router) + response_.set_status(status_type::not_found); + } + } } } diff --git a/include/cinatra/coro_http_request.hpp b/include/cinatra/coro_http_request.hpp index 6ae063dc..94cda89b 100644 --- a/include/cinatra/coro_http_request.hpp +++ b/include/cinatra/coro_http_request.hpp @@ -1,5 +1,7 @@ #pragma once + #include +#include #include "async_simple/coro/Lazy.h" #include "define.h" @@ -7,6 +9,7 @@ #include "ws_define.h" namespace cinatra { + inline std::vector split_sv(std::string_view s, std::string_view delimiter) { size_t start = 0; @@ -108,6 +111,7 @@ inline std::vector> parse_ranges(std::string_view range_str, } return vec; } + class coro_http_connection; class coro_http_request { public: @@ -221,6 +225,9 @@ class coro_http_request { return true; } + std::unordered_map params_; + std::smatch matches_; + private: http_parser& parser_; std::string_view body_; diff --git a/include/cinatra/coro_http_router.hpp b/include/cinatra/coro_http_router.hpp index 94bba8ac..fc0503a6 100644 --- a/include/cinatra/coro_http_router.hpp +++ b/include/cinatra/coro_http_router.hpp @@ -10,7 +10,9 @@ #include "cinatra/cinatra_log_wrapper.hpp" #include "cinatra/coro_http_request.hpp" +#include "cinatra/coro_radix_tree.hpp" #include "cinatra/response_cv.hpp" +#include "cinatra/utils.hpp" #include "coro_http_response.hpp" #include "ylt/util/type_traits.h" @@ -51,27 +53,69 @@ class coro_http_router { // std::string_view, avoid memcpy when route using return_type = typename util::function_traits::return_type; if constexpr (is_lazy_v) { - auto [it, ok] = coro_keys_.emplace(std::move(whole_str)); - if (!ok) { - CINATRA_LOG_WARNING << key << " has already registered."; - return; + if (whole_str.find(":") != std::string::npos) { + std::vector coro_method_names = {}; + std::string coro_method_str; + coro_method_str.append(method_name); + coro_method_names.push_back(coro_method_str); + coro_router_tree_->coro_insert(key, std::move(handler), + coro_method_names); } - coro_handles_.emplace(*it, std::move(handler)); - if (!aspects.empty()) { - has_aspects_ = true; - aspects_.emplace(*it, std::move(aspects)); + else { + if (whole_str.find("{") != std::string::npos || + whole_str.find(")") != std::string::npos) { + std::string pattern = whole_str; + + if (pattern.find("{}") != std::string::npos) { + replace_all(pattern, "{}", "([^/]+)"); + } + + coro_regex_handles_.emplace_back(std::regex(pattern), + std::move(handler)); + } + else { + auto [it, ok] = coro_keys_.emplace(std::move(whole_str)); + if (!ok) { + CINATRA_LOG_WARNING << key << " has already registered."; + return; + } + coro_handles_.emplace(*it, std::move(handler)); + if (!aspects.empty()) { + has_aspects_ = true; + aspects_.emplace(*it, std::move(aspects)); + } + } } } else { - auto [it, ok] = keys_.emplace(std::move(whole_str)); - if (!ok) { - CINATRA_LOG_WARNING << key << " has already registered."; - return; + if (whole_str.find(':') != std::string::npos) { + std::vector method_names = {}; + std::string method_str; + method_str.append(method_name); + method_names.push_back(method_str); + router_tree_->insert(key, std::move(handler), method_names); + } + else if (whole_str.find("{") != std::string::npos || + whole_str.find(")") != std::string::npos) { + std::string pattern = whole_str; + + if (pattern.find("{}") != std::string::npos) { + replace_all(pattern, "{}", "([^/]+)"); + } + + regex_handles_.emplace_back(std::regex(pattern), std::move(handler)); } - map_handles_.emplace(*it, std::move(handler)); - if (!aspects.empty()) { - has_aspects_ = true; - aspects_.emplace(*it, std::move(aspects)); + else { + auto [it, ok] = keys_.emplace(std::move(whole_str)); + if (!ok) { + CINATRA_LOG_WARNING << key << " has already registered."; + return; + } + map_handles_.emplace(*it, std::move(handler)); + if (!aspects.empty()) { + has_aspects_ = true; + aspects_.emplace(*it, std::move(aspects)); + } } } } @@ -148,6 +192,16 @@ class coro_http_router { const auto& get_coro_handlers() const { return coro_handles_; } + std::shared_ptr get_router_tree() { return router_tree_; } + + std::shared_ptr get_coro_router_tree() { + return coro_router_tree_; + } + + const auto& get_coro_regex_handlers() { return coro_regex_handles_; } + + const auto& get_regex_handlers() { return regex_handles_; } + bool handle_aspects(auto& req, auto& resp, auto& aspects, bool before) { bool r = true; for (auto& aspect : aspects) { @@ -192,6 +246,23 @@ class coro_http_router { std::function( coro_http_request& req, coro_http_response& resp)>> coro_handles_; + + std::shared_ptr router_tree_ = + std::make_shared(radix_tree()); + + std::shared_ptr coro_router_tree_ = + std::make_shared(radix_tree()); + + std::vector>> + regex_handles_; + + std::vector( + coro_http_request& req, coro_http_response& resp)>>> + coro_regex_handles_; + std::unordered_map>> aspects_; diff --git a/include/cinatra/coro_radix_tree.hpp b/include/cinatra/coro_radix_tree.hpp new file mode 100644 index 00000000..585446b8 --- /dev/null +++ b/include/cinatra/coro_radix_tree.hpp @@ -0,0 +1,409 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "cinatra/coro_http_request.hpp" +#include "coro_http_response.hpp" +#include "ylt/util/type_traits.h" + +namespace cinatra { +constexpr char type_asterisk = '*'; +constexpr char type_colon = ':'; +constexpr char type_slash = '/'; + +typedef std::tuple< + bool, std::function, + std::unordered_map> + parse_result; + +typedef std::tuple( + coro_http_request &req, coro_http_response &resp)>, + std::unordered_map> + coro_result; + +struct handler_t { + std::string method; + std::function handler; +}; + +struct coro_handler_t { + std::string method; + std::function(coro_http_request &req, + coro_http_response &resp)> + coro_handler; +}; + +struct radix_tree_node { + std::string path; + std::vector handlers; + std::vector coro_handlers; + std::string indices; + std::vector> children; + int max_params; + + radix_tree_node() = default; + radix_tree_node(const std::string &path) { this->path = path; } + ~radix_tree_node() {} + + std::function + get_handler(const std::string &method) { + for (auto &h : this->handlers) { + if (h.method == method) { + return h.handler; + } + } + return nullptr; + } + + std::function(coro_http_request &req, + coro_http_response &resp)> + get_coro_handler(const std::string &method) { + for (auto &h : this->coro_handlers) { + if (h.method == method) { + return h.coro_handler; + } + } + return nullptr; + } + + int add_handler( + std::function + handler, + const std::vector &methods) { + for (auto &m : methods) { + auto old_handler = this->get_handler(m); + this->handlers.push_back(handler_t{m, handler}); + } + return 0; + } + + int add_coro_handler(std::function( + coro_http_request &req, coro_http_response &resp)> + coro_handler, + const std::vector &methods) { + for (auto &m : methods) { + auto old_coro_handler = this->get_coro_handler(m); + this->coro_handlers.push_back(coro_handler_t{m, coro_handler}); + } + return 0; + } + + std::shared_ptr insert_child( + char index, std::shared_ptr child) { + auto i = this->get_index_position(index); + this->indices.insert(this->indices.begin() + i, index); + this->children.insert(this->children.begin() + i, child); + return child; + } + + std::shared_ptr get_child(char index) { + auto i = this->get_index_position(index); + return this->indices[i] != index ? nullptr : this->children[i]; + } + + int get_index_position(char target) { + int low = 0, high = this->indices.size(), mid; + + while (low < high) { + mid = low + ((high - low) >> 1); + if (this->indices[mid] < target) + low = mid + 1; + else + high = mid; + } + return low; + } +}; + +class radix_tree { + public: + radix_tree() { + this->root = std::make_shared(radix_tree_node()); + } + + ~radix_tree() {} + + int insert( + const std::string &path, + std::function + handler, + const std::vector &methods) { + auto root = this->root; + int i = 0, n = path.size(), param_count = 0, code = 0; + while (i < n) { + if (!root->indices.empty() && + (root->indices[0] == type_asterisk || path[i] == type_asterisk || + (path[i] != type_colon && root->indices[0] == type_colon) || + (path[i] == type_colon && root->indices[0] != type_colon) || + (path[i] == type_colon && root->indices[0] == type_colon && + path.substr(i + 1, find_pos(path, type_slash, i) - i - 1) != + root->children[0]->path))) { + code = -1; + break; + } + + auto child = root->get_child(path[i]); + if (!child) { + auto p = find_pos(path, type_colon, i); + + if (p == n) { + p = find_pos(path, type_asterisk, i); + + root = root->insert_child(path[i], std::make_shared( + path.substr(i, p - i))); + + if (p < n) { + root = root->insert_child( + type_asterisk, + std::make_shared(path.substr(p + 1))); + ++param_count; + } + + code = root->add_handler(handler, methods); + break; + } + + root = root->insert_child( + path[i], std::make_shared(path.substr(i, p - i))); + + i = find_pos(path, type_slash, p); + + root = root->insert_child( + type_colon, + std::make_shared(path.substr(p + 1, i - p - 1))); + ++param_count; + + if (i == n) { + code = root->add_handler(handler, methods); + break; + } + } + else { + root = child; + + if (path[i] == type_colon) { + ++param_count; + i += root->path.size() + 1; + + if (i == n) { + code = root->add_handler(handler, methods); + break; + } + } + else { + auto j = 0UL; + auto m = root->path.size(); + + for (; i < n && j < m && path[i] == root->path[j]; ++i, ++j) { + } + + if (j < m) { + std::shared_ptr child( + std::make_shared(root->path.substr(j))); + child->handlers = root->handlers; + child->indices = root->indices; + child->children = root->children; + + root->path = root->path.substr(0, j); + root->handlers = {}; + root->indices = child->path[0]; + root->children = {child}; + } + + if (i == n) { + code = root->add_handler(handler, methods); + break; + } + } + } + } + + if (param_count > this->root->max_params) + this->root->max_params = param_count; + + return code; + } + + int coro_insert(const std::string &path, + std::function( + coro_http_request &req, coro_http_response &resp)> + coro_handler, + const std::vector &methods) { + auto root = this->root; + int i = 0, n = path.size(), param_count = 0, code = 0; + while (i < n) { + if (!root->indices.empty() && + (root->indices[0] == type_asterisk || path[i] == type_asterisk || + (path[i] != type_colon && root->indices[0] == type_colon) || + (path[i] == type_colon && root->indices[0] != type_colon) || + (path[i] == type_colon && root->indices[0] == type_colon && + path.substr(i + 1, find_pos(path, type_slash, i) - i - 1) != + root->children[0]->path))) { + code = -1; + break; + } + + auto child = root->get_child(path[i]); + if (!child) { + auto p = find_pos(path, type_colon, i); + + if (p == n) { + p = find_pos(path, type_asterisk, i); + + root = root->insert_child(path[i], std::make_shared( + path.substr(i, p - i))); + + if (p < n) { + root = root->insert_child( + type_asterisk, + std::make_shared(path.substr(p + 1))); + ++param_count; + } + + code = root->add_coro_handler(coro_handler, methods); + break; + } + + root = root->insert_child( + path[i], std::make_shared(path.substr(i, p - i))); + + i = find_pos(path, type_slash, p); + + root = root->insert_child( + type_colon, + std::make_shared(path.substr(p + 1, i - p - 1))); + ++param_count; + + if (i == n) { + code = root->add_coro_handler(coro_handler, methods); + break; + } + } + else { + root = child; + + if (path[i] == type_colon) { + ++param_count; + i += root->path.size() + 1; + + if (i == n) { + code = root->add_coro_handler(coro_handler, methods); + break; + } + } + else { + auto j = 0UL; + auto m = root->path.size(); + + for (; i < n && j < m && path[i] == root->path[j]; ++i, ++j) { + } + + if (j < m) { + std::shared_ptr child( + std::make_shared(root->path.substr(j))); + child->handlers = root->handlers; + child->indices = root->indices; + child->children = root->children; + + root->path = root->path.substr(0, j); + root->handlers = {}; + root->indices = child->path[0]; + root->children = {child}; + } + + if (i == n) { + code = root->add_coro_handler(coro_handler, methods); + break; + } + } + } + } + + if (param_count > this->root->max_params) + this->root->max_params = param_count; + + return code; + } + + parse_result get(const std::string &path, const std::string &method) { + std::unordered_map params; + auto root = this->root; + + int i = 0, n = path.size(), p; + + while (i < n) { + if (root->indices.empty()) + return parse_result(); + + if (root->indices[0] == type_colon) { + root = root->children[0]; + + p = find_pos(path, type_slash, i); + params[root->path] = path.substr(i, p - i); + i = p; + } + else if (root->indices[0] == type_asterisk) { + root = root->children[0]; + params[root->path] = path.substr(i); + break; + } + else { + root = root->get_child(path[i]); + if (!root || path.substr(i, root->path.size()) != root->path) + return parse_result(); + i += root->path.size(); + } + } + + return parse_result{true, root->get_handler(method), params}; + } + + coro_result get_coro(const std::string &path, const std::string &method) { + std::unordered_map params; + auto root = this->root; + + int i = 0, n = path.size(), p; + + while (i < n) { + if (root->indices.empty()) + return coro_result(); + + if (root->indices[0] == type_colon) { + root = root->children[0]; + + p = find_pos(path, type_slash, i); + params[root->path] = path.substr(i, p - i); + i = p; + } + else if (root->indices[0] == type_asterisk) { + root = root->children[0]; + params[root->path] = path.substr(i); + break; + } + else { + root = root->get_child(path[i]); + if (!root || path.substr(i, root->path.size()) != root->path) + return coro_result(); + i += root->path.size(); + } + } + + return coro_result{true, root->get_coro_handler(method), params}; + } + + private: + int find_pos(const std::string &str, char target, int start) { + auto i = str.find(target, start); + return i == -1 ? str.size() : i; + } + + std::shared_ptr root; +}; +} // namespace cinatra \ No newline at end of file diff --git a/tests/test_coro_http_server.cpp b/tests/test_coro_http_server.cpp index 7a23ee72..094930c2 100644 --- a/tests/test_coro_http_server.cpp +++ b/tests/test_coro_http_server.cpp @@ -1160,6 +1160,151 @@ TEST_CASE("test http download server") { } } +TEST_CASE("test restful api") { + cinatra::coro_http_server server(1, 9001); + + server.set_http_handler( + "/test2/{}/test3/{}", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + // coroutine in other thread. + CHECK(req.matches_.str(1) == "name"); + CHECK(req.matches_.str(2) == "test"); + resp.set_status_and_content(cinatra::status_type::ok, "hello world"); + }); + co_return; + }); + + server.set_http_handler( + R"(/numbers/(\d+)/test/(\d+))", + [](coro_http_request &req, coro_http_response &response) { + CHECK(req.matches_.str(1) == "100"); + CHECK(req.matches_.str(2) == "200"); + response.set_status_and_content(status_type::ok, "number regex ok"); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client; + client.get("http://127.0.0.1:9001/test2/name/test3/test"); + client.get("http://127.0.0.1:9001/numbers/100/test/200"); +} + +TEST_CASE("test radix tree restful api") { + cinatra::coro_http_server server(1, 9001); + + server.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &response) { + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.set_http_handler( + "/user/:id", [](coro_http_request &req, coro_http_response &response) { + CHECK(req.params_["id"] == "cinatra"); + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.set_http_handler( + "/user/:id/subscriptions", + [](coro_http_request &req, coro_http_response &response) { + CHECK(req.params_["id"] == "subid"); + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.set_http_handler( + "/users/:userid/subscriptions/:subid", + [](coro_http_request &req, coro_http_response &response) { + CHECK(req.params_["userid"] == "ultramarines"); + CHECK(req.params_["subid"] == "guilliman"); + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.set_http_handler( + "/values/:x/:y/:z", + [](coro_http_request &req, coro_http_response &response) { + CHECK(req.params_["x"] == "guilliman"); + CHECK(req.params_["y"] == "cawl"); + CHECK(req.params_["z"] == "yvraine"); + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client; + client.get("http://127.0.0.1:9001/user/cinatra"); + client.get("http://127.0.0.1:9001/user/subid/subscriptions"); + client.get("http://127.0.0.1:9001/user/ultramarines/subscriptions/guilliman"); + client.get("http://127.0.0.1:9001/value/guilliman/cawl/yvraine"); +} + +TEST_CASE("test coro radix tree restful api") { + cinatra::coro_http_server server(1, 9001); + + server.set_http_handler( + "/", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.set_http_handler( + "/user/:id", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + CHECK(req.params_["id"] == "cinatra"); + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.set_http_handler( + "/user/:id/subscriptions", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&] { + CHECK(req.params_["id"] == "subid"); + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.set_http_handler( + "/users/:userid/subscriptions/:subid", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&] { + CHECK(req.params_["userid"] == "ultramarines"); + CHECK(req.params_["subid"] == "guilliman"); + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.set_http_handler( + "/values/:x/:y/:z", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&] { + CHECK(req.params_["x"] == "guilliman"); + CHECK(req.params_["y"] == "cawl"); + CHECK(req.params_["z"] == "yvraine"); + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client; + client.get("http://127.0.0.1:9001/user/cinatra"); + client.get("http://127.0.0.1:9001/user/subid/subscriptions"); + client.get("http://127.0.0.1:9001/user/ultramarines/subscriptions/guilliman"); + client.get("http://127.0.0.1:9001/value/guilliman/cawl/yvraine"); +} + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) int main(int argc, char **argv) { return doctest::Context(argc, argv).run(); } DOCTEST_MSVC_SUPPRESS_WARNING_POP \ No newline at end of file