From 53a731caf53a5f1bfb1e1d44466cc3409226fc5f Mon Sep 17 00:00:00 2001 From: Chunting Gu Date: Wed, 11 Sep 2019 15:10:12 +0800 Subject: [PATCH] Extract the route related code to new class Router and add UT. --- unittest/router_unittest.cc | 77 +++++++++++++++++++++++++++ webcc/router.cc | 101 ++++++++++++++++++++++++++++++++++++ webcc/router.h | 51 ++++++++++++++++++ webcc/server.cc | 95 +++------------------------------ webcc/server.h | 35 ++----------- 5 files changed, 240 insertions(+), 119 deletions(-) create mode 100644 unittest/router_unittest.cc create mode 100644 webcc/router.cc create mode 100644 webcc/router.h diff --git a/unittest/router_unittest.cc b/unittest/router_unittest.cc new file mode 100644 index 0000000..bc7dab5 --- /dev/null +++ b/unittest/router_unittest.cc @@ -0,0 +1,77 @@ +#include "gtest/gtest.h" + +#include "webcc/router.h" +#include "webcc/response_builder.h" + +// ----------------------------------------------------------------------------- + +class MyView : public webcc::View { +public: + webcc::ResponsePtr Handle(webcc::RequestPtr request) override { + return webcc::ResponseBuilder{}.OK()(); + } +}; + +// ----------------------------------------------------------------------------- + +TEST(RouterTest, URL_RegexBasic) { + webcc::Router router; + + router.Route(webcc::R("/instance/(\\d+)"), + std::make_shared()); + + std::string url = "/instance/12345"; + webcc::UrlArgs args; + + webcc::ViewPtr view = router.FindView("GET", url, &args); + + EXPECT_TRUE(!!view); + + EXPECT_EQ(1, args.size()); + EXPECT_EQ("12345", args[0]); + + url = "/instance/abcde"; + args.clear(); + view = router.FindView("GET", url, &args); + + EXPECT_TRUE(!view); +} + +TEST(RouterTest, URL_RegexMultiple) { + webcc::Router router; + + router.Route(webcc::R("/study/(\\d+)/series/(\\d+)/instance/(\\d+)"), + std::make_shared()); + + std::string url = "/study/1/series/2/instance/3"; + webcc::UrlArgs args; + + webcc::ViewPtr view = router.FindView("GET", url, &args); + + EXPECT_TRUE(!!view); + + EXPECT_EQ(3, args.size()); + EXPECT_EQ("1", args[0]); + EXPECT_EQ("2", args[1]); + EXPECT_EQ("3", args[2]); + + url = "/study/a/series/b/instance/c"; + args.clear(); + view = router.FindView("GET", url, &args); + + EXPECT_TRUE(!view); +} + +TEST(RouterTest, URL_NonRegex) { + webcc::Router router; + + router.Route("/instances", std::make_shared()); + + std::string url = "/instances"; + webcc::UrlArgs args; + + webcc::ViewPtr view = router.FindView("GET", url, &args); + + EXPECT_TRUE(!!view); + EXPECT_TRUE(args.empty()); +} diff --git a/webcc/router.cc b/webcc/router.cc new file mode 100644 index 0000000..d16caea --- /dev/null +++ b/webcc/router.cc @@ -0,0 +1,101 @@ +#include "webcc/router.h" + +#include + +#include "boost/algorithm/string.hpp" + +#include "webcc/logger.h" + +namespace webcc { + +bool Router::Route(const std::string& url, ViewPtr view, + const Strings& methods) { + assert(view); + + // TODO: More error check + + routes_.push_back({ url, {}, view, methods }); + + return true; +} + +bool Router::Route(const UrlRegex& regex_url, ViewPtr view, + const Strings& methods) { + assert(view); + + // TODO: More error check + + try { + + routes_.push_back({ "", regex_url(), view, methods }); + + } catch (const std::regex_error& e) { + LOG_ERRO("Not a valid regular expression: %s", e.what()); + return false; + } + + return true; +} + +ViewPtr Router::FindView(const std::string& method, const std::string& url, + UrlArgs* args) { + assert(args != nullptr); + + for (auto& route : routes_) { + if (std::find(route.methods.begin(), route.methods.end(), method) == + route.methods.end()) { + continue; + } + + if (route.url.empty()) { + std::smatch match; + + if (std::regex_match(url, match, route.url_regex)) { + // Any sub-matches? + // Start from 1 because match[0] is the whole string itself. + for (size_t i = 1; i < match.size(); ++i) { + args->push_back(match[i].str()); + } + + return route.view; + } + } else { + if (boost::iequals(route.url, url)) { + return route.view; + } + } + } + + return ViewPtr(); +} + +bool Router::MatchView(const std::string& method, const std::string& url, + bool* stream) { + assert(stream != nullptr); + *stream = false; + + for (auto& route : routes_) { + if (std::find(route.methods.begin(), route.methods.end(), method) == + route.methods.end()) { + continue; + } + + if (route.url.empty()) { + std::smatch match; + + if (std::regex_match(url, match, route.url_regex)) { + *stream = route.view->Stream(method); + return true; + } + } else { + if (boost::iequals(route.url, url)) { + *stream = route.view->Stream(method); + return true; + } + } + } + + return false; +} + +} // namespace webcc diff --git a/webcc/router.h b/webcc/router.h new file mode 100644 index 0000000..0efb8ae --- /dev/null +++ b/webcc/router.h @@ -0,0 +1,51 @@ +#ifndef WEBCC_ROUTER_H_ +#define WEBCC_ROUTER_H_ + +#include +#include + +#include "webcc/globals.h" +#include "webcc/view.h" + +namespace webcc { + +class Router { +public: + virtual ~Router() = default; + + // Route a URL to a view. + // The URL should start with "/". E.g., "/instances". + bool Route(const std::string& url, ViewPtr view, + const Strings& methods = { "GET" }); + + // Route a URL (as regular expression) to a view. + // The URL should start with "/" and be a regular expression. + // E.g., "/instances/(\\d+)". + bool Route(const UrlRegex& regex_url, ViewPtr view, + const Strings& methods = { "GET" }); + + // Find the view by HTTP method and URL (path). + ViewPtr FindView(const std::string& method, const std::string& url, + UrlArgs* args); + + // Match the view by HTTP method and URL (path). + // Return if a view is matched or not. + // If the view asks for data streaming, |stream| will be set to true. + bool MatchView(const std::string& method, const std::string& url, + bool* stream); + +private: + struct RouteInfo { + std::string url; + std::regex url_regex; + ViewPtr view; + Strings methods; + }; + + // Route table. + std::vector routes_; +}; + +} // namespace webcc + +#endif // WEBCC_ROUTER_H_ diff --git a/webcc/server.cc b/webcc/server.cc index 2579c7b..730c84a 100644 --- a/webcc/server.cc +++ b/webcc/server.cc @@ -1,10 +1,8 @@ #include "webcc/server.h" -#include #include #include -#include "boost/algorithm/string.hpp" #include "boost/filesystem/fstream.hpp" #include "boost/filesystem/operations.hpp" @@ -12,7 +10,6 @@ #include "webcc/logger.h" #include "webcc/request.h" #include "webcc/response.h" -#include "webcc/url.h" #include "webcc/utility.h" namespace bfs = boost::filesystem; @@ -27,35 +24,6 @@ Server::Server(std::uint16_t port, const Path& doc_root) AddSignals(); } -bool Server::Route(const std::string& url, ViewPtr view, - const Strings& methods) { - assert(view); - - // TODO: More error check - - routes_.push_back({ url, {}, view, methods }); - - return true; -} - -bool Server::Route(const UrlRegex& regex_url, ViewPtr view, - const Strings& methods) { - assert(view); - - // TODO: More error check - - try { - - routes_.push_back({ "", regex_url(), view, methods }); - - } catch (const std::regex_error& e) { - LOG_ERRO("Not a valid regular expression: %s", e.what()); - return false; - } - - return true; -} - void Server::Run(std::size_t workers, std::size_t loops) { assert(workers > 0); @@ -195,7 +163,8 @@ void Server::AsyncAccept() { LOG_INFO("Accepted a connection."); using namespace std::placeholders; - auto view_matcher = std::bind(&Server::MatchView, this, _1, _2, _3); + auto view_matcher = std::bind(&Server::MatchViewOrStatic, this, _1, + _2, _3); auto connection = std::make_shared( std::move(socket), &pool_, &queue_, std::move(view_matcher)); @@ -321,62 +290,10 @@ void Server::Handle(ConnectionPtr connection) { } } -ViewPtr Server::FindView(const std::string& method, const std::string& url, - UrlArgs* args) { - assert(args != nullptr); - - for (auto& route : routes_) { - if (std::find(route.methods.begin(), route.methods.end(), method) == - route.methods.end()) { - continue; - } - - if (route.url.empty()) { - std::smatch match; - - if (std::regex_match(url, match, route.url_regex)) { - // Any sub-matches? - // Start from 1 because match[0] is the whole string itself. - for (size_t i = 1; i < match.size(); ++i) { - args->push_back(match[i].str()); - } - - return route.view; - } - } else { - if (boost::iequals(route.url, url)) { - return route.view; - } - } - } - - return ViewPtr(); -} - -bool Server::MatchView(const std::string& method, const std::string& url, - bool* stream) { - assert(stream != nullptr); - *stream = false; - - for (auto& route : routes_) { - if (std::find(route.methods.begin(), route.methods.end(), method) == - route.methods.end()) { - continue; - } - - if (route.url.empty()) { - std::smatch match; - - if (std::regex_match(url, match, route.url_regex)) { - *stream = route.view->Stream(method); - return true; - } - } else { - if (boost::iequals(route.url, url)) { - *stream = route.view->Stream(method); - return true; - } - } +bool Server::MatchViewOrStatic(const std::string& method, + const std::string& url, bool* stream) { + if (Router::MatchView(method, url, stream)) { + return true; } // Try to match a static file. diff --git a/webcc/server.h b/webcc/server.h index 40ad4ce..e2304be 100644 --- a/webcc/server.h +++ b/webcc/server.h @@ -12,12 +12,12 @@ #include "webcc/connection.h" #include "webcc/connection_pool.h" #include "webcc/queue.h" +#include "webcc/router.h" #include "webcc/url.h" -#include "webcc/view.h" namespace webcc { -class Server { +class Server : public Router { public: explicit Server(std::uint16_t port, const Path& doc_root = {}); @@ -31,17 +31,6 @@ public: file_chunk_size_ = file_chunk_size; } - // Route a URL to a view. - // The URL should start with "/". E.g., "/instances". - bool Route(const std::string& url, ViewPtr view, - const Strings& methods = { "GET" }); - - // Route a URL (as regular expression) to a view. - // The URL should start with "/" and be a regular expression. - // E.g., "/instances/(\\d+)". - bool Route(const UrlRegex& regex_url, ViewPtr view, - const Strings& methods = { "GET" }); - // Start and run the server. // This method is blocking so will not return until Stop() is called (from // another thread) or a signal like SIGINT is caught. @@ -92,28 +81,17 @@ private: // The connection will keep alive if it's a persistent connection. When next // request comes, this connection will be put back to the queue again. virtual void Handle(ConnectionPtr connection); - - // Find the view by HTTP method and URL (path). - ViewPtr FindView(const std::string& method, const std::string& url, - UrlArgs* args); - + // Match the view by HTTP method and URL (path). // Return if a view or static file is matched or not. // If the view asks for data streaming, |stream| will be set to true. - bool MatchView(const std::string& method, const std::string& url, - bool* stream); + bool MatchViewOrStatic(const std::string& method, const std::string& url, + bool* stream); // Serve static files from the doc root. ResponsePtr ServeStatic(RequestPtr request); private: - struct RouteInfo { - std::string url; - std::regex url_regex; - ViewPtr view; - Strings methods; - }; - // Port number. std::uint16_t port_; @@ -147,9 +125,6 @@ private: // The queue with connection waiting for the workers to process. Queue queue_; - - // Route table. - std::vector routes_; }; } // namespace webcc