├── .github └── workflows │ ├── macos.yml │ ├── ubuntu.yml │ └── windows.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── router-config.cmake └── target_supported_compile_options.cmake ├── include ├── router.h └── router │ ├── fields.h │ ├── function_traits.h │ ├── impl │ └── variables.h │ ├── path.h │ ├── path_callback.h │ ├── path_map.h │ ├── proxy.h │ ├── slug.h │ ├── table.h │ ├── tuple_slice.h │ ├── variables.h │ ├── variadic_lookup.h │ └── wrap_callback.h └── tests ├── CMakeLists.txt ├── catch2.hpp ├── cmake └── Modules │ └── FindRuntime.cmake ├── main.cpp ├── path.cpp ├── proxy_table.cpp ├── slug.cpp ├── table.cpp ├── tuple_slice.cpp └── variables.cpp /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macos-latest 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: cmake 17 | run: cmake -B build -DCMAKE_BUILD_TYPE=Debug 18 | - name: build 19 | run: cmake --build build 20 | - name: test 21 | run: ./build/tests/test 22 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: ubuntu-latest 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: compiler 17 | run: sudo apt-get update && sudo apt-get install -y clang 18 | - name: cmake 19 | run: cmake -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=/usr/bin/clang++ 20 | - name: build 21 | run: cmake --build build 22 | - name: test 23 | run: ./build/tests/test 24 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: windows-latest 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: cmake 17 | run: cmake -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=/usr/bin/clang++ 18 | - name: build 19 | run: cmake --build build 20 | - name: test 21 | run: .\build\tests\Debug\test.exe 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Build dir 35 | build* 36 | 37 | # Editor temp files 38 | **swp 39 | 40 | # clangd / youcompleteme 41 | compile_commands.json 42 | .clangd 43 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(router 3 | VERSION 0.0.1 4 | LANGUAGES CXX 5 | ) 6 | 7 | add_library(router INTERFACE) 8 | add_library(router::router ALIAS router) 9 | 10 | target_compile_features(router INTERFACE cxx_std_17) 11 | 12 | target_include_directories(router INTERFACE 13 | $ 14 | $ 15 | ) 16 | 17 | set(ROUTER_MASTER_PROJECT OFF) 18 | if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) 19 | set(ROUTER_MASTER_PROJECT ON) 20 | endif() 21 | 22 | option(ROUTER_TEST "Build the tests" ${ROUTER_MASTER_PROJECT}) 23 | 24 | # only override the warning options if we're build as the master 25 | # project, in other cases leave them alone since we might be added 26 | # to a project with laxer standards for dealing with warnings 27 | if (ROUTER_MASTER_PROJECT) 28 | # msvc does not support normal options 29 | if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") 30 | # set warning level to 4 31 | target_compile_options(router INTERFACE /W4) 32 | else() 33 | # include helper to enable only options which are supported 34 | include(cmake/target_supported_compile_options.cmake) 35 | 36 | # try all the regular options and enable them if possible 37 | target_supported_compile_options(router INTERFACE -Wall) 38 | target_supported_compile_options(router INTERFACE -Wextra) 39 | target_supported_compile_options(router INTERFACE -Wdeprecated) 40 | target_supported_compile_options(router INTERFACE -Wdocumentation) 41 | endif() 42 | endif() 43 | 44 | install( 45 | DIRECTORY include/router 46 | DESTINATION include 47 | ) 48 | 49 | install( 50 | FILES include/router.h 51 | DESTINATION include 52 | ) 53 | 54 | install( 55 | TARGETS router 56 | EXPORT router-targets 57 | DESTINATION lib 58 | ) 59 | 60 | install( 61 | EXPORT router-targets 62 | NAMESPACE router:: 63 | DESTINATION lib/cmake/router 64 | ) 65 | 66 | include(CMakePackageConfigHelpers) 67 | write_basic_package_version_file( 68 | "${CMAKE_CURRENT_BINARY_DIR}/router/router-config-version.cmake" 69 | VERSION ${PROJECT_VERSION} 70 | COMPATIBILITY AnyNewerVersion 71 | ) 72 | 73 | export( 74 | EXPORT router-targets 75 | FILE "${CMAKE_CURRENT_BINARY_DIR}/router/router-targets.cmake" 76 | NAMESPACE router:: 77 | ) 78 | 79 | configure_file(cmake/router-config.cmake 80 | "${CMAKE_CURRENT_BINARY_DIR}/router/router-config.cmake" 81 | COPYONLY 82 | ) 83 | 84 | install( 85 | FILES 86 | cmake/router-config.cmake 87 | "${CMAKE_CURRENT_BINARY_DIR}/router/router-config-version.cmake" 88 | DESTINATION 89 | lib/cmake/router 90 | ) 91 | 92 | if (ROUTER_TEST) 93 | add_subdirectory(tests) 94 | endif() 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License 1.0 (BSL-1.0) 2 | 3 | Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: 4 | 5 | The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cpprouter 2 | A modern, header-only request router for C++ 3 | 4 | ![](https://github.com/omartijn/cpprouter/workflows/ubuntu-latest/badge.svg) 5 | ![](https://github.com/omartijn/cpprouter/workflows/macos-latest/badge.svg) 6 | ![](https://github.com/omartijn/cpprouter/workflows/windows-latest/badge.svg) 7 | 8 | ## Routing requests 9 | 10 | This library is designed to route requests to an associated callback. Requests are matched 11 | using a _pattern_, which may contain embedded regular expressions. The data matched with the 12 | regular expression is then parsed and provided to the callback. 13 | 14 | ### URL patterns 15 | 16 | The router matches incoming requests against list of URL patterns. A URL pattern consists 17 | of a combination of literal data and _slugs_, matched using a regular expression. Slugs are 18 | opened with a `{` and closed with a `}`. 19 | 20 | The following example pattern 21 | 22 | `/test/{\d+}/{\w+}` 23 | 24 | would match targets like `/test/123/hello` and `/test/999/bye`, but not `/test/xyz/abc` (because 25 | \d only matches digits). 26 | 27 | ### Callbacks 28 | 29 | The library does not impose a particular callback signature on the user, instead letting the user 30 | choose the signature that suits their needs. 31 | 32 | The list of patterns and callbacks is stored in a _routing table_, which is available in the 33 | `router::table` class. This class is templated on the signature of the used callback functions. 34 | 35 | Say, your callback functions follow the `int(std::string&& body)` signature, you would create 36 | an instance of `router::table`. On this instance you can register your URL 37 | patterns and their associated callbacks using the `add()` member function: 38 | 39 | ``` 40 | router.add<&callback>("pattern"); 41 | router.add<&class::callback>("pattern", instance_pointer); 42 | ``` 43 | 44 | The first variant is used for registering a _free function_, while the second variant is used 45 | for registering a _member function_ as the callback. Callbacks may be registered on _any_ class, 46 | as long as they have the correct signature. 47 | 48 | ### Working with slug data 49 | 50 | If the URL patterns contain _slugs_, you are probably interested in the data they hold. There are 51 | three ways to get this data. 52 | 53 | - Accept the data as separate parameters to the callback 54 | - Accept a tuple with the slug data in the callback 55 | - Accept a custom _data transfer object_ in the callback 56 | 57 | The first two options are the easiest. Let's assume our previous examples of a callback definition 58 | of `int(std::string&&)` and a pattern of `/test/{\d+}/{\w+}`. From the pattern, it's clear that the 59 | first slug should contain _numeric data_, and the second slug contains a string. We could therefore 60 | define and register our callback like this: 61 | 62 | ``` 63 | int callback(std::string&& body, int numeric, std::string&& word); 64 | int callback(std::string&& body, std::tuple&& slugs); 65 | ``` 66 | 67 | When a matching request is routed, the slugs are automatically parsed and converted to the right 68 | type before the callback is invoked. 69 | 70 | It is also possible to make a dedicated struct, a so-called _data transfer object_, to receive 71 | the slug data. 72 | 73 | The _data transfer object_ needs to be annotated so that cpprouter knows how to parse the slug 74 | data, this is done by defining a `using` alias on the class. 75 | 76 | ``` 77 | class slug_dto { 78 | int numeric; 79 | std::string word; 80 | 81 | using dto = router::dto 82 | ::bind<&slug_dto::numeric> 83 | ::bind<&slug_dto::word>; 84 | }; 85 | ``` 86 | 87 | Assuming the same callback signature as before, the router and callback could be declared as follows: 88 | 89 | `int callback(std::string&& body, slug_dto&& slugs);` 90 | 91 | ### Bringing it all together 92 | 93 | See the examples (TODO) 94 | -------------------------------------------------------------------------------- /cmake/router-config.cmake: -------------------------------------------------------------------------------- 1 | include("${CMAKE_CURRENT_LIST_DIR}/router-targets.cmake") 2 | -------------------------------------------------------------------------------- /cmake/target_supported_compile_options.cmake: -------------------------------------------------------------------------------- 1 | include(CheckCXXCompilerFlag) 2 | 3 | function (target_supported_compile_options target) 4 | set(options BEFORE) 5 | set(arguments INTERFACE PUBLIC PRIVATE) 6 | cmake_parse_arguments(OPTION "${options}" "" "${arguments}" ${ARGN}) 7 | 8 | foreach(compile_option ${OPTION_INTERFACE}) 9 | # create the flag to use - CMake reuses the output variable 10 | # and will skip the check if the variable already exists 11 | string(REPLACE "-" "_" supported ${compile_option}_cxx) 12 | 13 | check_cxx_compiler_flag(${compile_option} ${supported}) 14 | if (${${supported}}) 15 | if (${OPTION_BEFORE}) 16 | target_compile_options(${target} BEFORE INTERFACE ${compile_option}) 17 | else() 18 | target_compile_options(${target} INTERFACE ${compile_option}) 19 | endif() 20 | endif() 21 | endforeach(compile_option) 22 | 23 | foreach(compile_option ${OPTION_PUBLIC}) 24 | # create the flag to use - CMake reuses the output variable 25 | # and will skip the check if the variable already exists 26 | string(REPLACE "-" "_" supported ${compile_option}_cxx) 27 | 28 | check_cxx_compiler_flag(${compile_option} ${supported}) 29 | if (${${supported}}) 30 | if (${OPTION_BEFORE}) 31 | target_compile_options(${target} BEFORE PUBLIC ${compile_option}) 32 | else() 33 | target_compile_options(${target} PUBLIC ${compile_option}) 34 | endif() 35 | endif() 36 | endforeach(compile_option) 37 | 38 | foreach(compile_option ${OPTION_PRIVATE}) 39 | # create the flag to use - CMake reuses the output variable 40 | # and will skip the check if the variable already exists 41 | string(REPLACE "-" "_" supported ${compile_option}_cxx) 42 | 43 | check_cxx_compiler_flag(${compile_option} ${supported}) 44 | if (${${supported}}) 45 | if (${OPTION_BEFORE}) 46 | target_compile_options(${target} BEFORE PRIVATE ${compile_option}) 47 | else() 48 | target_compile_options(${target} PRIVATE ${compile_option}) 49 | endif() 50 | endif() 51 | endforeach(compile_option) 52 | 53 | endfunction() 54 | -------------------------------------------------------------------------------- /include/router.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | #include "router/table.h" 5 | -------------------------------------------------------------------------------- /include/router/fields.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | namespace router { 11 | 12 | /** 13 | * Process a simple string field 14 | * 15 | * @param input The slug data 16 | * @param output The field to set 17 | */ 18 | inline void process_field(std::string_view input, std::string& output) 19 | { 20 | // assign the input data to the output string 21 | output.assign(input); 22 | } 23 | 24 | /** 25 | * Process a simple string field 26 | * 27 | * @param input The slug data 28 | * @param output The field to set 29 | */ 30 | inline void process_field(std::string_view input, std::string_view& output) 31 | { 32 | // assign the input data to the output string 33 | output = input; 34 | } 35 | 36 | /** 37 | * Process a field containing numerical data 38 | * 39 | * @param input The slug data 40 | * @param output The field to set 41 | */ 42 | template 43 | std::enable_if_t> 44 | process_field(std::string_view input, integral& output) 45 | { 46 | // first parse the input data 47 | auto result = std::from_chars(input.data(), std::next(input.data(), input.size()), output, 10); 48 | 49 | // check whether we successfully parsed the data and whether all of it was parsed 50 | if (result.ec != std::errc{}) { 51 | // some of the data was invalid and could not be converted 52 | throw std::range_error{ std::make_error_code(result.ec).message() }; 53 | } else if (result.ptr != std::next(input.data(), input.size())) { 54 | // part of the data contained numeric input, but not all of it 55 | throw std::range_error{ "Input data contains non-numerical data" }; 56 | } 57 | } 58 | 59 | /** 60 | * Type trait to check whether a field is processable 61 | * 62 | * Default for when no valid match is made 63 | */ 64 | template 65 | struct is_processable_field : std::false_type {}; 66 | 67 | /** 68 | * Match for a valid processable field 69 | */ 70 | template 71 | struct is_processable_field())) 76 | > 77 | > 78 | >> : std::true_type {}; 79 | 80 | } 81 | -------------------------------------------------------------------------------- /include/router/function_traits.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "tuple_slice.h" 5 | 6 | 7 | namespace router { 8 | 9 | /** 10 | * Unspecialized function traits 11 | */ 12 | template 13 | struct function_traits; 14 | 15 | /** 16 | * Specialize for a function type 17 | */ 18 | template 19 | struct function_traits 20 | { 21 | constexpr static bool is_member_function = false; 22 | constexpr static bool is_const = false; 23 | constexpr static bool is_noexcept = false; 24 | constexpr static std::size_t arity = sizeof...(Args); 25 | using return_type = R; 26 | 27 | using arguments_tuple = std::tuple; 28 | 29 | template 30 | using arguments_slice = tuple_slice_t...>; 31 | 32 | template 33 | using argument_type = std::tuple_element_t>; 34 | }; 35 | 36 | /** 37 | * Specialize for noexcept function types 38 | */ 39 | template 40 | struct function_traits 41 | { 42 | constexpr static bool is_member_function = false; 43 | constexpr static bool is_const = false; 44 | constexpr static bool is_noexcept = true; 45 | constexpr static std::size_t arity = sizeof...(Args); 46 | using return_type = R; 47 | 48 | using arguments_tuple = std::tuple; 49 | 50 | template 51 | using arguments_slice = tuple_slice_t...>; 52 | 53 | template 54 | using argument_type = std::tuple_element_t>; 55 | }; 56 | 57 | /** 58 | * Alias for a function pointer 59 | */ 60 | template 61 | struct function_traits : public function_traits {}; 62 | 63 | /** 64 | * Alias for a noexcept-qualified function pointer 65 | */ 66 | template 67 | struct function_traits : public function_traits {}; 68 | 69 | /** 70 | * Specialize for a member function type 71 | */ 72 | template 73 | struct function_traits 74 | { 75 | constexpr static bool is_member_function = true; 76 | constexpr static bool is_const = false; 77 | constexpr static bool is_noexcept = false; 78 | constexpr static std::size_t arity = sizeof...(Args); 79 | using member_type = X; 80 | using return_type = R; 81 | 82 | using arguments_tuple = std::tuple; 83 | 84 | template 85 | using arguments_slice = tuple_slice_t...>; 86 | 87 | template 88 | using argument_type = std::tuple_element_t>; 89 | }; 90 | 91 | /** 92 | * Specialize for a const member function type 93 | */ 94 | template 95 | struct function_traits 96 | { 97 | constexpr static bool is_member_function = true; 98 | constexpr static bool is_const = true; 99 | constexpr static bool is_noexcept = false; 100 | constexpr static std::size_t arity = sizeof...(Args); 101 | using member_type = X; 102 | using return_type = R; 103 | 104 | using arguments_tuple = std::tuple; 105 | 106 | template 107 | using arguments_slice = tuple_slice_t...>; 108 | 109 | template 110 | using argument_type = std::tuple_element_t>; 111 | }; 112 | 113 | /** 114 | * Specialize for a noexcept member function type 115 | */ 116 | template 117 | struct function_traits 118 | { 119 | constexpr static bool is_member_function = true; 120 | constexpr static bool is_const = false; 121 | constexpr static bool is_noexcept = true; 122 | constexpr static std::size_t arity = sizeof...(Args); 123 | using member_type = X; 124 | using return_type = R; 125 | 126 | using arguments_tuple = std::tuple; 127 | 128 | template 129 | using arguments_slice = tuple_slice_t...>; 130 | 131 | template 132 | using argument_type = std::tuple_element_t>; 133 | }; 134 | 135 | /** 136 | * Specialize for a const noexcept member function type 137 | */ 138 | template 139 | struct function_traits 140 | { 141 | constexpr static bool is_member_function = true; 142 | constexpr static bool is_const = true; 143 | constexpr static bool is_noexcept = true; 144 | constexpr static std::size_t arity = sizeof...(Args); 145 | using member_type = X; 146 | using return_type = R; 147 | 148 | using arguments_tuple = std::tuple; 149 | 150 | template 151 | using arguments_slice = tuple_slice_t...>; 152 | 153 | template 154 | using argument_type = std::tuple_element_t>; 155 | }; 156 | 157 | } 158 | -------------------------------------------------------------------------------- /include/router/impl/variables.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "../fields.h" 7 | 8 | 9 | namespace router::impl { 10 | 11 | /** 12 | * Read a vector of slugs and parse them 13 | * into the given tuple 14 | */ 15 | template 16 | void to_dto(const std::vector& slugs, std::tuple& output, std::index_sequence) 17 | { 18 | // ensure the number of slugs is correct 19 | if (slugs.size() != sizeof...(types)) { 20 | // we cannot parse the data 21 | throw std::logic_error{ "Cannot convert slugs to dto: slug count mismatch" }; 22 | } 23 | 24 | // iterator to use in the fold expression 25 | auto iter = begin(slugs); 26 | 27 | // fold the fields into the output object 28 | (process_field(*iter++, std::get(output)), ...); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /include/router/path.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "slug.h" 7 | 8 | 9 | 10 | namespace router { 11 | 12 | /** 13 | * A path that can be routed 14 | */ 15 | class path 16 | { 17 | public: 18 | /** 19 | * Constructor 20 | * 21 | * @param path The path to route 22 | */ 23 | path(std::string_view path) 24 | { 25 | // find the first slug inside the given path 26 | std::size_t position = slug::find_start(path); 27 | 28 | // does the given path contain a slug? 29 | if (position == std::string_view::npos) { 30 | // we use the whole path as prefix, since there are no slugs 31 | _prefix.assign(path); 32 | return; 33 | } 34 | 35 | // extract the prefix part from the path 36 | _prefix.assign(path, 0, position); 37 | path.remove_prefix(position); 38 | 39 | // process all slugs given in the path 40 | while (!path.empty()) { 41 | // parse the slug at the front of the path 42 | slug slug { path }; 43 | std::string_view suffix { }; 44 | 45 | // find the start of the next slug (if any) 46 | position = slug::find_start(path); 47 | 48 | // do we have another slug available 49 | if (position == std::string_view::npos) { 50 | // no more slugs, use the whole path 51 | std::swap(suffix, path); 52 | } else { 53 | // extract the part up to the next slug from the path 54 | suffix = path.substr(0, position); 55 | path = path.substr(position); 56 | } 57 | 58 | // store the slug for later matching 59 | _edges.emplace_back(std::move(slug), suffix); 60 | } 61 | } 62 | 63 | /** 64 | * Get the fixed prefix for this path 65 | * 66 | * @return The fixed part, up to the first slug 67 | */ 68 | std::string_view prefix() const noexcept 69 | { 70 | return _prefix; 71 | } 72 | 73 | /** 74 | * Check whether the prefix matches the given input 75 | * 76 | * @param input The input to test 77 | * @return Whether the prefix matches 78 | */ 79 | bool match_prefix(std::string_view input) const noexcept 80 | { 81 | // check whether the input correctly begins with the prefix 82 | return input.substr(0, _prefix.size()) == _prefix; 83 | } 84 | 85 | /** 86 | * Check whether the path matches the given input 87 | * 88 | * @param input The input to test 89 | * @param output The matched slug data 90 | * @return Whether the input matches the path 91 | */ 92 | bool match(std::string_view input, std::vector& output) const 93 | { 94 | // clean the output and allocate memory 95 | output.clear(); 96 | output.reserve(_edges.size()); 97 | 98 | // first we check whether the input correctly begins with the prefix 99 | if (!match_prefix(input)) { 100 | // input does not start with the prefix 101 | return false; 102 | } 103 | 104 | // remove the prefix from the input 105 | input.remove_prefix(_prefix.size()); 106 | 107 | // go over all the edges 108 | for (const auto& [slug, suffix] : _edges) { 109 | // the matched slug data 110 | std::string_view matched_data; 111 | 112 | // check the slug 113 | if (!slug.match(input, matched_data)) { 114 | // the slug failed to match 115 | return false; 116 | } 117 | 118 | // check whether the input now begins with 119 | // the slug suffix (which comes after a slug) 120 | if (input.substr(0, suffix.size()) != suffix) { 121 | // the slug suffix did not match 122 | return false; 123 | } 124 | 125 | // remove the suffix from the input and store the matched data 126 | input.remove_prefix(suffix.size()); 127 | output.push_back(matched_data); 128 | } 129 | 130 | // prefix and all sludges matched, this should have 131 | // consumed all data, otherwise there is unmatched 132 | // trailing input 133 | return input.empty(); 134 | } 135 | private: 136 | /** 137 | * An edge consists of a slug and a 138 | * literal trailing suffix 139 | */ 140 | using edge = std::pair; 141 | 142 | std::string _prefix; // the part of the path up to the first slug 143 | std::vector _edges; 144 | }; 145 | 146 | } 147 | -------------------------------------------------------------------------------- /include/router/path_callback.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "function_traits.h" 4 | #include "wrap_callback.h" 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | namespace router { 11 | 12 | /** 13 | * In-place option used for the constructor 14 | */ 15 | template 16 | struct in_place_value {}; 17 | 18 | /** 19 | * Undefined templated class for specializing 20 | * into a function-like template class 21 | */ 22 | template 23 | class path_callback; 24 | 25 | /** 26 | * A callback, wrapped to be used with a path 27 | */ 28 | template 29 | class path_callback 30 | { 31 | public: 32 | /** 33 | * Default constructor 34 | */ 35 | path_callback() = default; 36 | 37 | /** 38 | * Construct with a free function 39 | * 40 | * @tparam callback The callback to install 41 | */ 42 | template >> 43 | path_callback(in_place_value) noexcept : 44 | _callback{ &wrap_callback } 45 | {} 46 | 47 | /** 48 | * Construct with a member function 49 | * 50 | * @tparam callback The callback to install 51 | * @param instance The instance to invoke on 52 | */ 53 | template >> 54 | path_callback(in_place_value, typename function_traits::member_type* instance) noexcept : 55 | _callback{ &wrap_callback }, 56 | _instance{ instance } 57 | {} 58 | 59 | /** 60 | * Set a free function to be invoked 61 | * 62 | * @tparam callback The callback to install 63 | */ 64 | template 65 | std::enable_if_t> 66 | set() noexcept 67 | { 68 | _callback = &wrap_callback; 69 | } 70 | 71 | /** 72 | * Set a member function to be invoked 73 | * 74 | * @tparam callback The callback to install 75 | * @param instance The instance to invoke on 76 | */ 77 | template 78 | std::enable_if_t> 79 | set(typename function_traits::member_type* instance) noexcept 80 | { 81 | _callback = &wrap_callback; 82 | _instance = instance; 83 | } 84 | 85 | /** 86 | * Check if we have a valid callback installed 87 | * 88 | * @return Whether a callback was set 89 | */ 90 | bool valid() const noexcept { return _callback != nullptr; } 91 | operator bool() const noexcept { return valid(); } 92 | 93 | /** 94 | * Invoke the installed function 95 | * 96 | * @param slugs The slugs parsed from the path 97 | * @param parameters The parameters to the callback 98 | * @throws std::bad_function_call In case no member function is installed 99 | */ 100 | return_type operator()(const std::vector& slugs, arguments&&... parameters) const 101 | { 102 | // check whether we have a valid callback 103 | if (_callback == nullptr) { 104 | throw std::bad_function_call{}; 105 | } 106 | 107 | // invoke the callback 108 | return _callback(slugs, _instance, std::forward(parameters)...); 109 | } 110 | private: 111 | /** 112 | * Alias for a wrapped callback 113 | */ 114 | using wrapped_callback = return_type(*)(const std::vector& slugs, void* instance, arguments&&... parameters); 115 | 116 | wrapped_callback _callback{}; // the callback to invoke 117 | void* _instance{}; // the instance to invoke on (empty for non-member functions) 118 | }; 119 | 120 | } 121 | -------------------------------------------------------------------------------- /include/router/path_map.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "path.h" 6 | 7 | 8 | namespace router { 9 | 10 | /** 11 | * A class mapping paths to something else 12 | */ 13 | template 14 | class path_map 15 | { 16 | public: 17 | using value_type = V; 18 | 19 | /** 20 | * Add a value to the map 21 | * 22 | * @param endpoint The endpoint to add 23 | * @param parameters The parameters to create the value 24 | */ 25 | template 26 | value_type& add(std::string_view endpoint, arguments&&... parameters) 27 | { 28 | // create the path to route 29 | path path { endpoint }; 30 | 31 | // does the endpoint contain a fixed prefix? 32 | if (path.prefix().empty()) { 33 | // there is no prefix to sort on so 34 | // we add this to the unsorted list 35 | return _unsorted_paths.emplace_back( 36 | std::piecewise_construct, 37 | std::forward_as_tuple(std::move(path)), 38 | std::forward_as_tuple(std::forward(parameters)...) 39 | ).second; 40 | } else { 41 | // find the element to insert before 42 | auto iter = std::lower_bound(begin(_prefixed_paths), end(_prefixed_paths), path, [](const auto& a, const auto& b) { 43 | // endpoints are sorted by prefix 44 | return std::get<0>(a).prefix() < b.prefix(); 45 | }); 46 | 47 | // add it to the sorted list before the found element 48 | return _prefixed_paths.emplace(iter, 49 | std::piecewise_construct, 50 | std::forward_as_tuple(std::move(path)), 51 | std::forward_as_tuple(std::forward(parameters)...) 52 | )->second; 53 | } 54 | } 55 | 56 | /** 57 | * Find an entry in the map 58 | * 59 | * @param slugs The slugs to fill if found 60 | * @param endpoint The endpoint to lookup 61 | * @return The found value, or a nullptr 62 | */ 63 | const value_type* find(std::vector& slugs, std::string_view endpoint) const noexcept 64 | { 65 | // find the first possibly matching entry by prefix 66 | auto iter = std::lower_bound(begin(_prefixed_paths), end(_prefixed_paths), endpoint, [](const auto& a, const auto& b) { 67 | // check whether the prefix comes before the given endpoint 68 | return std::get<0>(a).prefix() < b.substr(0, std::get<0>(a).prefix().size()); 69 | }); 70 | 71 | // try all valid matches 72 | while (iter != end(_prefixed_paths)) { 73 | // retrieve the path, callback and instance 74 | const auto& [path, value] = *iter; 75 | 76 | // if the prefix no longer matches we have exhausted all options 77 | if (!path.match_prefix(endpoint)) { 78 | // stop now to avoid expensive regex calls 79 | break; 80 | } 81 | 82 | // try to match the path to the given endpoint 83 | if (path.match(endpoint, slugs)) { 84 | // we matched the endpoint, return the handler 85 | return &value; 86 | } 87 | 88 | // move on to the next entry 89 | ++iter; 90 | } 91 | 92 | // none of the prefixed paths matched, so try the 93 | // unsorted paths without a known prefix 94 | for (const auto& [path, value] : _unsorted_paths) { 95 | // try to match the path to the given endpoint 96 | if (path.match(endpoint, slugs)) { 97 | // we matched the endpoint, return the handler 98 | return &value; 99 | } 100 | } 101 | 102 | // none of the paths matched 103 | return nullptr; 104 | } 105 | private: 106 | /** 107 | * The entry type we store inside the map, we store both the 108 | * path and the given value type together in a flat structure 109 | */ 110 | using entry = std::pair; 111 | 112 | std::vector _prefixed_paths; // a sorted list of paths with prefixes 113 | std::vector _unsorted_paths; // paths without a prefix, not sorted 114 | }; 115 | 116 | } 117 | -------------------------------------------------------------------------------- /include/router/proxy.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "variadic_lookup.h" 4 | #include "function_traits.h" 5 | #include "wrap_callback.h" 6 | #include "path_callback.h" 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | namespace router { 13 | 14 | /** 15 | * Undefined templated class for specializing 16 | * into a function-like template class 17 | */ 18 | template 19 | class proxy; 20 | 21 | /** 22 | * A proxy for forwarding a single route to a set of 23 | * different callbacks, depending on an extra method. 24 | */ 25 | template 26 | class proxy 27 | { 28 | public: 29 | /** 30 | * Alias for the callback type proxied to 31 | */ 32 | using callback_type = path_callback; 33 | 34 | /** 35 | * Retrieve an installed handler 36 | * 37 | * @param method The method to retrieve the handler for 38 | * @return The handler, or a nullptr if not set 39 | */ 40 | const callback_type& get(decltype(first) method) const noexcept 41 | { 42 | // the index to retrieve 43 | auto index = method_index(method); 44 | 45 | // retrieve the callback 46 | return _callbacks[index]; 47 | } 48 | 49 | /** 50 | * Set up a handler 51 | * 52 | * @tparam method The method to register under 53 | * @tparam callback The callback to register 54 | */ 55 | template 56 | std::enable_if_t, proxy&> 57 | set() noexcept 58 | { 59 | // the index to store under 60 | constexpr auto index = method_index(method); 61 | 62 | // wrap and store the callback 63 | _callbacks[index].template set(); 64 | 65 | // allow chaining 66 | return *this; 67 | } 68 | 69 | /** 70 | * Set up a handler 71 | * 72 | * @tparam method The method to register under 73 | * @tparam callback The callback to register 74 | * @param instance The instance to invoke the callback on 75 | */ 76 | template 77 | std::enable_if_t, proxy&> 78 | set(typename function_traits::member_type* instance) noexcept 79 | { 80 | // the index to store under 81 | constexpr auto index = method_index(method); 82 | 83 | // wrap and store the callback 84 | _callbacks[index].template set(instance); 85 | 86 | // allow chaining 87 | return *this; 88 | } 89 | private: 90 | /** 91 | * Retrieve the index for the method 92 | * in the list of methods 93 | * 94 | * @param method The method to lookup 95 | */ 96 | constexpr static std::size_t method_index(decltype(first) method) 97 | { 98 | return variadic_lookup<0, first, rest...>(method); 99 | } 100 | 101 | /** 102 | * The number of available options, note that we 103 | * add one to the count for the first argument 104 | */ 105 | constexpr static std::size_t count = 1 + sizeof...(rest); 106 | 107 | /** 108 | * All the registered callbacks 109 | */ 110 | std::array _callbacks{}; 111 | }; 112 | 113 | } 114 | -------------------------------------------------------------------------------- /include/router/slug.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | namespace router { 9 | 10 | /** 11 | * Class holding the data for a slug, 12 | * allowing to extract the data. 13 | */ 14 | class slug 15 | { 16 | public: 17 | /** 18 | * Constructor 19 | * 20 | * @param pattern The slug pattern, the pattern is consumed from the input 21 | */ 22 | slug(std::string_view& pattern) 23 | { 24 | // the pattern must include the slug opening character 25 | if (pattern.empty() || pattern[0] != '{') { 26 | // not a valid slug pattern, does not start with an opening brace 27 | throw std::logic_error{ "Missing slug opening character" }; 28 | } 29 | 30 | // track whether the last character was an escape character 31 | // and how many levels of curly braces we have entered 32 | bool last_char_was_escape { false }; 33 | std::size_t brace_nesting_level { 0 }; 34 | 35 | // read all characters until we hit the slug end 36 | for (std::size_t i{ 0 }; i < pattern.size(); ++i) { 37 | // did we find an escape character? 38 | if (pattern[i] == '\\') { 39 | // if the previous character was also an escape character 40 | // then we escaped the escape character (to get a literal 41 | // backslash), so we need to flip the state back to false 42 | // otherwise it's set to true. 43 | last_char_was_escape = !last_char_was_escape; 44 | continue; 45 | } 46 | 47 | // escaped characters aren't checked, they do not change the 48 | // brace nesting level, so we can skip further processing 49 | if (last_char_was_escape) { 50 | last_char_was_escape = false; 51 | continue; 52 | } 53 | 54 | // did we find another curly brace character? 55 | if (pattern[i] == '{') { 56 | // increase nesting level 57 | ++brace_nesting_level; 58 | } else if (pattern[i] == '}') { 59 | // decrease nesting level 60 | --brace_nesting_level; 61 | } 62 | 63 | // did we find the final, closing slug character? 64 | if (brace_nesting_level == 0) { 65 | // to get the regular expression we ignore the first 66 | // character from the pattern (the opening {) and the 67 | // final closing character (the }) 68 | auto expression = pattern.substr(1, i - 1); 69 | 70 | // initialize the regex and consume the data 71 | _pattern.assign(begin(expression), end(expression)); 72 | pattern.remove_prefix(i + 1); 73 | 74 | // the slug is complete, stop processing data 75 | break; 76 | } 77 | } 78 | 79 | // check if the slug was properly closed 80 | if (brace_nesting_level > 0) { 81 | // there are more opening braces than closing 82 | // braces, which means the pattern does not contain 83 | // the closing brace, we cannot create the regex 84 | throw std::logic_error{ "Unterminated slug, missing closing curly brace" }; 85 | } 86 | } 87 | 88 | /** 89 | * Match the slug against the given input 90 | * 91 | * @param input The input to test 92 | * @param output Set to the matched data 93 | * @return Whether the input matched the slug pattern 94 | */ 95 | bool match(std::string_view& input, std::string_view& output) const 96 | { 97 | // the match results to use 98 | using match_results = std::match_results; 99 | 100 | // search the input and get the results 101 | match_results matches { }; 102 | bool matched { std::regex_search(begin(input), end(input), matches, _pattern) }; 103 | 104 | // did we find a match and did it occur at the start of the input? 105 | if (matched && matches.position(0) == 0) { 106 | // store the match in the output 107 | output = input.substr(0, matches.length(0)); 108 | 109 | // consume the matched input 110 | input.remove_prefix(output.size()); 111 | return true; 112 | } 113 | 114 | // no match found, or the match was not at the start of the input 115 | return false; 116 | } 117 | 118 | /** 119 | * Find the beginning of slug data 120 | * in a given (sub)path. 121 | * 122 | * @param path The path to search in 123 | * @return index inside the path, or std::string_view::npos 124 | */ 125 | constexpr static std::size_t find_start(std::string_view path) noexcept 126 | { 127 | // the iterator to the slug inside the path, and the position to continue our search 128 | // find the slug character inside the path 129 | auto position = path.find_first_of('{'); 130 | 131 | // keep going until we find a match or run out of data 132 | while (position != std::string_view::npos) { 133 | // if the match is the first character in the path 134 | // there can be no escape character before it 135 | if (position == 0) { 136 | break; 137 | } 138 | 139 | // check whether the match is not escaped by a backslash 140 | if (path[position - 1] != '\\') { 141 | break; 142 | } 143 | 144 | // the slug point we found was escaped by a backslash, 145 | // so we search the remainder of the path 146 | position = path.find_first_of('{', position + 1); 147 | } 148 | 149 | // return the position of the slug character 150 | return position; 151 | } 152 | private: 153 | std::regex _pattern; // the pattern to match for this slug 154 | }; 155 | 156 | } 157 | -------------------------------------------------------------------------------- /include/router/table.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "path.h" 5 | #include "proxy.h" 6 | #include "path_map.h" 7 | #include "path_callback.h" 8 | #include "function_traits.h" 9 | 10 | 11 | namespace router { 12 | 13 | /** 14 | * Undefined templated class for specializing 15 | * into a function-like template class 16 | */ 17 | template 18 | class table; 19 | 20 | /** 21 | * A routing table, containing paths to route and mapping 22 | * them to their associated callbacks. 23 | */ 24 | template 25 | class table 26 | { 27 | public: 28 | /** 29 | * Add an endpoint to the routing table 30 | * 31 | * @tparam callback The callback to route to 32 | * @param endpoint The path to add 33 | */ 34 | template 35 | std::enable_if_t> 36 | add(std::string_view endpoint) 37 | { 38 | // add the endpoint using the wrapped callback, since 39 | // the callback is not a member function we need no instance 40 | _paths.add(endpoint, in_place_value{}); 41 | } 42 | 43 | /** 44 | * Add an endpoint to the routing table 45 | * 46 | * @tparam callback The callback to route to 47 | * @param endpoint The path to add 48 | * @param instance The instance to invoke the callback on 49 | */ 50 | template 51 | std::enable_if_t> 52 | add(std::string_view endpoint, typename function_traits::member_type* instance) 53 | { 54 | // add the endpoint using the wrapped callback 55 | _paths.add(endpoint, in_place_value{}, instance); 56 | } 57 | 58 | /** 59 | * Set a handler for endpoints that are not found 60 | * 61 | * @tparam callback The callback to route to 62 | */ 63 | template 64 | std::enable_if_t> 65 | set_not_found() 66 | { 67 | // store the function pointer, there is no instance 68 | _not_found_handler.template set(); 69 | } 70 | 71 | /** 72 | * Set a handler for endpoints that are not found 73 | * 74 | * @tparam callback The callback to route to 75 | * @param instance The instance to invoke the callback on 76 | */ 77 | template 78 | std::enable_if_t> 79 | set_not_found(typename function_traits::member_type* instance) 80 | { 81 | // store the function pointer and instance 82 | _not_found_handler.template set(instance); 83 | } 84 | 85 | /** 86 | * Check whether a given endpoint can be routed 87 | * 88 | * Note that this function ignores the not-found 89 | * handler, since this would otherwise make this 90 | * function return true unconditionally. 91 | * 92 | * @param endpoint The endpoint to check 93 | * @return Whether the endpoint can be routed 94 | */ 95 | bool routable(std::string_view endpoint) const noexcept 96 | { 97 | // find the handler for the given endpoint 98 | auto callback= get_handler(endpoint); 99 | 100 | // check whether the callback is valid 101 | return callback.valid(); 102 | } 103 | 104 | /** 105 | * Check whether a fallback handler is installed 106 | * 107 | * If a fallback handler is installed and no endpoint is 108 | * available when routing, the fallback handler is used. 109 | * This also means that routing will never fail due to a 110 | * missing endpoint. 111 | * 112 | * @return Whether a fallback handler is installed 113 | */ 114 | bool has_not_found_handler() const noexcept 115 | { 116 | return _not_found_handler.valid(); 117 | } 118 | 119 | /** 120 | * Route a request to one of the callbacks 121 | * 122 | * @param endpoint The endpoint to route 123 | * @param parameters The arguments to give to the callback 124 | * @return The result of the callback 125 | * @throws std::out_of_range 126 | */ 127 | return_type route(std::string_view endpoint, arguments... parameters) const 128 | { 129 | // find the handler for the given endpoint 130 | if (auto callback= get_handler(endpoint); callback.valid()) { 131 | // invoke the callback 132 | return callback(slugs(), std::forward(parameters)...); 133 | } 134 | 135 | // do we have a handler for endpoints that aren't registered 136 | if (_not_found_handler) { 137 | // invoke the handler 138 | return _not_found_handler({}, std::forward(parameters)...); 139 | } 140 | 141 | // none of the paths matched 142 | throw std::out_of_range{ "Route not matched" }; 143 | } 144 | private: 145 | /** 146 | * Alias for the callback type used by the routing table 147 | */ 148 | using callback_type = path_callback; 149 | 150 | /** 151 | * Retrieve the handler for a specific endpoint 152 | * 153 | * If a valid handler is found (i.e. the result contains a valid 154 | * callback) then _slugs is filled with the found slug data. 155 | * 156 | * @param endpoint The endpoint to find 157 | * @return The found match, which may be invalid 158 | */ 159 | callback_type get_handler(std::string_view endpoint) const noexcept 160 | { 161 | if (auto* handler = _paths.find(slugs(), endpoint); handler != nullptr) { 162 | return *handler; 163 | } else { 164 | return {}; 165 | } 166 | } 167 | 168 | /** 169 | * Retrieve the slug data 170 | * 171 | * @note This data is stored locally per thread to ensure 172 | * that the table remains thread-safe, and can re-use 173 | * allocated data between calls. 174 | * 175 | * @return The slug data 176 | */ 177 | static std::vector& slugs() noexcept 178 | { 179 | // the slugs to cache 180 | thread_local std::vector slugs; 181 | return slugs; 182 | } 183 | 184 | path_map _paths; // all registered paths in the table 185 | callback_type _not_found_handler; // the optional handler for paths not found 186 | }; 187 | 188 | /** 189 | * A routing table, containing paths to route and mapping 190 | * them to a proxy for further processing 191 | */ 192 | template 193 | class table> 194 | { 195 | public: 196 | /** 197 | * The proxy we route to 198 | */ 199 | using proxy_type = proxy; 200 | 201 | /** 202 | * Add an endpoint to the routing table, the callbacks 203 | * can be registered on the returned proxy 204 | * 205 | * @param endpoint The path to add 206 | */ 207 | proxy_type& add(std::string_view endpoint) 208 | { 209 | // add the endpoint to create a proxy 210 | return _paths.add(endpoint); 211 | } 212 | 213 | /** 214 | * Set a handler for endpoints that are not found 215 | * 216 | * @tparam callback The callback to route to 217 | */ 218 | template 219 | std::enable_if_t> 220 | set_not_found() 221 | { 222 | // store the function pointer, there is no instance 223 | _not_found_handler.template set(); 224 | } 225 | 226 | /** 227 | * Set a handler for endpoints that are not found 228 | * 229 | * @tparam callback The callback to route to 230 | * @param instance The instance to invoke the callback on 231 | */ 232 | template 233 | std::enable_if_t> 234 | set_not_found(typename function_traits::member_type* instance) 235 | { 236 | // store the function pointer and instance 237 | _not_found_handler.template set(instance); 238 | } 239 | 240 | /** 241 | * Set a handler for endpoints without a method handler 242 | * 243 | * @tparam callback The callback to invoke 244 | */ 245 | template 246 | std::enable_if_t> 247 | set_not_proxied() 248 | { 249 | // store the function pointer, there is no instance 250 | _not_proxied_handler.template set(); 251 | } 252 | 253 | /** 254 | * Set a handler for endpoints without a method handler 255 | * 256 | * @tparam callback The callback to invoke 257 | * @param instance The instance to invoke the callback on 258 | */ 259 | template 260 | std::enable_if_t> 261 | set_not_proxied(typename function_traits::member_type* instance) 262 | { 263 | // store the function pointer and instance 264 | _not_proxied_handler.template set(instance); 265 | } 266 | 267 | /** 268 | * Check whether a given endpoint can be routed 269 | * 270 | * Note that this function ignores the not-found 271 | * handler, since this would otherwise make this 272 | * function return true unconditionally. 273 | * 274 | * @param endpoint The endpoint to check 275 | * @return Whether the endpoint can be routed 276 | */ 277 | bool routable(std::string_view endpoint) const noexcept 278 | { 279 | // find the handler for the given endpoint 280 | return _paths.find(slugs(), endpoint) != nullptr; 281 | } 282 | 283 | /** 284 | * Check whether a fallback handler is installed 285 | * 286 | * If a fallback handler is installed and no endpoint is 287 | * available when routing, the fallback handler is used. 288 | * This also means that routing will never fail due to a 289 | * missing endpoint. 290 | * 291 | * @return Whether a fallback handler is installed 292 | */ 293 | bool has_not_found_handler() const noexcept 294 | { 295 | return _not_found_handler.valid(); 296 | } 297 | 298 | /** 299 | * Route a request to one of the callbacks 300 | * 301 | * @param endpoint The endpoint to route 302 | * @param method The method to proxy to 303 | * @param parameters The arguments to give to the callback 304 | * @return The result of the callback 305 | * @throws std::out_of_range 306 | */ 307 | return_type route(std::string_view endpoint, decltype(first) method, arguments... parameters) const 308 | { 309 | // find the handler for the given endpoint 310 | if (auto proxy = _paths.find(slugs(), endpoint); proxy != nullptr) { 311 | // do we have a handler for the method 312 | if (proxy->get(method).valid()) { 313 | // invoke the callback 314 | return proxy->get(method)(slugs(), std::forward(parameters)...); 315 | } else if (_not_proxied_handler.valid()) { 316 | // invoke the missing-method handler 317 | return _not_proxied_handler({}, std::forward(parameters)...); 318 | } 319 | } 320 | 321 | // do we have a handler for endpoints that aren't registered 322 | if (_not_found_handler) { 323 | // invoke the handler 324 | return _not_found_handler({}, std::forward(parameters)...); 325 | } 326 | 327 | // none of the paths matched 328 | throw std::out_of_range{ "Route not matched" }; 329 | } 330 | private: 331 | /** 332 | * Alias for the callback type used by the routing table 333 | */ 334 | using callback_type = path_callback; 335 | 336 | /** 337 | * Retrieve the slug data 338 | * 339 | * @note This data is stored locally per thread to ensure 340 | * that the table remains thread-safe, and can re-use 341 | * allocated data between calls. 342 | * 343 | * @return The slug data 344 | */ 345 | static std::vector& slugs() noexcept 346 | { 347 | // the slugs to cache 348 | thread_local std::vector slugs; 349 | return slugs; 350 | } 351 | 352 | path_map _paths; // all registered paths in the table 353 | callback_type _not_found_handler; // the optional handler for paths not found 354 | callback_type _not_proxied_handler; // the optional handler for when a method is not proxied 355 | }; 356 | 357 | } 358 | -------------------------------------------------------------------------------- /include/router/tuple_slice.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | 6 | namespace router { 7 | 8 | /** 9 | * Unimplemented base template 10 | * for tuple slice. It is implemented 11 | * in various specialisations below. 12 | */ 13 | template 14 | struct tuple_slice; 15 | 16 | /** 17 | * Specialisation when at least one 18 | * element has to still be removed 19 | */ 20 | template 21 | struct tuple_slice 22 | { 23 | using type = typename tuple_slice::type; 24 | }; 25 | 26 | /** 27 | * Specialisation for when there are no 28 | * types to be removed 29 | */ 30 | template <> 31 | struct tuple_slice<0> 32 | { 33 | using type = std::tuple<>; 34 | }; 35 | 36 | template 37 | struct tuple_slice<0, element> 38 | { 39 | using type = std::tuple; 40 | }; 41 | 42 | template 43 | struct tuple_slice<0, element, remaining...> 44 | { 45 | using type = std::tuple; 46 | }; 47 | 48 | /** 49 | * Type alias for tuple slice 50 | */ 51 | template 52 | using tuple_slice_t = typename tuple_slice::type; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /include/router/variables.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "impl/variables.h" 8 | #include "fields.h" 9 | 10 | 11 | namespace router { 12 | 13 | /** 14 | * Templated class describing bindings between 15 | * slug data and struct members. 16 | * 17 | * The struct is templated on the struct we need 18 | * to bind to, as well as all the members that 19 | * need binding. 20 | */ 21 | template 22 | struct dto 23 | { 24 | /** 25 | * Bind a slug to a field. 26 | * 27 | * Fields are bound to slugs in order of appearance. The first field 28 | * bound will be filled with the data from the first slug, the second 29 | * field with that of the second slug, etc... 30 | * 31 | * @tparam field The field to bind 32 | */ 33 | template 34 | using bind = dto; 35 | 36 | /** 37 | * Get the number of bound fields 38 | * 39 | * @return The number of fields bound to the structure 40 | */ 41 | constexpr static std::size_t size() noexcept 42 | { 43 | return sizeof...(members); 44 | } 45 | 46 | /** 47 | * Parse all slug data into the given 48 | * data transfer object. 49 | * 50 | * @param slugs The slug data to parse 51 | * @param output The object to fill 52 | */ 53 | static void to_dto(const std::vector& slugs, data_type& output) 54 | { 55 | // ensure the number of slugs is correct 56 | if (slugs.size() != size()) { 57 | // we cannot parse the data 58 | throw std::logic_error{ "Cannot convert slugs to dto: slug count mismatch" }; 59 | } 60 | 61 | // iterator to use in the fold expression 62 | auto iter = begin(slugs); 63 | 64 | // fold the fields into the output object 65 | (process_field(*iter++, output.*members), ...); 66 | } 67 | }; 68 | 69 | /** 70 | * Type trait for a type not implementing 71 | * the dto-specific requirements. 72 | */ 73 | template 74 | struct is_dto_type : std::false_type {}; 75 | 76 | /** 77 | * Type trait for a valid dto-compatible type 78 | */ 79 | template 80 | struct is_dto_type&>(), 89 | std::declval() 90 | )) 91 | >> 92 | >> : std::true_type {}; 93 | 94 | /** 95 | * Value alias for dto type deduction 96 | */ 97 | template 98 | constexpr bool is_dto_type_v = is_dto_type::value; 99 | 100 | 101 | /** 102 | * Type trait for an std::tuple of processable fields 103 | */ 104 | template 105 | struct is_dto_tuple : std::false_type {}; 106 | 107 | /** 108 | * Type trait for a valid tuple match 109 | */ 110 | template 111 | struct is_dto_tuple> : 112 | std::integral_constant::value)> {}; 113 | 114 | /** 115 | * Value alias for dto tuple deduction 116 | */ 117 | template 118 | constexpr bool is_dto_tuple_v = is_dto_tuple::value; 119 | 120 | /** 121 | * Read a vector of slugs and parse them 122 | * into the given dto type 123 | */ 124 | template 125 | std::enable_if_t> 126 | to_dto(const std::vector& slugs, data_type& output) 127 | { 128 | // invoke the conversion routine on the types dto alias 129 | data_type::dto::to_dto(slugs, output); 130 | } 131 | 132 | /** 133 | * Read a vector of slugs and parse them 134 | * into the given tuple 135 | */ 136 | template 137 | std::enable_if_t>> 138 | to_dto(const std::vector& slugs, std::tuple& output) 139 | { 140 | // create integer sequence for retrieving types from the tuple 141 | impl::to_dto(slugs, output, std::make_index_sequence()); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /include/router/variadic_lookup.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | 7 | namespace router { 8 | 9 | template 10 | constexpr std::size_t variadic_lookup(decltype(straw) needle) 11 | { 12 | if (needle == straw) { 13 | return index; 14 | } else if constexpr(sizeof...(haystack) > 0) { 15 | return variadic_lookup(needle); 16 | } else { 17 | throw std::invalid_argument{ "Lookup failed: needle not found in variadic list" }; 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /include/router/wrap_callback.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "function_traits.h" 4 | #include "variables.h" 5 | #include 6 | #include 7 | 8 | 9 | namespace router { 10 | 11 | /** 12 | * Wrap a callback to create a uniform handler 13 | * 14 | * @tparam callback The callback to wrap 15 | * @param slugs The slug data from the target 16 | * @param instance The instance to invoke the callback on 17 | * @param parameters Additional arguments to pass to the callback 18 | */ 19 | template 20 | return_type wrap_callback(const std::vector& slugs, void* instance, arguments&&... parameters) 21 | { 22 | // the number of arguments our callback function takes 23 | // ignoring the extra variable parameter it may take 24 | constexpr std::size_t arity = sizeof...(arguments); 25 | 26 | // traits for the callback function, and the data type used for the variables 27 | using traits = function_traits; 28 | 29 | // does the callback require slug parsing? 30 | if constexpr (traits::arity == arity) { 31 | // is it a member function? 32 | if constexpr (!traits::is_member_function) { 33 | // it is not, we can invoke the callback directly 34 | return callback(std::forward(parameters)...); 35 | } else { 36 | // invoke the function on the given instance 37 | return (static_cast(instance)->*callback)(std::forward(parameters)...); 38 | } 39 | } else if constexpr (traits::arity == arity + 1 && (is_dto_type_v>> || is_dto_tuple_v>>)) { 40 | // determine the type used for the slug data, this comes as the optional 41 | // last parameter the function may take, elements are zero-based 42 | using variable_type = std::remove_reference_t>; 43 | 44 | // create the variables to be filled and try to match the target 45 | variable_type variables{}; 46 | 47 | // parse the variables 48 | to_dto(slugs, variables); 49 | 50 | // is it a member function? 51 | if constexpr (!traits::is_member_function) { 52 | // invoke the wrapped callback and return the result 53 | return callback(std::forward(parameters)..., std::move(variables)); 54 | } else { 55 | // invoke the function on the given instance 56 | return (static_cast(instance)->*callback)(std::forward(parameters)..., std::move(variables)); 57 | } 58 | } else { 59 | // the variable type is a tuple of the slug arguments 60 | // which is unpacked later using std::apply 61 | using variable_type = typename traits::template arguments_slice; 62 | 63 | // create the variables to be filled and try to match the target 64 | variable_type variables{}; 65 | 66 | // parse the variables 67 | to_dto(slugs, variables); 68 | 69 | // is it a member function? 70 | if constexpr (!traits::is_member_function) { 71 | // invoke the wrapped callback and return the result 72 | return std::apply(callback, std::tuple_cat( 73 | std::forward_as_tuple(std::forward(parameters)...), 74 | std::move(variables) 75 | )); 76 | } else { 77 | // invoke the function on the given instance 78 | return std::apply(callback, std::tuple_cat( 79 | std::forward_as_tuple(static_cast(instance)), 80 | std::forward_as_tuple(std::forward(parameters)...), 81 | std::move(variables) 82 | )); 83 | } 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/") 2 | 3 | find_package(Runtime REQUIRED) 4 | 5 | set(test-sources 6 | main.cpp 7 | path.cpp 8 | slug.cpp 9 | table.cpp 10 | variables.cpp 11 | tuple_slice.cpp 12 | proxy_table.cpp 13 | ) 14 | 15 | add_executable(test ${test-sources}) 16 | target_link_libraries(test router::router) 17 | target_link_libraries(test cxx::runtime) 18 | -------------------------------------------------------------------------------- /tests/cmake/Modules/FindRuntime.cmake: -------------------------------------------------------------------------------- 1 | #[=======================================================================[.rst: 2 | 3 | FindRuntime 4 | ########### 5 | 6 | This module finds required runtime libraries. Use the 7 | :imp-target:`cxx::runtime` imported target to link to it. 8 | 9 | This script will first try to link without specifying a 10 | custom runtime, only if this fails will other runtimes 11 | be tried. If no custom runtimes are required, linking to 12 | :imp-target:`cxx::runtime` will not change linker flags. 13 | 14 | #]=======================================================================] 15 | 16 | 17 | if (TARGET cxx::runtime) 18 | # This module has already been processed. Don't do it again. 19 | return() 20 | endif() 21 | 22 | include(CMakePushCheckState) 23 | include(CheckCXXSourceCompiles) 24 | 25 | cmake_push_check_state() 26 | 27 | # All of our tests require C++17 or later 28 | set(CMAKE_CXX_STANDARD 17) 29 | 30 | # the test script to compile 31 | set(code [[ 32 | #include 33 | #include 34 | 35 | int main() 36 | { 37 | std::size_t number; 38 | std::from_chars("100", "100", number, 10); 39 | } 40 | ]]) 41 | 42 | # can we link in one way or another? 43 | set(can_link TRUE) 44 | 45 | # first check if we can link without specifying a custom runtime 46 | check_cxx_source_compiles("${code}" CXX_RUNTIME_NO_LINK_NEEDED) 47 | 48 | # can we compile without adding a runtime? 49 | if (NOT CXX_RUNTIME_NO_LINK_NEEDED) 50 | # add the llvm runtime and retry the check 51 | # note: CMAKE_REQUIRED_LINK_OPTIONS would be better, but 52 | # it's only supported in CMake 3.14+ 53 | set(CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES} -rtlib=compiler-rt -lgcc_s) 54 | 55 | # now compile again to see if we can now build our program 56 | check_cxx_source_compiles("${code}" CXX_RUNTIME_COMPILER_RT) 57 | 58 | # if linking fails again we simply cannot link 59 | set(can_link ${CXX_RUNTIME_COMPILER_RT}) 60 | endif() 61 | 62 | cmake_pop_check_state() 63 | 64 | # did we manage to link the example program 65 | if (can_link) 66 | # create the target 67 | add_library(cxx::runtime INTERFACE IMPORTED) 68 | 69 | # do we need to link to clangs compiler runtime? 70 | if (CXX_RUNTIME_COMPILER_RT) 71 | # add the linker options 72 | target_link_options(cxx::runtime INTERFACE -rtlib=compiler-rt) 73 | target_link_libraries(cxx::runtime INTERFACE -lgcc_s) 74 | endif() 75 | endif() 76 | 77 | # if we can link we have found the runtime 78 | set(Runtime_FOUND ${can_link} CACHE BOOL "TRUE if we can compile and link a program using cxx::runtime" FORCE) 79 | 80 | # did we fail to find the required module? 81 | if(Runtime_FIND_REQUIRED AND NOT Runtime_FOUND) 82 | message(FATAL_ERROR "Cannot Compile simple program using cxx::runtime") 83 | endif() 84 | -------------------------------------------------------------------------------- /tests/main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include "catch2.hpp" 3 | -------------------------------------------------------------------------------- /tests/path.cpp: -------------------------------------------------------------------------------- 1 | #include "catch2.hpp" 2 | #include 3 | 4 | 5 | TEST_CASE("paths should correctly identify and parse slugs", "[path]") 6 | { 7 | // a container to hold slug matches 8 | std::vector slugs; 9 | 10 | SECTION("path without any slugs") { 11 | // no slug start inside the part, this is a very simple path 12 | std::string_view slug_free { "/simple/path/without/slugs" }; 13 | router::path path { slug_free }; 14 | 15 | // since there are no slugs, both match_prefix 16 | // and match should behave exactly the same 17 | REQUIRE(path.match_prefix(slug_free) == true); 18 | REQUIRE(path.match(slug_free, slugs) == true); 19 | 20 | // popping a character off prevents matching, because 21 | // a path without slugs has the whole path as prefix 22 | slug_free.remove_suffix(1); 23 | 24 | // ensure the path no longer matches 25 | REQUIRE(path.match_prefix(slug_free) == false); 26 | REQUIRE(path.match(slug_free, slugs) == false); 27 | } 28 | 29 | SECTION("path with a simple slug") { 30 | // path with only a simple slug, with a very simple regex 31 | router::path path { "/test/{\\d+}/test" }; 32 | 33 | // testing prefix only works when all data up to the first prefix is available 34 | REQUIRE(path.match_prefix("/test/no-longer-inprefix") == true); 35 | REQUIRE(path.match_prefix("/testing/10/test") == false); 36 | 37 | // if the data _after_ the slug is invalid, the 38 | // match should still fail as well 39 | REQUIRE(path.match("/test/10/testing", slugs) == false); 40 | 41 | // check whether the regex is validated correctly 42 | REQUIRE(path.match("/test/ten/test", slugs) == false); 43 | REQUIRE(path.match("/test//test", slugs) == false); 44 | REQUIRE(path.match("/test/test", slugs) == false); 45 | 46 | // finally test correct input 47 | REQUIRE(path.match("/test/10/test", slugs) == true); 48 | 49 | // the slug should have been correctly parsed 50 | REQUIRE(slugs.size() == 1); 51 | REQUIRE(slugs[0] == "10"); 52 | } 53 | 54 | SECTION("slug with embedded curly braces") { 55 | // this path has curly braces inside the slug regex 56 | router::path path { "/test/{\\d{2}}/test" }; 57 | 58 | // this should only match on data with the correct number 59 | // of numbers inside the slug data 60 | REQUIRE(path.match("/test/10/test", slugs) == true); 61 | REQUIRE(path.match("/test/1/test", slugs) == false); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/proxy_table.cpp: -------------------------------------------------------------------------------- 1 | #include "catch2.hpp" 2 | #include 3 | #include 4 | 5 | 6 | TEST_CASE("paths and methods can be matched", "[path-method]") 7 | { 8 | // the methods to support 9 | enum class method 10 | { 11 | get, 12 | put, 13 | post 14 | }; 15 | 16 | // an empty table to test with 17 | router::table> table; 18 | 19 | SECTION("never route on an empty table") { 20 | // the table is empty, so all attempts to route 21 | // will yield a out of range condition 22 | REQUIRE_THROWS_AS(table.route("/test", method::get), std::out_of_range); 23 | REQUIRE(table.routable("/test") == false); 24 | REQUIRE(table.has_not_found_handler() == false); 25 | } 26 | 27 | SECTION("404 handler") { 28 | struct not_found_handler 29 | { 30 | void handle_404() { handler_invoked = true; } 31 | bool handler_invoked{ false }; 32 | }; 33 | 34 | not_found_handler tester; 35 | 36 | table.set_not_found<¬_found_handler::handle_404>(&tester); 37 | table.route("/wherever/not/found", method::get); 38 | REQUIRE(table.routable("/wherever/not/found") == false); 39 | REQUIRE(table.has_not_found_handler() == true); 40 | 41 | REQUIRE(tester.handler_invoked == true); 42 | } 43 | 44 | SECTION("missing method on valid route") { 45 | struct callback_tester 46 | { 47 | void handle_404() { not_found_invoked = true; } 48 | bool not_found_invoked{ false }; 49 | 50 | void handle_405() { method_not_allowed = true; } 51 | bool method_not_allowed{ false }; 52 | 53 | void handle_get() { get_invoked = true; } 54 | bool get_invoked{ false }; 55 | }; 56 | 57 | callback_tester tester; 58 | 59 | table.add("/callback") 60 | .set(&tester); 61 | 62 | table.set_not_found<&callback_tester::handle_404>(&tester); 63 | table.route("/callback", method::post); 64 | REQUIRE(table.routable("/callback") == true); 65 | REQUIRE(table.has_not_found_handler() == true); 66 | 67 | REQUIRE(tester.not_found_invoked == true); 68 | 69 | // now set up a handler for missing method specifically 70 | table.set_not_proxied<&callback_tester::handle_405>(&tester); 71 | table.route("/callback", method::post); 72 | 73 | REQUIRE(tester.method_not_allowed == true); 74 | } 75 | 76 | SECTION("route call to correct method") { 77 | struct callback_tester 78 | { 79 | void handle_404() { not_found_invoked = true; } 80 | bool not_found_invoked{ false }; 81 | 82 | void handle_405() { method_not_allowed = true; } 83 | bool method_not_allowed{ false }; 84 | 85 | void handle_get() { get_invoked = true; } 86 | bool get_invoked{ false }; 87 | 88 | void handle_post() { post_invoked = true; } 89 | bool post_invoked{ false }; 90 | }; 91 | 92 | callback_tester tester; 93 | 94 | table.add("/callback") 95 | .set(&tester) 96 | .set(&tester); 97 | 98 | table.set_not_found<&callback_tester::handle_404>(&tester); 99 | table.route("/callback", method::put); 100 | REQUIRE(table.routable("/callback") == true); 101 | REQUIRE(table.has_not_found_handler() == true); 102 | 103 | REQUIRE(tester.not_found_invoked == true); 104 | 105 | // now set up a handler for missing method specifically 106 | table.set_not_proxied<&callback_tester::handle_405>(&tester); 107 | table.route("/callback", method::put); 108 | 109 | REQUIRE(tester.method_not_allowed == true); 110 | 111 | table.route("/callback", method::post); 112 | REQUIRE(tester.post_invoked == true); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /tests/slug.cpp: -------------------------------------------------------------------------------- 1 | #include "catch2.hpp" 2 | #include 3 | 4 | TEST_CASE("slugs should correctly detect start and end", "[slug]") 5 | { 6 | SECTION("slug with invalid starting character") { 7 | std::string_view pattern{ "\\d}" }; 8 | REQUIRE_THROWS_AS(router::slug{ pattern }, std::logic_error); 9 | } 10 | 11 | SECTION("unterminated slug data") { 12 | std::string_view pattern{ "{\\d" }; 13 | REQUIRE_THROWS_AS(router::slug{ pattern }, std::logic_error); 14 | } 15 | 16 | SECTION("slug-only data") { 17 | std::string_view pattern { "{\\d}" }; 18 | router::slug slug { pattern }; 19 | 20 | REQUIRE(pattern.empty()); 21 | } 22 | 23 | SECTION("nested curly braces") { 24 | std::string_view pattern { "{\\d{2}}" }; 25 | router::slug slug { pattern }; 26 | 27 | REQUIRE(pattern.empty()); 28 | } 29 | 30 | SECTION("slug with trailing suffix") { 31 | std::string_view pattern { "{\\d}/test" }; 32 | router::slug slug { pattern }; 33 | 34 | REQUIRE(pattern == "/test"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/table.cpp: -------------------------------------------------------------------------------- 1 | #include "catch2.hpp" 2 | #include 3 | #include 4 | 5 | static bool free_callback_invoked{ false }; 6 | 7 | void free_callback() 8 | { 9 | free_callback_invoked = true; 10 | }; 11 | 12 | 13 | TEST_CASE("paths can be matched", "[path]") 14 | { 15 | // an empty table to test with 16 | router::table table; 17 | 18 | SECTION("never route on an empty table") { 19 | // the table is empty, so all attempts to route 20 | // will yield a out of range condition 21 | REQUIRE_THROWS_AS(table.route("/test"), std::out_of_range); 22 | REQUIRE(table.routable("/test") == false); 23 | REQUIRE(table.has_not_found_handler() == false); 24 | } 25 | 26 | SECTION("404 handler") { 27 | struct not_found_handler 28 | { 29 | void handle_404() { handler_invoked = true; } 30 | bool handler_invoked{ false }; 31 | }; 32 | 33 | not_found_handler tester; 34 | 35 | table.set_not_found<¬_found_handler::handle_404>(&tester); 36 | table.route("/wherever/not/found"); 37 | REQUIRE(table.routable("/wherever/not/found") == false); 38 | REQUIRE(table.has_not_found_handler() == true); 39 | 40 | REQUIRE(tester.handler_invoked == true); 41 | } 42 | 43 | SECTION("invoking a free function") { 44 | table.add<&free_callback>("/callback"); 45 | 46 | REQUIRE(free_callback_invoked == false); 47 | 48 | table.route("/callback"); 49 | REQUIRE(table.routable("/callback") == true); 50 | REQUIRE(table.routable("/some/other/path") == false); 51 | 52 | REQUIRE(free_callback_invoked == true); 53 | } 54 | 55 | SECTION("invoking a member function") { 56 | struct callback_tester 57 | { 58 | void callback1() { callback1_invoked = true; } 59 | void callback2() noexcept { callback2_invoked = true; } 60 | void callback3() const { callback3_invoked = true; } 61 | void callback4() const noexcept { callback4_invoked = true; } 62 | 63 | bool callback1_invoked { false }; 64 | bool callback2_invoked { false }; 65 | mutable bool callback3_invoked { false }; 66 | mutable bool callback4_invoked { false }; 67 | }; 68 | 69 | callback_tester tester; 70 | 71 | table.add<&callback_tester::callback1>("/callback/1", &tester); 72 | table.add<&callback_tester::callback2>("/callback/2", &tester); 73 | table.add<&callback_tester::callback3>("/callback/3", &tester); 74 | table.add<&callback_tester::callback4>("/callback/4", &tester); 75 | 76 | REQUIRE(tester.callback1_invoked == false); 77 | REQUIRE(tester.callback2_invoked == false); 78 | REQUIRE(tester.callback3_invoked == false); 79 | REQUIRE(tester.callback4_invoked == false); 80 | 81 | table.route("/callback/1"); 82 | 83 | REQUIRE(tester.callback1_invoked == true); 84 | REQUIRE(tester.callback2_invoked == false); 85 | REQUIRE(tester.callback3_invoked == false); 86 | REQUIRE(tester.callback4_invoked == false); 87 | 88 | table.route("/callback/2"); 89 | 90 | REQUIRE(tester.callback1_invoked == true); 91 | REQUIRE(tester.callback2_invoked == true); 92 | REQUIRE(tester.callback3_invoked == false); 93 | REQUIRE(tester.callback4_invoked == false); 94 | 95 | table.route("/callback/3"); 96 | 97 | REQUIRE(tester.callback1_invoked == true); 98 | REQUIRE(tester.callback2_invoked == true); 99 | REQUIRE(tester.callback3_invoked == true); 100 | REQUIRE(tester.callback4_invoked == false); 101 | 102 | table.route("/callback/4"); 103 | 104 | REQUIRE(tester.callback1_invoked == true); 105 | REQUIRE(tester.callback2_invoked == true); 106 | REQUIRE(tester.callback3_invoked == true); 107 | REQUIRE(tester.callback4_invoked == true); 108 | } 109 | 110 | SECTION("invoking a member function with slug") { 111 | struct slug_data 112 | { 113 | std::size_t number; 114 | std::string slug; 115 | 116 | using dto = router::dto 117 | ::bind<&slug_data::number> 118 | ::bind<&slug_data::slug>; 119 | }; 120 | 121 | struct callback_tester 122 | { 123 | void callback1(std::size_t number, std::string&& slug) 124 | { 125 | _number = number; 126 | _slug = std::move(slug); 127 | } 128 | 129 | void callback2(std::tuple&& slugs) 130 | { 131 | _number = std::get<0>(slugs); 132 | _slug = std::move(std::get<1>(slugs)); 133 | } 134 | 135 | void callback3(slug_data&& slugs) 136 | { 137 | _number = slugs.number; 138 | _slug = std::move(slugs.slug); 139 | } 140 | 141 | std::size_t number() const noexcept { return _number; } 142 | const std::string& slug() const noexcept { return _slug; } 143 | 144 | std::size_t _number; 145 | std::string _slug; 146 | }; 147 | 148 | callback_tester tester; 149 | 150 | table.add<&callback_tester::callback1>("/callback1/{\\d+}/{\\w+}", &tester); 151 | table.add<&callback_tester::callback2>("/callback2/{\\d+}/{\\w+}", &tester); 152 | table.add<&callback_tester::callback3>("/callback3/{\\d+}/{\\w+}", &tester); 153 | 154 | table.route("/callback1/100/hello_world"); 155 | 156 | REQUIRE(tester.number() == 100); 157 | REQUIRE(tester.slug() == "hello_world"); 158 | 159 | table.route("/callback2/200/hello_callback"); 160 | 161 | REQUIRE(tester.number() == 200); 162 | REQUIRE(tester.slug() == "hello_callback"); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/tuple_slice.cpp: -------------------------------------------------------------------------------- 1 | #include "catch2.hpp" 2 | #include 3 | 4 | 5 | TEST_CASE("We should be able to deduce a slice of a tuple") 6 | { 7 | // check that slicing 0 elements works 8 | static_assert( 9 | std::is_same_v< 10 | typename router::tuple_slice<0, std::size_t>::type, 11 | std::tuple 12 | >, "Slicing 0 elements from tuple breaks" 13 | ); 14 | 15 | // now check whether slicing off 2 out of 4 elements 16 | static_assert( 17 | std::is_same_v< 18 | typename router::tuple_slice<2, std::size_t, uint64_t, float, double>::type, 19 | std::tuple 20 | >, "Slicing 2 out of 4 elements breaks" 21 | ); 22 | 23 | // check that empty parameter list works 24 | static_assert( 25 | std::is_same_v< 26 | typename router::tuple_slice<0>::type, 27 | std::tuple<> 28 | >, "Slicing an empty tuple breaks" 29 | ); 30 | 31 | // check that consuming all types works 32 | static_assert( 33 | std::is_same_v< 34 | typename router::tuple_slice<1, float>::type, 35 | std::tuple<> 36 | >, "Slicing off all elements from a tuple breaks" 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /tests/variables.cpp: -------------------------------------------------------------------------------- 1 | #include "catch2.hpp" 2 | #include 3 | 4 | TEST_CASE("slugs should parse correctly into structs") 5 | { 6 | std::vector slugs { "abc", "10", "def" }; 7 | 8 | SECTION("parse slugs into struct") { 9 | struct dto_struct 10 | { 11 | std::string field1; 12 | std::size_t field2; 13 | std::string_view field3; 14 | 15 | using dto = router::dto 16 | ::bind<&dto_struct::field1> 17 | ::bind<&dto_struct::field2> 18 | ::bind<&dto_struct::field3>; 19 | }; 20 | 21 | dto_struct output{}; 22 | 23 | router::to_dto(slugs, output); 24 | 25 | REQUIRE(output.field1 == "abc"); 26 | REQUIRE(output.field2 == 10); 27 | REQUIRE(output.field3 == "def"); 28 | } 29 | 30 | SECTION("parse slugs into tuple") { 31 | std::tuple output{}; 32 | 33 | router::to_dto(slugs, output); 34 | 35 | REQUIRE(std::get<0>(output) == "abc"); 36 | REQUIRE(std::get<1>(output) == 10); 37 | REQUIRE(std::get<2>(output) == "def"); 38 | } 39 | } 40 | --------------------------------------------------------------------------------