From a8d24fa8a5006840c952b3cc252fd808e5f01f0e Mon Sep 17 00:00:00 2001 From: kamchatka-volcano Date: Mon, 12 Aug 2024 23:00:56 +0500 Subject: [PATCH] -implemented new route path masks; allow to register route paths type safely with compile time strings on C++20; -removed rx strings, now regexp routes are specified with routeRegex method; --- .github/workflows/build_and_test.yml | 2 +- CMakeLists.txt | 4 +- README.md | 16 +- include/whaleroute/requestprocessor.h | 25 +- include/whaleroute/requestrouter.h | 183 +++++-- include/whaleroute/route.h | 10 +- include/whaleroute/routeparamparser.h | 185 +++++++ include/whaleroute/routeparamtype.h | 50 ++ include/whaleroute/types.h | 36 +- include/whaleroute/utils.h | 30 +- tests/CMakeLists.txt | 1 - tests/test_alt_regex_specifying.cpp | 202 -------- tests/test_multiple_route_matchers.cpp | 2 +- tests/test_router.cpp | 489 +++++++++++++++++- tests/test_router_alt_request_processor.cpp | 38 +- tests/test_router_without_response_value.cpp | 22 +- tests/test_router_without_route_context.cpp | 32 +- tests/test_router_without_route_matchers.cpp | 22 +- ...hout_route_matchers_and_response_value.cpp | 22 +- tests_cpp20/CMakeLists.txt | 16 + 20 files changed, 1012 insertions(+), 375 deletions(-) create mode 100644 include/whaleroute/routeparamparser.h create mode 100644 include/whaleroute/routeparamtype.h delete mode 100644 tests/test_alt_regex_specifying.cpp create mode 100644 tests_cpp20/CMakeLists.txt diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 5695134..b37c0c8 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -53,7 +53,7 @@ jobs: - uses: ilammy/msvc-dev-cmd@v1 - name: Configure CMake - run: cmake -B ${{github.workspace}}/build -DENABLE_TESTS=ON --preset="${{ matrix.config.cmake-preset }}" + run: cmake -B ${{github.workspace}}/build -DENABLE_TESTS=ON -DENABLE_TESTS_CPP20=ON --preset="${{ matrix.config.cmake-preset }}" - name: Build run: cmake --build ${{github.workspace}}/build diff --git a/CMakeLists.txt b/CMakeLists.txt index 11900d6..5471196 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ include(external/seal_lake) SealLake_Bundle( NAME whaleroute_sfun GIT_REPOSITORY https://github.com/kamchatka-volcano/sfun.git - GIT_TAG v5.1.0 + GIT_TAG develop DIRECTORIES include/sfun DESTINATION include/whaleroute/external @@ -18,4 +18,4 @@ SealLake_HeaderOnlyLibrary( COMPILE_FEATURES cxx_std_17 ) -SealLake_OptionalSubProjects(tests) +SealLake_OptionalSubProjects(tests tests_cpp20) diff --git a/README.md b/README.md index 95e8747..eb941d7 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ class Router : public whaleroute::RequestRouter{ }; auto router = Router{}; - router.route(whaleroute::rx{".*"}, Request::Method::GET).process(const Request& request, Response&)){ + router.route(whaleroute::Regex{".*"}, Request::Method::GET).process(const Request& request, Response&)){ log(request); }); router.route("/").process([](const Request&, Response& response)){ @@ -214,7 +214,7 @@ void authorize(const Request& request, Response&, Context& ctx) ctx.isAuthorized = true; } - router.route(whaleroute::rx{".*"}, Request::Method::POST).process(authorize); + router.route(whaleroute::Regex{".*"}, Request::Method::POST).process(authorize); router.route("/").process([](const Request&, Response& response, const Context& ctx)){ if (ctx.isAuthorized) response.send("HTTP/1.1 200 OK\r\n\r\n"); @@ -275,7 +275,7 @@ struct RouteMatcher { The `route` method of the Router can accept a regular expression instead of a string to specify the path of the route: ```c++ -router.route(whaleroute::rx{"/.*"}, Request::Method::GET).set("HTTP/1.1 200 OK\r\n\r\n"); +router.route(whaleroute::Regex{"/.*"}, Request::Method::GET).set("HTTP/1.1 200 OK\r\n\r\n"); ``` Currently, the regular expressions use the standard C++ library with ECMAScript grammar. @@ -288,7 +288,7 @@ void showPage(int pageNumber, const Request&, Response& response) { response.send("page" + std::to_string(pageNumber)); } -router.route(whaleroute::rx{"/page/(\\d+)"}, Request::Method::GET).process(showPage); +router.route(whaleroute::Regex{"/page/(\\d+)"}, Request::Method::GET).process(showPage); ``` The conversion of strings from the capturing groups to the parameters of the request processor is performed using the @@ -312,7 +312,7 @@ void showPage(PageNumber pageNumber, const Request&, Response& response) { response.send("page" + std::to_string(pageNumber.value)); } -router.route(whaleroute::rx{"/page/(\\d+)"}, Request::Method::GET).process(showPage); +router.route(whaleroute::Regex{"/page/(\\d+)"}, Request::Method::GET).process(showPage); ``` When the regular expression of a route is set dynamically, you may need to capture an arbitrary number of parameters. In @@ -327,8 +327,8 @@ void showBook(const RouteParameters<>& bookIds, const Request&, Response& respon if (bookIds.value.size() == 2) response.send("book" + std::to_string(bookIds.value.at(0)) + std::to_string(bookIds.value.at(1))); } -router.route(whaleroute::rx{"/book/(\\d+)/(\\d+)"}, Request::Method::GET).process(showBook); -router.route(whaleroute::rx{"/book/(\\d+)"}, Request::Method::GET).process(showBook); +router.route(whaleroute::Regex{"/book/(\\d+)/(\\d+)"}, Request::Method::GET).process(showBook); +router.route(whaleroute::Regex{"/book/(\\d+)"}, Request::Method::GET).process(showBook); ``` If capturing the string array is more suitable for your request processor, you can use `RouteParameters` with a specific @@ -339,7 +339,7 @@ void showPage(const RouteParameters<1>& pageNumber, const Request&, Response& re { response.send("page" + pageNumber.value().at(0)); } -router.route(whaleroute::rx{"/page/(\\d+)"}, Request::Method::GET).process(showPage); +router.route(whaleroute::Regex{"/page/(\\d+)"}, Request::Method::GET).process(showPage); ``` #### Trailing slash matching diff --git a/include/whaleroute/requestprocessor.h b/include/whaleroute/requestprocessor.h index e6d4fa3..7223b35 100644 --- a/include/whaleroute/requestprocessor.h +++ b/include/whaleroute/requestprocessor.h @@ -139,7 +139,7 @@ auto readRouteParams(const std::vector& routeParams) return params; } else { - if (paramsSize > routeParams.size()) + if (paramsSize != routeParams.size()) return RouteParameterCountMismatch{paramsSize, static_cast(routeParams.size())}; return makeParams(routeParams); @@ -177,6 +177,7 @@ constexpr int getParamsCount() } template< + auto checkParam, typename TResponseConverter, typename TRequestProcessor, typename TRequest, @@ -216,6 +217,28 @@ void invokeRequestProcessor( } } else { + if constexpr (!std::is_same_v) { + + constexpr auto paramsTuple = + sfun::decay_tuple_t())>>{}; + constexpr auto isAnyRouteParameters = std::tuple_size_v == 1 && + std::is_base_of_v::type>; + + if constexpr (!isAnyRouteParameters) { + constexpr auto requestProcessorParams = sfun::type_list{paramsTuple}; + if constexpr (std::is_same_v){ + static_assert( + requestProcessorParams.size() == checkParam, + "Request processor has a mismatched number of route parameters and route's regular expression capture groups."); + } + else { + static_assert( + requestProcessorParams == checkParam, + "Request processor can't be invoked with route parameters"); + } + } + } auto paramsResult = readRouteParams(routeParams); auto paramsResultVisitor = sfun::overloaded{ [&](const RouteParameterError& error) diff --git a/include/whaleroute/requestrouter.h b/include/whaleroute/requestrouter.h index 09a35db..1636724 100755 --- a/include/whaleroute/requestrouter.h +++ b/include/whaleroute/requestrouter.h @@ -4,31 +4,33 @@ #include "irequestrouter.h" #include "requestprocessorqueue.h" #include "route.h" +#include "routeparamparser.h" +#include "routeparamtype.h" #include "types.h" #include "utils.h" #include "external/sfun/functional.h" #include "external/sfun/interface.h" +#include #include +#include #include +#include #include namespace whaleroute { template class RequestRouter : private detail::IRequestRouter { - using Route = detail::Route; using RequestProcessorFunc = std::function&, TRouteContext&)>; struct RegExpRouteMatch { std::regex regExp; - Route route; + std::any route; + std::function&, TRouteContext&)>>()> + getRouteRequestProcessors; }; - struct PathRouteMatch { - std::string path; - Route route; - }; - using RouteMatch = std::variant; public: RequestRouter() @@ -42,28 +44,62 @@ class RequestRouter : private detail::IRequestRouter { } template - Route& route(const std::string& path, TRouteMatcherArgs&&... matcherArgs) + auto& route(const std::string& path, TRouteMatcherArgs&&... matcherArgs) { - return pathRouteImpl(path, {std::forward(matcherArgs)...}); + return dynamicRouteImpl(path, {std::forward(matcherArgs)...}); + //return makeRegexRoute(pathStr, {std::forward(matcherArgs)...}); } - Route& route(const std::string& path) + auto& route(const std::string& path) { - return pathRouteImpl(path, {}); + return dynamicRouteImpl(path); + //return makeRegexRoute(path, {}); } template - Route& route(const rx& regExp, TRouteMatcherArgs&&... matcherArgs) + auto& routeRegex(const std::string& regex, TRouteMatcherArgs&&... matcherArgs) + { + return makeRegexRoute(regex, {std::forward(matcherArgs)...}); + } + + auto& routeRegex(const std::string& regex) + { + return makeRegexRoute(regex, {}); + } + +#if (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) || (!defined(_MSVC_LANG) && __cplusplus >= 202002L) + + template + auto& route() + { + return routeImpl({}); + } + + template + auto& route(TRouteMatcherArgs&&... matcherArgs) { - return regexRouteImpl(regExp, {std::forward(matcherArgs)...}); + return routeImpl({std::forward(matcherArgs)...}); } - Route& route(const rx& regExp) + template + auto& routeRegex() { - return regexRouteImpl(regExp, {}); + constexpr auto captureGroupCount = detail::countRegexCaptureGroups(); + return makeRegexRoute(std::string{regex.str()}, {}); } - Route& route() + template + auto& routeRegex(TRouteMatcherArgs&&... matcherArgs) + { + constexpr auto captureGroupCount = detail::countRegexCaptureGroups(); + return makeRegexRoute( + std::string{regex.str()}, + {std::forward(matcherArgs)...}); + } + +#endif + + auto& route() { return noMatchRoute_; } @@ -97,6 +133,68 @@ class RequestRouter : private detail::IRequestRouter { } private: +#if (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) || (!defined(_MSVC_LANG) && __cplusplus >= 202002L) + template + auto& routeImpl(std::vector> routeMatchers = {}) + { + constexpr auto paramIds = detail::readPathParamIds(); + constexpr auto paramTraits = detail::paramIdsToParamTraitList(); + auto pathStr = detail::prepareRegexString(path.str()); + paramTraits.for_each( + [&](auto paramTraitTypeId) + { + using Param = typename decltype(paramTraitTypeId)::type; + static_assert( + sfun::is_complete_type_v, + "Trying to use an unregistered parameter name in route"); + pathStr = sfun::replace( + pathStr, + "\\{" + std::string{Param::name} + "\\}", + "(" + std::string{Param::regex} + ")"); + }); + + constexpr auto paramTypes = detail::paramIdsToParamTypeList(); + return makeRegexRoute(pathStr, std::move(routeMatchers)); + } +#endif + + auto& dynamicRouteImpl( + const std::string& path, + std::vector> routeMatchers = {}) + { + auto params = detail::readPathParams(path); + auto pathStr = detail::prepareRegexString(path); + for (const auto& param : params) { + auto paramRegex = routeParamRegexImpl(param); + if (!paramRegex.has_value()) + throw std::runtime_error{ + "Regular expression for route parameter '" + param + + "' isn't registered. Override routeParamRegex() method to add it."}; + + pathStr = sfun::replace(pathStr, "\\{" + param + "\\}", "(" + std::string{paramRegex.value()} + ")"); + } + return makeRegexRoute(pathStr, std::move(routeMatchers)); + } + + std::optional routeParamRegexImpl(std::string_view paramName) const + { + static const auto routeRegexMap = std::unordered_map{ + {"int", R"(\d+)"}, + {"str", R"([\w\$-\.\+!*'\(\)]+)"}, + {"path", R"([\w\$-\.\+!*'\(\)/]+)"}}; + + auto it = routeRegexMap.find(std::string{paramName}); + if (it != routeRegexMap.end()) + return it->second; + + return routeParamRegex(paramName); + } + + virtual std::optional routeParamRegex(std::string_view paramName) const + { + return std::nullopt; + } + virtual bool isRouteProcessingFinished(const TRequest&, TResponse&) const { return true; @@ -127,7 +225,7 @@ class RequestRouter : private detail::IRequestRouter { auto [result, routeParams] = detail::matchRegex(requestPath, match.regExp); if (result) return makeRequestProcessorInvokerList( - match.route.getRequestProcessors(), + match.getRouteRequestProcessors(), request, response, routeParams); @@ -137,7 +235,7 @@ class RequestRouter : private detail::IRequestRouter { detail::matchRegex(getAlternativeTrailingSlashPath(requestPath), match.regExp); if (retryResult) return makeRequestProcessorInvokerList( - match.route.getRequestProcessors(), + match.getRouteRequestProcessors(), request, response, retryRouteParams); @@ -146,26 +244,13 @@ class RequestRouter : private detail::IRequestRouter { }; } - auto makePathMatchProcessor(const TRequest& request, TResponse& response) - { - return [&](const PathRouteMatch& match) -> std::vector> - { - if (match.path == detail::makePath(this->getRequestPath(request), trailingSlashMode_)) - return makeRequestProcessorInvokerList(match.route.getRequestProcessors(), request, response, {}); - else - return {}; - }; - } - std::vector> makeRouteRequestProcessorInvokerList( const TRequest& request, TResponse& response) { auto result = std::vector>{}; - const auto matchVisitor = - sfun::overloaded{makeRegexMatchProcessor(request, response), makePathMatchProcessor(request, response)}; for (auto& match : routeMatchList_) - detail::concat(result, std::visit(matchVisitor, match)); + detail::concat(result, makeRegexMatchProcessor(request, response)(match)); return result; } @@ -193,29 +278,27 @@ class RequestRouter : private detail::IRequestRouter { return result; }; - Route& pathRouteImpl( - const std::string& path, + template + auto& makeRegexRoute( + const std::string& regex, std::vector> routeMatchers = {}) { - auto routePath = detail::makePath(path, trailingSlashMode_); - auto& routeMatch = routeMatchList_.emplace_back( - PathRouteMatch{routePath, Route{routeMatchers, routeParametersErrorHandler()}}); - return std::get(routeMatch).route; - } - - Route& regexRouteImpl( - const rx& regExp, - std::vector> routeMatchers = {}) - { - auto& routeMatch = routeMatchList_.emplace_back(RegExpRouteMatch{ - detail::makeRegex(regExp, trailingSlashMode_), - {std::move(routeMatchers), routeParametersErrorHandler()}}); - return std::get(routeMatch).route; + using RouteType = detail::Route; + auto routeMatch = RegExpRouteMatch{}; + routeMatch.regExp = std::regex{regex}; + routeMatch.route = RouteType{std::move(routeMatchers), routeParametersErrorHandler()}; + routeMatchList_.emplace_back(routeMatch); + routeMatchList_.back().getRouteRequestProcessors = + [&route = std::any_cast(routeMatchList_.back().route)]() + { + return route.getRequestProcessors(); + }; + return std::any_cast(routeMatchList_.back().route); } private: - std::deque routeMatchList_; - Route noMatchRoute_; + std::deque routeMatchList_; + detail::Route noMatchRoute_; TrailingSlashMode trailingSlashMode_ = TrailingSlashMode::Optional; }; diff --git a/include/whaleroute/route.h b/include/whaleroute/route.h index 725a4c9..1d199d2 100755 --- a/include/whaleroute/route.h +++ b/include/whaleroute/route.h @@ -19,7 +19,7 @@ class RequestRouter; namespace whaleroute::detail { -template +template class Route { using ProcessorFunc = std::function&, TRouteContext&)>; @@ -46,7 +46,7 @@ class Route { const std::vector& routeParams, TRouteContext& routeContext) mutable { - invokeRequestProcessor( + invokeRequestProcessor( requestProcessor, request, response, @@ -64,7 +64,7 @@ class Route { const std::vector& routeParams, TRouteContext& routeContext) { - invokeRequestProcessor( + invokeRequestProcessor( *requestProcessor, request, response, @@ -87,7 +87,7 @@ class Route { const std::vector& routeParams, TRouteContext& routeContext) { - invokeRequestProcessor( + invokeRequestProcessor( requestProcessor, request, response, @@ -104,7 +104,7 @@ class Route { const std::vector& routeParams, TRouteContext& routeContext) { - invokeRequestProcessor( + invokeRequestProcessor( requestProcessor, request, response, diff --git a/include/whaleroute/routeparamparser.h b/include/whaleroute/routeparamparser.h new file mode 100644 index 0000000..866c289 --- /dev/null +++ b/include/whaleroute/routeparamparser.h @@ -0,0 +1,185 @@ +#ifndef WHALEROUTE_ROUTEPARAMPARSER_H +#define WHALEROUTE_ROUTEPARAMPARSER_H +#include "routeparamtype.h" +#include "external/sfun/type_list.h" +#include +#include +#include +#include + +namespace whaleroute::detail{ + +inline std::vector readPathParams(std::string_view s) +{ + auto params = std::vector{}; + std::size_t i = 0; + int openBraceCount = 0; + int paramPos = 0; + while (i < s.size()) { + if (s[i] == '{') { + openBraceCount++; + paramPos = i + 1; + } + if (s[i] == '}') { + openBraceCount--; + params.emplace_back(std::next(s.begin(), paramPos), std::next(s.begin(), i)); + } + if (openBraceCount > 1) + return {}; + i++; + } + if (openBraceCount != 0) + return {}; + + return params; +} + +#if (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) || (!defined(_MSVC_LANG) && __cplusplus >= 202002L) + +template +constexpr auto paramIdListToParamTraitTupleImpl(std::index_sequence) { + return std::tuple>...>{}; +} + +template +constexpr auto paramIdListToParamTraitTuple() { + return paramIdListToParamTraitTupleImpl(std::make_index_sequence{}); +} + +template +constexpr auto paramIdListToParamTypeTupleImpl(std::index_sequence) { + return std::tuple>...>{}; +} + +template +constexpr auto paramIdListToParamTypeTuple() { + return paramIdListToParamTypeTupleImpl(std::make_index_sequence{}); +} + +template +constexpr auto forEachIdImpl(std::index_sequence, TFunc&& func) +{ + (func(idArray[Is]), ...); +} + +template +constexpr auto forEachId(TFunc&& func) +{ + forEachIdImpl(std::make_index_sequence{}, func); +} + +template +struct static_vector { + using type = T; + + constexpr auto operator[](std::size_t i) -> T& { + return raw[i]; + } + + constexpr auto operator[](std::size_t i) const -> T const& { + return raw[i]; + } + + constexpr void emplace_back(auto&& expr) { + raw[count] = static_cast(expr); + count += 1; + } + + constexpr auto size() const -> std::size_t { + return count; + } + + constexpr auto data() const -> const T* { + return raw.data(); + } + + std::array raw{}; + std::size_t count = 0; +}; + +template +consteval auto makeArrayFromStaticVector() +{ + auto result = std::array{}; + for (auto i = 0; i < result.size(); ++i) + result[i] = staticVector.raw[i]; + return result; +} + +constexpr auto countRegexCaptureGroups(std::string_view s) +{ + std::size_t i = 0; + int openBraceCount = 0; + int groupCount = 0; + while (i < s.size()) { + if (s[i] == '(') { + openBraceCount++; + groupCount++; + } + if (s[i] == ')') { + openBraceCount--; + } + if (openBraceCount > 1) + return -1; + i++; + } + if (openBraceCount != 0) + return -1; + + return groupCount; +}; + +constexpr auto readPathParamIds(std::string_view s) +{ + auto params = static_vector{}; + std::size_t i = 0; + int openBraceCount = 0; + int paramPos = 0; + while (i < s.size()) { + if (s[i] == '{') { + openBraceCount++; + paramPos = i + 1; + } + if (s[i] == '}') { + openBraceCount--; + params.emplace_back(fnv1a(std::string_view{std::next(s.begin(), paramPos), std::next(s.begin(), i)})); + } + if (openBraceCount > 1) + return static_vector{}; + i++; + } + if (openBraceCount != 0) + return static_vector{}; + + return params; +} + +template +constexpr auto readPathParamIds() +{ + return makeArrayFromStaticVector(); +} + +template +constexpr auto countRegexCaptureGroups() +{ + return countRegexCaptureGroups(path.str()); +} + +template +constexpr auto paramIdsToParamTraitList() +{ + return sfun::type_list{paramIdListToParamTraitTuple()}; +} + +template +constexpr auto paramIdsToParamTypeList() +{ + return sfun::type_list{paramIdListToParamTypeTuple()}; +} + +#endif + +} + +#endif //WHALEROUTE_ROUTEPARAMPARSER_H diff --git a/include/whaleroute/routeparamtype.h b/include/whaleroute/routeparamtype.h new file mode 100644 index 0000000..d1a66b0 --- /dev/null +++ b/include/whaleroute/routeparamtype.h @@ -0,0 +1,50 @@ +#ifndef WHALEROUTE_ROUTEPARAMTYPE_H +#define WHALEROUTE_ROUTEPARAMTYPE_H +#include "utils.h" +#include +#include + +namespace whaleroute { + +constexpr std::uint32_t routeParamId(std::string_view paramTypeName) +{ + return detail::fnv1a(paramTypeName); +} + +template +struct RouteParam; + +template<> +struct RouteParam +{ + using type = int; + inline static std::string_view name = "int"; + inline static std::string_view regex = R"(\d+)"; +}; + +template<> +struct RouteParam +{ + using type = std::string; + inline static std::string_view name = "str"; + inline static std::string_view regex = R"([\w\$-\.\+!*'\(\)]+)"; +}; + +template<> +struct RouteParam +{ + using type = std::string; + inline static std::string_view name = "path"; + inline static std::string_view regex = R"([\w\$-\.\+!*'\(\)/]+)"; +}; + + +namespace detail { +template +using RouteParamType = typename RouteParam::type; + +} + +} // namespace whaleroute + +#endif //WHALEROUTE_ROUTEPARAMTYPE_H diff --git a/include/whaleroute/types.h b/include/whaleroute/types.h index a1cddc6..612c474 100755 --- a/include/whaleroute/types.h +++ b/include/whaleroute/types.h @@ -1,6 +1,8 @@ #ifndef WHALEROUTE_TYPES_H #define WHALEROUTE_TYPES_H +#include +#include #include #include #include @@ -8,6 +10,30 @@ namespace whaleroute { +#if (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) || (!defined(_MSVC_LANG) && __cplusplus >= 202002L) +namespace detail { + +template +struct StaticString { + char data[bytesCount]; + constexpr size_t size() const + { + return bytesCount - 1; + } + constexpr std::string_view str() const + { + return {data, size()}; + } + constexpr StaticString(const char (&init)[bytesCount]) + { + std::copy_n(init, bytesCount, data); + } +}; + +} //namespace detail + +#endif + struct _ {}; inline bool operator==(const _&, const _&) @@ -20,9 +46,6 @@ enum class TrailingSlashMode { Strict }; -struct rx { - std::string value; -}; namespace detail { struct RouteParameters { @@ -45,13 +68,6 @@ struct RouteParameterReadError { }; using RouteParameterError = std::variant; -namespace string_literals { -inline rx operator""_rx(const char* args, std::size_t size) -{ - return {std::string(args, size)}; -} -} // namespace string_literals - } // namespace whaleroute #endif // WHALEROUTE_TYPES_H diff --git a/include/whaleroute/utils.h b/include/whaleroute/utils.h index 6337c8c..6e8a37d 100644 --- a/include/whaleroute/utils.h +++ b/include/whaleroute/utils.h @@ -5,6 +5,7 @@ #include "types.h" #include "external/sfun/string_utils.h" #include +#include #include #include #include @@ -43,9 +44,9 @@ inline std::string makePath(const std::string& path, TrailingSlashMode mode) return path; } -inline std::regex makeRegex(const rx& regExp, TrailingSlashMode) +inline std::regex makeRegex(std::string_view regExp, TrailingSlashMode) { - return std::regex{regExp.value}; + return std::regex{std::string{regExp}}; } inline std::tuple> matchRegex(const std::string& path, const std::regex& regExp) @@ -60,6 +61,31 @@ inline std::tuple> matchRegex(const std::string& return {true, std::move(routeParams)}; }; +constexpr std::uint32_t fnv1a(std::string_view data) +{ + const auto fnvPrime = std::uint32_t{0x01000193}; + auto hash = std::uint32_t{0x811c9dc5}; + + for (auto ch : data) { + hash ^= static_cast(ch); + hash *= fnvPrime; + } + return hash; +} + +inline std::string prepareRegexString(std::string_view input) +{ + static const auto specialChars = std::string{R"(\.^$+()[]{}|?*)"}; + auto result = std::string{}; + result.reserve(input.size()); + for (auto ch : input) { + if (specialChars.find(ch) != std::string::npos) + result.push_back('\\'); + result.push_back(ch); + } + return result; +} + } // namespace whaleroute::detail #endif // WHALEROUTE_UTILS_H \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8de8c78..58f91b8 100755 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,7 +4,6 @@ project(test_whaleroute) SealLake_GoogleTest( SOURCES test_router.cpp - test_alt_regex_specifying.cpp test_response_value_arguments.cpp test_multiple_route_matchers.cpp test_router_without_route_context.cpp diff --git a/tests/test_alt_regex_specifying.cpp b/tests/test_alt_regex_specifying.cpp deleted file mode 100644 index 956088c..0000000 --- a/tests/test_alt_regex_specifying.cpp +++ /dev/null @@ -1,202 +0,0 @@ -#include "common.h" -#include -#include -#include - -struct ChapterString { - std::string value; -}; - -namespace whaleroute::config { -template<> -struct RouteMatcher { - bool operator()(RequestType value, const Request& request) const - { - return value == request.type; - } -}; - -template<> -struct StringConverter { - static std::optional fromString(const std::string& data) - { - return ChapterString{data}; - } -}; -} // namespace whaleroute::config - -namespace { - -struct ResponseSender { - void operator()(Response& response, const std::string& data) - { - response.send(data); - } -}; - -class AltRegexSpecifying : public ::testing::Test, - public whaleroute::RequestRouter { -public: - void processRequest(const std::string& path, RequestType requestType = RequestType::GET, std::string name = {}) - { - auto response = Response{}; - response.init(); - process(Request{requestType, path, std::move(name)}, response); - responseData_ = response.state->data; - } - - void checkResponse(const std::string& expectedResponseData) - { - EXPECT_EQ(responseData_, expectedResponseData); - } - -protected: - std::string getRequestPath(const Request& request) final - { - return request.requestPath; - } - - void processUnmatchedRequest(const Request&, Response& response) final - { - response.send("NO_MATCH"); - } - - bool isRouteProcessingFinished(const Request&, Response& response) const final - { - return response.state->wasSent; - } - - void onRouteParametersError(const Request&, Response& response, const whaleroute::RouteParameterError& error) - override - { - response.send(getRouteParamErrorInfo(error)); - } - -protected: - std::string responseData_; -}; - -class GreeterPageProcessor { -public: - GreeterPageProcessor(std::string name = {}) - : name_(std::move(name)) - { - } - - void operator()(const Request&, Response& response) - { - response.send("Hello " + (name_.empty() ? "world" : name_)); - } - -private: - std::string name_; -}; - -class ChapterNamePageIndexProcessor { -public: - ChapterNamePageIndexProcessor(std::string title = {}) - : title_{std::move(title)} - { - } - - void operator()(const std::string& chapterName, const int& pageIndex, const Request&, Response& response) - { - response.send( - title_ + (title_.empty() ? "" : " ") + "Chapter: " + chapterName + ", page[" + - std::to_string(pageIndex) + "]"); - } - -private: - std::string title_; -}; - -struct ChapterNameProcessor { - void operator()(const ChapterString& chapterName, const Request&, Response& response) - { - response.send("Chapter: " + chapterName.value); - } -}; - -} // namespace - -TEST_F(AltRegexSpecifying, StringLiterals) -{ - using namespace whaleroute::string_literals; - - route("/", RequestType::GET).process(); - route("/moon", RequestType::GET).process("Moon"); - route("/page0", RequestType::GET) - .process( - [](const Request&, Response& response) - { - response.send("Default page"); - }); - route(R"(/page\d*)"_rx, RequestType::GET) - .process( - [](const Request&, Response& response) - { - response.send("Some page"); - }); - route("/upload", RequestType::POST) - .process( - [](const Request&, Response& response) - { - response.send("OK"); - }); - route(R"(/chapter/(.+)/page(\d+)/)"_rx, RequestType::GET).process(); - route(R"(/chapter_(.+)/page_(\d+)/)"_rx, RequestType::GET).process("TestBook"); - auto parametrizedProcessor = ChapterNameProcessor{}; - route(R"(/chapter_(.+)/)"_rx, RequestType::GET).process(parametrizedProcessor); - route("/param_error").process(parametrizedProcessor); - route(R"(/files/(.*\.xml))"_rx, RequestType::GET) - .process( - [](const std::string& fileName, const Request&, Response& response) - { - auto fileContent = std::string{"XML file: " + fileName}; - response.send(fileContent); - }); - route().set("404"); - - processRequest("/"); - checkResponse("Hello world"); - processRequest("/", RequestType::POST); - checkResponse("404"); - - processRequest("/moon"); - checkResponse("Hello Moon"); - - processRequest("/upload", RequestType::POST); - checkResponse("OK"); - processRequest("/upload", RequestType::GET); - checkResponse("404"); - - processRequest("/page123"); - checkResponse("Some page"); - - processRequest("/page0"); - checkResponse("Default page"); - - processRequest("/chapter/test/page123"); - checkResponse("Chapter: test, page[123]"); - - processRequest("/chapter_test/page_123"); - checkResponse("TestBook Chapter: test, page[123]"); - - processRequest("/chapter_test"); - checkResponse("Chapter: test"); - - processRequest("/chapter_test/"); - checkResponse("Chapter: test"); - - processRequest("/param_error/"); - checkResponse("ROUTE_PARAM_ERROR: PARAM COUNT MISMATCH, EXPECTED:1 ACTUAL:0"); - - processRequest("/files/test.xml"); - checkResponse("XML file: test.xml"); - - processRequest("/files/test.xml1"); - checkResponse("404"); - - processRequest("/foo"); - checkResponse("404"); -} diff --git a/tests/test_multiple_route_matchers.cpp b/tests/test_multiple_route_matchers.cpp index d7d97c7..d6434e0 100644 --- a/tests/test_multiple_route_matchers.cpp +++ b/tests/test_multiple_route_matchers.cpp @@ -163,7 +163,7 @@ TEST_F(MultipleRouteMatchersWithContext, Default) TEST_F(MultipleRouteMatchersWithContext, ContextMatching) { - route(whaleroute::rx{".+"}) + routeRegex(".+") .process( [](const Request&, Response&, Context& ctx) { diff --git a/tests/test_router.cpp b/tests/test_router.cpp index 75f762c..6f9cf02 100755 --- a/tests/test_router.cpp +++ b/tests/test_router.cpp @@ -2,6 +2,7 @@ #include #include #include +#include struct ChapterString { std::string value; @@ -81,6 +82,59 @@ class Router : public ::testing::Test, std::string responseData_; }; +class RouterWithRouteParams : public ::testing::Test, + public whaleroute::RequestRouter { +public: + void processRequest(const std::string& path, RequestType requestType = RequestType::GET, std::string name = {}) + { + auto response = Response{}; + response.init(); + const auto request = Request{requestType, path, std::move(name)}; + process(request, response); + responseData_ = response.state->data; + } + + void checkResponse(const std::string& expectedResponseData) + { + EXPECT_EQ(responseData_, expectedResponseData); + } + + void onRouteParametersError(const Request&, Response& response, const whaleroute::RouteParameterError& error) + override + { + response.send(getRouteParamErrorInfo(error)); + } + +protected: + std::string getRequestPath(const Request& request) final + { + return request.requestPath; + } + + void processUnmatchedRequest(const Request&, Response& response) final + { + response.send("NO_MATCH"); + } + + bool isRouteProcessingFinished(const Request&, Response& response) const final + { + return response.state->wasSent; + } + + std::optional routeParamRegex(std::string_view paramName) const final + { + auto it = routeParamRegex_.find(std::string{paramName}); + if (it == routeParamRegex_.end()) + return std::nullopt; + return it->second; + } + +protected: + std::string responseData_; + std::unordered_map routeParamRegex_ = { + {"chapter_str", R"(\w+)"}}; +}; + class GreeterPageProcessor { public: GreeterPageProcessor(std::string name = {}) @@ -191,11 +245,265 @@ struct SendContext { } // namespace -using namespace whaleroute::string_literals; TEST_F(Router, Matching) { - route(whaleroute::rx{".+"}).process(); + routeRegex({".+"}).process(); + route("/", RequestType::GET).process(); + route("/moon", RequestType::GET).process("Moon"); + route("/page0", RequestType::GET) + .process( + [](const Request&, Response& response) + { + response.send("Default page"); + }); + routeRegex({R"(/page\d*)"}, RequestType::GET) + .process( + [](const Request&, Response& response) + { + response.send("Some page"); + }); + route("/upload", RequestType::POST) + .process( + [](const Request&, Response& response) + { + response.send("OK"); + }); + routeRegex({R"(/chapter/(.+)/page(\d+)/)"}, RequestType::GET).process(); + routeRegex({R"(/chapter_(.+)/page_(\d+)/)"}, RequestType::GET) + .process("TestBook"); + routeRegex({R"(/book-(.+)/chapter/(.+)/page/(\d+)/)"}, RequestType::GET).process(); + routeRegex({R"(/book-(.+)/chapter/(.+)/)"}, RequestType::GET).process(); + routeRegex({R"(/book/(\w+))"}, RequestType::GET).process(); + routeRegex({R"(/book/(\w+)/(\w+)/)"}, RequestType::GET).process(); + routeRegex({R"(/no_capture_groups)"}, RequestType::GET).process(); + route("/no_capture_groups2", RequestType::GET).process(); + auto parametrizedProcessor = ChapterNameProcessor{}; + routeRegex({R"(/chapter_(.+)/)"}, RequestType::GET).process(parametrizedProcessor); + route("/param_error").process(parametrizedProcessor); + routeRegex({R"(/files/(.*\.xml))"}, RequestType::GET) + .process( + [](const std::string& fileName, const Request&, Response& response) + { + auto fileContent = std::string{"XML file: " + fileName}; + response.send(fileContent); + }); + route("/context", RequestType::GET) + .process( + [](const Request&, Response& response, Context& context) + { + response.send(std::to_string(context.counter)); + }); + route("/context2", RequestType::GET).process(); + + routeRegex({"/(.+)/context"}, RequestType::GET) + .process( + [](const std::string& title, const Request&, Response& response, Context& context) + { + response.send(title + ": " + std::to_string(context.counter)); + }); + + route().set("404"); + + processRequest("/"); + checkResponse("Hello world"); + processRequest("/", RequestType::POST); + checkResponse("404"); + + processRequest("/moon"); + checkResponse("Hello Moon"); + + processRequest("/upload", RequestType::POST); + checkResponse("OK"); + processRequest("/upload", RequestType::GET); + checkResponse("404"); + + processRequest("/page123"); + checkResponse("Some page"); + + processRequest("/page0"); + checkResponse("Default page"); + + processRequest("/chapter/test/page123"); + checkResponse("Chapter: test, page[123]"); + + processRequest("/chapter_test/page_123"); + checkResponse("TestBook Chapter: test, page[123]"); + + processRequest("/book-Hello_world/chapter/test/page/123"); + checkResponse("Book: Hello_world, Chapter: test, page[123]"); + + processRequest("/book-Hello_world/chapter/test/"); + checkResponse("ROUTE_PARAM_ERROR: PARAM COUNT MISMATCH, EXPECTED:3 ACTUAL:2"); + + processRequest("/book/Hello/"); + checkResponse("Book: Hello"); + + processRequest("/book/Hello/world/"); + checkResponse("Book: Hello#world"); + + processRequest("/no_capture_groups"); + checkResponse("Book: "); + processRequest("/no_capture_groups2"); + checkResponse("Book: "); + + processRequest("/chapter_test"); + checkResponse("Chapter: test"); + + processRequest("/chapter_test/"); + checkResponse("Chapter: test"); + + processRequest("/param_error/"); + checkResponse("ROUTE_PARAM_ERROR: PARAM COUNT MISMATCH, EXPECTED:1 ACTUAL:0"); + + processRequest("/files/test.xml"); + checkResponse("XML file: test.xml"); + + processRequest("/files/test.xml1"); + checkResponse("404"); + + processRequest("/foo"); + checkResponse("404"); + + processRequest("/context"); + checkResponse("1"); + + processRequest("/context2"); + checkResponse("1"); + + processRequest("/test/context"); + checkResponse("test: 1"); +} + +TEST_F(RouterWithRouteParams, MatchingWithRouteParams) +{ + routeRegex({".+"}).process(); + route("/", RequestType::GET).process(); + route("/moon", RequestType::GET).process("Moon"); + route("/page0", RequestType::GET) + .process( + [](const Request&, Response& response) + { + response.send("Default page"); + }); + routeRegex({R"(/page\d*)"}, RequestType::GET) + .process( + [](const Request&, Response& response) + { + response.send("Some page"); + }); + route("/upload", RequestType::POST) + .process( + [](const Request&, Response& response) + { + response.send("OK"); + }); + route("/chapter/{str}/page{int}/", RequestType::GET).process(); + route("/chapter_{str}/page_{int}/", RequestType::GET).process("TestBook"); + route("/book-{str}/chapter/{str}/page/{int}", RequestType::GET).process(); + route("/book-{str}/chapter/{str}/", RequestType::GET).process(); + route("/book/{str}/", RequestType::GET).process(); + route("/book/{str}/{str}/", RequestType::GET).process(); + route("/no_capture_groups", RequestType::GET).process(); + route("/no_capture_groups2", RequestType::GET).process(); + auto parametrizedProcessor = ChapterNameProcessor{}; + route("/chapter_{chapter_str}/", RequestType::GET).process(parametrizedProcessor); + route("/files/{str}.xml", RequestType::GET) + .process( + [](const std::string& fileName, const Request&, Response& response) + { + auto fileContent = std::string{"XML file: " + fileName}; + response.send(fileContent); + }); + route("/context", RequestType::GET) + .process( + [](const Request&, Response& response, Context& context) + { + response.send(std::to_string(context.counter)); + }); + route("/context2", RequestType::GET).process(); + + route("/{str}/context", RequestType::GET) + .process( + [](const std::string& title, const Request&, Response& response, Context& context) + { + response.send(title + ": " + std::to_string(context.counter)); + }); + + route().set("404"); + + processRequest("/"); + checkResponse("Hello world"); + processRequest("/", RequestType::POST); + checkResponse("404"); + + processRequest("/moon"); + checkResponse("Hello Moon"); + + processRequest("/upload", RequestType::POST); + checkResponse("OK"); + processRequest("/upload", RequestType::GET); + checkResponse("404"); + + processRequest("/page123"); + checkResponse("Some page"); + + processRequest("/page0"); + checkResponse("Default page"); + + processRequest("/chapter/test/page123"); + checkResponse("Chapter: test, page[123]"); + + processRequest("/chapter_test/page_123"); + checkResponse("TestBook Chapter: test, page[123]"); + + processRequest("/book-Hello_world/chapter/test/page/123"); + checkResponse("Book: Hello_world, Chapter: test, page[123]"); + + processRequest("/book-Hello_world/chapter/test/"); + checkResponse("ROUTE_PARAM_ERROR: PARAM COUNT MISMATCH, EXPECTED:3 ACTUAL:2"); + + processRequest("/book/Hello/"); + checkResponse("Book: Hello"); + + processRequest("/book/Hello/world/"); + checkResponse("Book: Hello#world"); + + processRequest("/no_capture_groups"); + checkResponse("Book: "); + processRequest("/no_capture_groups2"); + checkResponse("Book: "); + + processRequest("/chapter_test"); + checkResponse("Chapter: test"); + + processRequest("/chapter_test/"); + checkResponse("Chapter: test"); + + processRequest("/files/test.xml"); + checkResponse("XML file: test"); + + processRequest("/files/test.xml1"); + checkResponse("404"); + + processRequest("/foo"); + checkResponse("404"); + + processRequest("/context"); + checkResponse("1"); + + processRequest("/context2"); + checkResponse("1"); + + processRequest("/test/context"); + checkResponse("test: 1"); +} + +#if (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) || (!defined(_MSVC_LANG) && __cplusplus >= 202002L) + +TEST_F(Router, MatchingRegex_CPP20) +{ + routeRegex<".+">().process(); route("/", RequestType::GET).process(); route("/moon", RequestType::GET).process("Moon"); route("/page0", RequestType::GET) @@ -204,7 +512,7 @@ TEST_F(Router, Matching) { response.send("Default page"); }); - route(whaleroute::rx{R"(/page\d*)"}, RequestType::GET) + routeRegex(RequestType::GET) .process( [](const Request&, Response& response) { @@ -216,19 +524,19 @@ TEST_F(Router, Matching) { response.send("OK"); }); - route(whaleroute::rx{R"(/chapter/(.+)/page(\d+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/chapter_(.+)/page_(\d+)/)"}, RequestType::GET) + routeRegex(RequestType::GET).process(); + routeRegex(RequestType::GET) .process("TestBook"); - route(whaleroute::rx{R"(/book-(.+)/chapter/(.+)/page/(\d+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/book-(.+)/chapter/(.+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/book/(\w+))"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/book/(\w+)/(\w+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/no_capture_groups)"}, RequestType::GET).process(); + routeRegex(RequestType::GET).process(); + routeRegex(RequestType::GET).process(); + routeRegex(RequestType::GET).process(); + routeRegex(RequestType::GET).process(); + routeRegex(RequestType::GET).process(); route("/no_capture_groups2", RequestType::GET).process(); auto parametrizedProcessor = ChapterNameProcessor{}; - route(whaleroute::rx{R"(/chapter_(.+)/)"}, RequestType::GET).process(parametrizedProcessor); + routeRegex(RequestType::GET).process(parametrizedProcessor); route("/param_error").process(parametrizedProcessor); - route(whaleroute::rx{R"(/files/(.*\.xml))"}, RequestType::GET) + routeRegex(RequestType::GET) .process( [](const std::string& fileName, const Request&, Response& response) { @@ -243,7 +551,7 @@ TEST_F(Router, Matching) }); route("/context2", RequestType::GET).process(); - route(whaleroute::rx{"/(.+)/context"}, RequestType::GET) + routeRegex<"/(.+)/context">(RequestType::GET) .process( [](const std::string& title, const Request&, Response& response, Context& context) { @@ -322,16 +630,153 @@ TEST_F(Router, Matching) checkResponse("test: 1"); } +namespace whaleroute { +template<> +struct RouteParam +{ + using type = ChapterString; + inline static std::string_view name = "chapter_str"; + inline static std::string_view regex = R"(\w+)"; +}; +} + + +TEST_F(Router, Matching_Path_CPP20) +{ + route<"{path}">().process(); + route<"/">(RequestType::GET).process(); + route<"/moon">(RequestType::GET).process("Moon"); + route<"/page0">(RequestType::GET) + .process( + [](const Request&, Response& response) + { + response.send("Default page"); + }); + routeRegex({R"(/page\d*)"}, RequestType::GET) + .process( + [](const Request&, Response& response) + { + response.send("Some page"); + }); + route<"/upload">(RequestType::POST) + .process( + [](const Request&, Response& response) + { + response.send("OK"); + }); + route<"/chapter/{str}/page{int}/">(RequestType::GET).process(); + route<"/chapter_{str}/page_{int}/">(RequestType::GET).process("TestBook"); + route<"/book-{str}/chapter/{str}/page/{int}">(RequestType::GET).process(); + route<"/book-{str}/chapter/{str}/">(RequestType::GET).process(); + route<"/book/{str}/">(RequestType::GET).process(); + route<"/book/{str}/{str}/">(RequestType::GET).process(); + route<"/no_capture_groups">(RequestType::GET).process(); + route<"/no_capture_groups2">(RequestType::GET).process(); + auto parametrizedProcessor = ChapterNameProcessor{}; + route<"/chapter_{chapter_str}/">(RequestType::GET).process(parametrizedProcessor); + route<"/files/{str}.xml">(RequestType::GET) + .process( + [](const std::string& fileName, const Request&, Response& response) + { + auto fileContent = std::string{"XML file: " + fileName}; + response.send(fileContent); + }); + route<"/context">(RequestType::GET) + .process( + [](const Request&, Response& response, Context& context) + { + response.send(std::to_string(context.counter)); + }); + route<"/context2">(RequestType::GET).process(); + + route<"/{str}/context">(RequestType::GET) + .process( + [](const std::string& title, const Request&, Response& response, Context& context) + { + response.send(title + ": " + std::to_string(context.counter)); + }); + + route().set("404"); + + processRequest("/"); + checkResponse("Hello world"); + processRequest("/", RequestType::POST); + checkResponse("404"); + + processRequest("/moon"); + checkResponse("Hello Moon"); + + processRequest("/upload", RequestType::POST); + checkResponse("OK"); + processRequest("/upload", RequestType::GET); + checkResponse("404"); + + processRequest("/page123"); + checkResponse("Some page"); + + processRequest("/page0"); + checkResponse("Default page"); + + processRequest("/chapter/test/page123"); + checkResponse("Chapter: test, page[123]"); + + processRequest("/chapter_test/page_123"); + checkResponse("TestBook Chapter: test, page[123]"); + + processRequest("/book-Hello_world/chapter/test/page/123"); + checkResponse("Book: Hello_world, Chapter: test, page[123]"); + + processRequest("/book-Hello_world/chapter/test/"); + checkResponse("ROUTE_PARAM_ERROR: PARAM COUNT MISMATCH, EXPECTED:3 ACTUAL:2"); + + processRequest("/book/Hello/"); + checkResponse("Book: Hello"); + + processRequest("/book/Hello/world/"); + checkResponse("Book: Hello#world"); + + processRequest("/no_capture_groups"); + checkResponse("Book: "); + processRequest("/no_capture_groups2"); + checkResponse("Book: "); + + processRequest("/chapter_test"); + checkResponse("Chapter: test"); + + processRequest("/chapter_test/"); + checkResponse("Chapter: test"); + + processRequest("/files/test.xml"); + checkResponse("XML file: test"); + + processRequest("/files/test.xml1"); + checkResponse("404"); + + processRequest("/foo"); + checkResponse("404"); + + processRequest("/context"); + checkResponse("1"); + + processRequest("/context2"); + checkResponse("1"); + + processRequest("/test/context"); + checkResponse("test: 1"); +} + +#endif + TEST_F(Router, RequestProcessorWithoutResponse) { auto noResponseRequestProcessor = NoResponseRequestProcessor{}; auto parametrizedNoResponseRequestProcessor = ParametrizedNoResponseRequestProcessor{}; route("/context/").process(); auto paramRequestProcessor = ParametrizedAltIncrementContext{}; - route(whaleroute::rx{"/context/param/(\\d+)"}).process(paramRequestProcessor); + routeRegex("/context/param/(\\d+)").process(paramRequestProcessor); route("/test").process(noResponseRequestProcessor); - route(R"(/test/(\d+)/)"_rx).process(parametrizedNoResponseRequestProcessor); - route(R"(/context/.*)"_rx, RequestType::GET) + routeRegex(R"(/test/(\d+)/)").process(parametrizedNoResponseRequestProcessor); + routeRegex(R"(/context/.*)", RequestType::GET) .process( [](const Request&, Response& response, Context& context) { @@ -358,7 +803,7 @@ TEST_F(Router, DefaultUnmatchedRequestHandler) TEST_F(Router, MultipleRoutesMatching) { - route(whaleroute::rx{"/greet/.*"}, RequestType::GET) + routeRegex({"/greet/.*"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -372,7 +817,7 @@ TEST_F(Router, MultipleRoutesMatching) response.state->wasSent = true; }); auto testState = std::string{}; - route(whaleroute::rx{"/thank/.*"}, RequestType::GET) + routeRegex({"/thank/.*"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -476,8 +921,8 @@ TEST_F(Router, SameParametrizedProcessorObjectUsedInMultipleRoutes) { int state = 0; auto counterProcessor = ParametrizedCounterRouteProcessor{state}; - route(whaleroute::rx{"/test/(.+)"}, RequestType::GET).process(counterProcessor); - route(whaleroute::rx{"/test2/(.+)"}, RequestType::GET).process(counterProcessor); + routeRegex({"/test/(.+)"}, RequestType::GET).process(counterProcessor); + routeRegex({"/test2/(.+)"}, RequestType::GET).process(counterProcessor); processRequest("/test/foo"); checkResponse("TEST foo"); @@ -489,8 +934,8 @@ TEST_F(Router, SameParametrizedProcessorObjectUsedInMultipleRoutes) TEST_F(Router, SameParametrizedProcessorTypeCreatedInMultipleRoutes) { int state = 0; - route(whaleroute::rx{"/test/(.+)"}, RequestType::GET).process(state); - route(whaleroute::rx{"/test2/(.+)"}, RequestType::GET).process(state); + routeRegex({"/test/(.+)"}, RequestType::GET).process(state); + routeRegex({"/test2/(.+)"}, RequestType::GET).process(state); processRequest("/test/foo"); checkResponse("TEST foo"); diff --git a/tests/test_router_alt_request_processor.cpp b/tests/test_router_alt_request_processor.cpp index 64586a6..1d7b09a 100644 --- a/tests/test_router_alt_request_processor.cpp +++ b/tests/test_router_alt_request_processor.cpp @@ -159,11 +159,9 @@ struct SendContext { } // namespace -using namespace whaleroute::string_literals; - TEST_F(RouterAltRequestProcessor, Matching) { - route(whaleroute::rx{".+"}).process(); + routeRegex(".+").process(); route("/", RequestType::GET).process(); route("/moon", RequestType::GET).process("Moon"); route("/page0", RequestType::GET) @@ -172,7 +170,7 @@ TEST_F(RouterAltRequestProcessor, Matching) { response.send("Default page"); }); - route(whaleroute::rx{R"(/page\d*)"}, RequestType::GET) + routeRegex({R"(/page\d*)"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -184,19 +182,19 @@ TEST_F(RouterAltRequestProcessor, Matching) { response.send("OK"); }); - route(whaleroute::rx{R"(/chapter/(.+)/page(\d+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/chapter_(.+)/page_(\d+)/)"}, RequestType::GET) + routeRegex({R"(/chapter/(.+)/page(\d+)/)"}, RequestType::GET).process(); + routeRegex({R"(/chapter_(.+)/page_(\d+)/)"}, RequestType::GET) .process("TestBook"); - route(whaleroute::rx{R"(/book-(.+)/chapter/(.+)/page/(\d+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/book-(.+)/chapter/(.+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/book/(\w+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/book/(\w+)/(\w+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/no_capture_groups)"}, RequestType::GET).process(); + routeRegex({R"(/book-(.+)/chapter/(.+)/page/(\d+)/)"}, RequestType::GET).process(); + routeRegex({R"(/book-(.+)/chapter/(.+)/)"}, RequestType::GET).process(); + routeRegex({R"(/book/(\w+)/)"}, RequestType::GET).process(); + routeRegex({R"(/book/(\w+)/(\w+)/)"}, RequestType::GET).process(); + routeRegex({R"(/no_capture_groups)"}, RequestType::GET).process(); route("/no_capture_groups2", RequestType::GET).process(); auto parametrizedProcessor = ChapterNameProcessor{}; - route(whaleroute::rx{R"(/chapter_(.+)/)"}, RequestType::GET).process(parametrizedProcessor); + routeRegex({R"(/chapter_(.+)/)"}, RequestType::GET).process(parametrizedProcessor); route("/param_error").process(parametrizedProcessor); - route(whaleroute::rx{R"(/files/(.*\.xml))"}, RequestType::GET) + routeRegex({R"(/files/(.*\.xml))"}, RequestType::GET) .process( [](const std::string& fileName, const Request&, Response& response) { @@ -211,7 +209,7 @@ TEST_F(RouterAltRequestProcessor, Matching) }); route("/context2", RequestType::GET).process(); - route(whaleroute::rx{"/(.+)/context"}, RequestType::GET) + routeRegex({"/(.+)/context"}, RequestType::GET) .process( [](const std::string& title, const Request&, Response& response, Context& context) { @@ -301,7 +299,7 @@ TEST_F(RouterAltRequestProcessor, DefaultUnmatchedRequestHandler) TEST_F(RouterAltRequestProcessor, MultipleRoutesMatching) { - route(whaleroute::rx{"/greet/.*"}, RequestType::GET) + routeRegex({"/greet/.*"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -315,7 +313,7 @@ TEST_F(RouterAltRequestProcessor, MultipleRoutesMatching) response.state->wasSent = true; }); auto testState = std::string{}; - route(whaleroute::rx{"/thank/.*"}, RequestType::GET) + routeRegex({"/thank/.*"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -419,8 +417,8 @@ TEST_F(RouterAltRequestProcessor, SameParametrizedProcessorObjectUsedInMultipleR { int state = 0; auto counterProcessor = ParametrizedCounterRouteProcessor{state}; - route(whaleroute::rx{"/test/(.+)"}, RequestType::GET).process(counterProcessor); - route(whaleroute::rx{"/test2/(.+)"}, RequestType::GET).process(counterProcessor); + routeRegex({"/test/(.+)"}, RequestType::GET).process(counterProcessor); + routeRegex({"/test2/(.+)"}, RequestType::GET).process(counterProcessor); processRequest("/test/foo"); checkResponse("TEST foo"); @@ -432,8 +430,8 @@ TEST_F(RouterAltRequestProcessor, SameParametrizedProcessorObjectUsedInMultipleR TEST_F(RouterAltRequestProcessor, SameParametrizedProcessorTypeCreatedInMultipleRoutes) { int state = 0; - route(whaleroute::rx{"/test/(.+)"}, RequestType::GET).process(state); - route(whaleroute::rx{"/test2/(.+)"}, RequestType::GET).process(state); + routeRegex({"/test/(.+)"}, RequestType::GET).process(state); + routeRegex({"/test2/(.+)"}, RequestType::GET).process(state); processRequest("/test/foo"); checkResponse("TEST foo"); diff --git a/tests/test_router_without_response_value.cpp b/tests/test_router_without_response_value.cpp index bc3b8c7..c912f35 100644 --- a/tests/test_router_without_response_value.cpp +++ b/tests/test_router_without_response_value.cpp @@ -121,7 +121,7 @@ TEST_F(RouterWithoutResponseValue, Matching) { response.send("Default page"); }); - route(whaleroute::rx{R"(/page\d*)"}, RequestType::GET) + routeRegex({R"(/page\d*)"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -133,13 +133,13 @@ TEST_F(RouterWithoutResponseValue, Matching) { response.send("OK"); }); - route(whaleroute::rx{R"(/chapter/(.+)/page(\d+))"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/chapter_(.+)/page_(\d+))"}, RequestType::GET) + routeRegex({R"(/chapter/(.+)/page(\d+))"}, RequestType::GET).process(); + routeRegex({R"(/chapter_(.+)/page_(\d+))"}, RequestType::GET) .process("TestBook"); auto parametrizedProcessor = ChapterNameProcessor{}; - route(whaleroute::rx{R"(/chapter_(.+)/)"}, RequestType::GET).process(parametrizedProcessor); + routeRegex({R"(/chapter_(.+)/)"}, RequestType::GET).process(parametrizedProcessor); route("/param_error").process(parametrizedProcessor); - route(whaleroute::rx{R"(/files/(.*\.xml))"}, RequestType::GET) + routeRegex({R"(/files/(.*\.xml))"}, RequestType::GET) .process( [](const std::string& fileName, const Request&, Response& response) { @@ -209,7 +209,7 @@ TEST_F(RouterWithoutResponseValue, DefaultUnmatchedRequestHandler) TEST_F(RouterWithoutResponseValue, MultipleRoutesMatching) { - route(whaleroute::rx{"/greet/.*"}, RequestType::GET) + routeRegex({"/greet/.*"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -223,7 +223,7 @@ TEST_F(RouterWithoutResponseValue, MultipleRoutesMatching) response.state->wasSent = true; }); auto testState = std::string{}; - route(whaleroute::rx{"/thank/.*"}, RequestType::GET) + routeRegex({"/thank/.*"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -340,8 +340,8 @@ TEST_F(RouterWithoutResponseValue, SameParametrizedProcessorObjectUsedInMultiple { int state = 0; auto counterProcessor = ParametrizedCounterRouteProcessor{state}; - route(whaleroute::rx{"/test/(.+)"}, RequestType::GET).process(counterProcessor); - route(whaleroute::rx{"/test2/(.+)"}, RequestType::GET).process(counterProcessor); + routeRegex({"/test/(.+)"}, RequestType::GET).process(counterProcessor); + routeRegex({"/test2/(.+)"}, RequestType::GET).process(counterProcessor); processRequest("/test/foo"); checkResponse("TEST foo"); @@ -353,8 +353,8 @@ TEST_F(RouterWithoutResponseValue, SameParametrizedProcessorObjectUsedInMultiple TEST_F(RouterWithoutResponseValue, SameParametrizedProcessorTypeCreatedInMultipleRoutes) { int state = 0; - route(whaleroute::rx{"/test/(.+)"}, RequestType::GET).process(state); - route(whaleroute::rx{"/test2/(.+)"}, RequestType::GET).process(state); + routeRegex({"/test/(.+)"}, RequestType::GET).process(state); + routeRegex({"/test2/(.+)"}, RequestType::GET).process(state); processRequest("/test/foo"); checkResponse("TEST foo"); diff --git a/tests/test_router_without_route_context.cpp b/tests/test_router_without_route_context.cpp index db10c5c..ec7fdcc 100644 --- a/tests/test_router_without_route_context.cpp +++ b/tests/test_router_without_route_context.cpp @@ -140,8 +140,6 @@ class ChapterNameProcessor { } }; -using namespace whaleroute::string_literals; - TEST_F(RouterWithoutRouteContext, Matching) { route("/", RequestType::GET).process(); @@ -152,7 +150,7 @@ TEST_F(RouterWithoutRouteContext, Matching) { response.send("Default page"); }); - route(whaleroute::rx{R"(/page\d*)"}, RequestType::GET) + routeRegex({R"(/page\d*)"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -164,17 +162,17 @@ TEST_F(RouterWithoutRouteContext, Matching) { response.send("OK"); }); - route(whaleroute::rx{R"(/chapter/(.+)/page(\d+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/chapter_(.+)/page_(\d+)/)"}, RequestType::GET) + routeRegex({R"(/chapter/(.+)/page(\d+)/)"}, RequestType::GET).process(); + routeRegex({R"(/chapter_(.+)/page_(\d+)/)"}, RequestType::GET) .process("TestBook"); - route(whaleroute::rx{R"(/book-(.+)/chapter/(.+)/page/(\d+)/)"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/book-(.+)/chapter/(.+))"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/book/(\w+))"}, RequestType::GET).process(); - route(whaleroute::rx{R"(/book/(\w+)/(\w+))"}, RequestType::GET).process(); + routeRegex({R"(/book-(.+)/chapter/(.+)/page/(\d+)/)"}, RequestType::GET).process(); + routeRegex({R"(/book-(.+)/chapter/(.+))"}, RequestType::GET).process(); + routeRegex({R"(/book/(\w+))"}, RequestType::GET).process(); + routeRegex({R"(/book/(\w+)/(\w+))"}, RequestType::GET).process(); auto parametrizedProcessor = ChapterNameProcessor{}; - route(whaleroute::rx{R"(/chapter_(.+)/)"}, RequestType::GET).process(parametrizedProcessor); + routeRegex({R"(/chapter_(.+)/)"}, RequestType::GET).process(parametrizedProcessor); route("/param_error").process(parametrizedProcessor); - route(whaleroute::rx{R"(/files/(.*\.xml))"}, RequestType::GET) + routeRegex({R"(/files/(.*\.xml))"}, RequestType::GET) .process( [](const std::string& fileName, const Request&, Response& response) { @@ -250,7 +248,7 @@ TEST_F(RouterWithoutRouteContext, DefaultUnmatchedRequestHandler) TEST_F(RouterWithoutRouteContext, MultipleRoutesMatching) { - route(whaleroute::rx{"/greet/.*"}, RequestType::GET) + routeRegex({"/greet/.*"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -264,7 +262,7 @@ TEST_F(RouterWithoutRouteContext, MultipleRoutesMatching) response.state->wasSent = true; }); auto testState = std::string{}; - route(whaleroute::rx{"/thank/.*"}, RequestType::GET) + routeRegex({"/thank/.*"}, RequestType::GET) .process( [](const Request&, Response& response) { @@ -367,8 +365,8 @@ class ParametrizedCounterRouteProcessor { // TEST_F(RouterWithoutRouteContext, SameParametrizedProcessorObjectUsedInMultipleRoutes){ // int state = 0; // auto counterProcessor = ParametrizedCounterRouteProcessor{state}; -// route(whaleroute::rx{"/test/(.+)"}, RequestType::GET).process(counterProcessor); -// route(whaleroute::rx{"/test2/(.+)"}, RequestType::GET).process(counterProcessor); +// routeRegex({"/test/(.+)"}, RequestType::GET).process(counterProcessor); +// routeRegex({"/test2/(.+)"}, RequestType::GET).process(counterProcessor); // // processRequest("/test/foo"); // checkResponse("TEST foo"); @@ -379,8 +377,8 @@ class ParametrizedCounterRouteProcessor { // TEST_F(RouterWithoutRouteContext, SameParametrizedProcessorTypeCreatedInMultipleRoutes){ // int state = 0; -// route(whaleroute::rx{"/test/(.+)"}, RequestType::GET).process(state); -// route(whaleroute::rx{"/test2/(.+)"}, RequestType::GET).process(state); +// routeRegex({"/test/(.+)"}, RequestType::GET).process(state); +// routeRegex({"/test2/(.+)"}, RequestType::GET).process(state); // // processRequest("/test/foo"); // checkResponse("TEST foo"); diff --git a/tests/test_router_without_route_matchers.cpp b/tests/test_router_without_route_matchers.cpp index 2575961..7132ecb 100644 --- a/tests/test_router_without_route_matchers.cpp +++ b/tests/test_router_without_route_matchers.cpp @@ -119,7 +119,7 @@ TEST_F(RouterWithoutRouteMatchers, Matching) { response.send("Default page"); }); - route(whaleroute::rx{R"(/page\d*)"}) + routeRegex({R"(/page\d*)"}) .process( [](const Request&, Response& response) { @@ -130,12 +130,12 @@ TEST_F(RouterWithoutRouteMatchers, Matching) { response.send("OK"); }); - route(whaleroute::rx{R"(/chapter/(.+)/page(\d+))"}).process(); - route(whaleroute::rx{R"(/chapter_(.+)/page_(\d+))"}).process("TestBook"); + routeRegex({R"(/chapter/(.+)/page(\d+))"}).process(); + routeRegex({R"(/chapter_(.+)/page_(\d+))"}).process("TestBook"); auto parametrizedProcessor = ChapterNameProcessor{}; - route(whaleroute::rx{R"(/chapter_(.+)/)"}).process(parametrizedProcessor); + routeRegex({R"(/chapter_(.+)/)"}).process(parametrizedProcessor); route("/param_error").process(parametrizedProcessor); - route(whaleroute::rx{R"(/files/(.*\.xml))"}) + routeRegex({R"(/files/(.*\.xml))"}) .process( [](const std::string& fileName, const Request&, Response& response) { @@ -190,7 +190,7 @@ TEST_F(RouterWithoutRouteMatchers, DefaultUnmatchedRequestHandler) TEST_F(RouterWithoutRouteMatchers, MultipleRoutesMatching) { - route(whaleroute::rx{"/greet/.*"}) + routeRegex({"/greet/.*"}) .process( [](const Request&, Response& response) { @@ -204,7 +204,7 @@ TEST_F(RouterWithoutRouteMatchers, MultipleRoutesMatching) response.state->wasSent = true; }); auto testState = std::string{}; - route(whaleroute::rx{"/thank/.*"}) + routeRegex({"/thank/.*"}) .process( [](const Request&, Response& response) { @@ -308,8 +308,8 @@ TEST_F(RouterWithoutRouteMatchers, SameParametrizedProcessorObjectUsedInMultiple { int state = 0; auto counterProcessor = ParametrizedCounterRouteProcessor{state}; - route(whaleroute::rx{"/test/(.+)"}).process(counterProcessor); - route(whaleroute::rx{"/test2/(.+)"}).process(counterProcessor); + routeRegex({"/test/(.+)"}).process(counterProcessor); + routeRegex({"/test2/(.+)"}).process(counterProcessor); processRequest("/test/foo"); checkResponse("TEST foo"); @@ -321,8 +321,8 @@ TEST_F(RouterWithoutRouteMatchers, SameParametrizedProcessorObjectUsedInMultiple TEST_F(RouterWithoutRouteMatchers, SameParametrizedProcessorTypeCreatedInMultipleRoutes) { int state = 0; - route(whaleroute::rx{"/test/(.+)"}).process(state); - route(whaleroute::rx{"/test2/(.+)"}).process(state); + routeRegex({"/test/(.+)"}).process(state); + routeRegex({"/test2/(.+)"}).process(state); processRequest("/test/foo"); checkResponse("TEST foo"); diff --git a/tests/test_router_without_route_matchers_and_response_value.cpp b/tests/test_router_without_route_matchers_and_response_value.cpp index 1f01f7d..5eb51a9 100644 --- a/tests/test_router_without_route_matchers_and_response_value.cpp +++ b/tests/test_router_without_route_matchers_and_response_value.cpp @@ -113,7 +113,7 @@ TEST_F(RouterWithoutRouteMatchersAndResponseValue, Matching) { response.send("Default page"); }); - route(whaleroute::rx{R"(/page\d*)"}) + routeRegex({R"(/page\d*)"}) .process( [](const Request&, Response& response) { @@ -124,12 +124,12 @@ TEST_F(RouterWithoutRouteMatchersAndResponseValue, Matching) { response.send("OK"); }); - route(whaleroute::rx{R"(/chapter/(.+)/page(\d+))"}).process(); - route(whaleroute::rx{R"(/chapter_(.+)/page_(\d+))"}).process("TestBook"); + routeRegex({R"(/chapter/(.+)/page(\d+))"}).process(); + routeRegex({R"(/chapter_(.+)/page_(\d+))"}).process("TestBook"); auto parametrizedProcessor = ChapterNameProcessor{}; - route(whaleroute::rx{R"(/chapter_(.+)/)"}).process(parametrizedProcessor); + routeRegex({R"(/chapter_(.+)/)"}).process(parametrizedProcessor); route("/param_error").process(parametrizedProcessor); - route(whaleroute::rx{R"(/files/(.*\.xml))"}) + routeRegex({R"(/files/(.*\.xml))"}) .process( [](const std::string& fileName, const Request&, Response& response) { @@ -192,7 +192,7 @@ TEST_F(RouterWithoutRouteMatchersAndResponseValue, DefaultUnmatchedRequestHandle TEST_F(RouterWithoutRouteMatchersAndResponseValue, MultipleRoutesMatching) { - route(whaleroute::rx{"/greet/.*"}) + routeRegex({"/greet/.*"}) .process( [](const Request&, Response& response) { @@ -206,7 +206,7 @@ TEST_F(RouterWithoutRouteMatchersAndResponseValue, MultipleRoutesMatching) response.state->wasSent = true; }); auto testState = std::string{}; - route(whaleroute::rx{"/thank/.*"}) + routeRegex({"/thank/.*"}) .process( [](const Request&, Response& response) { @@ -318,8 +318,8 @@ TEST_F(RouterWithoutRouteMatchersAndResponseValue, SameParametrizedProcessorObje { int state = 0; auto counterProcessor = ParametrizedCounterRouteProcessor{state}; - route(whaleroute::rx{"/test/(.+)"}).process(counterProcessor); - route(whaleroute::rx{"/test2/(.+)"}).process(counterProcessor); + routeRegex({"/test/(.+)"}).process(counterProcessor); + routeRegex({"/test2/(.+)"}).process(counterProcessor); processRequest("/test/foo"); checkResponse("TEST foo"); @@ -331,8 +331,8 @@ TEST_F(RouterWithoutRouteMatchersAndResponseValue, SameParametrizedProcessorObje TEST_F(RouterWithoutRouteMatchersAndResponseValue, SameParametrizedProcessorTypeCreatedInMultipleRoutes) { int state = 0; - route(whaleroute::rx{"/test/(.+)"}).process(state); - route(whaleroute::rx{"/test2/(.+)"}).process(state); + routeRegex({"/test/(.+)"}).process(state); + routeRegex({"/test2/(.+)"}).process(state); processRequest("/test/foo"); checkResponse("TEST foo"); diff --git a/tests_cpp20/CMakeLists.txt b/tests_cpp20/CMakeLists.txt new file mode 100644 index 0000000..fb73628 --- /dev/null +++ b/tests_cpp20/CMakeLists.txt @@ -0,0 +1,16 @@ +project(test_whaleroute_cpp20) + +SealLake_GoogleTest( + SOURCES + ../tests/test_router.cpp + ../tests/test_response_value_arguments.cpp + ../tests/test_multiple_route_matchers.cpp + ../tests/test_router_without_route_context.cpp + ../tests/test_router_without_response_value.cpp + ../tests/test_router_without_route_matchers.cpp + ../tests/test_router_without_route_matchers_and_response_value.cpp + ../tests/test_router_alt_request_processor.cpp + COMPILE_FEATURES cxx_std_20 + LIBRARIES + whaleroute::whaleroute +) \ No newline at end of file