From 25dee6648be5ac1b476ff48b7aa2f9470c5912aa Mon Sep 17 00:00:00 2001 From: "John R. Patek" Date: Thu, 23 Apr 2026 02:20:59 -0500 Subject: [PATCH] added tree to request router --- include/webframe.hpp | 42 +++++---- include/webframe/router.hpp | 35 +++++++- src/router.cpp | 149 +++++++++++++++++++++++++++---- src/runtimes/desktop/webview.cpp | 2 +- src/runtimes/server/runtime.cpp | 2 +- tests/src/client.cpp | 3 +- 6 files changed, 196 insertions(+), 37 deletions(-) diff --git a/include/webframe.hpp b/include/webframe.hpp index a2e30f4..4c7ef64 100644 --- a/include/webframe.hpp +++ b/include/webframe.hpp @@ -74,6 +74,16 @@ namespace webframe class request { public: + /** + * @brief URL variables extracted from the request path + * @details If the request path matches a route with variables, the variables will be extracted + * and stored in this vector. The order of the variables corresponds to the order of the placeholders + * in the route definition. + * + * TODO: find a cleaner way to do this. + */ + std::vector path_variables; + /** * @brief get the HTTP method of the request * @return the HTTP method of the request @@ -86,12 +96,12 @@ namespace webframe virtual std::string get_path() const = 0; virtual std::string get_uri() const = 0; - + /** * @brief get the value of a specific header * @param key the header key * @param value reference to the header value - * @return true if the header exists, false otherwise. The string reference will only + * @return true if the header exists, false otherwise. The string reference will only * be set if the header exists. */ virtual bool get_header(const std::string &key, std::string &value) const = 0; @@ -99,8 +109,8 @@ namespace webframe /** * @brief get the body of the request as a pointer and size * @return a pair containing a pointer to the body data and the size of the body - * @details The body data is not guaranteed to be null-terminated. The pointer and size are only - * valid for the duration of the request handling. If the request does not have a body, the pointer + * @details The body data is not guaranteed to be null-terminated. The pointer and size are only + * valid for the duration of the request handling. If the request does not have a body, the pointer * will be null and the size will be zero. */ virtual std::pair get_body() const = 0; @@ -108,8 +118,8 @@ namespace webframe /** * @brief read the body of the request using a callback * @param callback a function to be called with the body data and size - * @details The callback will be called with chunks of the body data. None of the runtimes are - * currently capable of streaming request bodies, so the callback will be called at most once with the + * @details The callback will be called with chunks of the body data. None of the runtimes are + * currently capable of streaming request bodies, so the callback will be called at most once with the * entire body. If there is no request body, the callback will not be called. */ virtual void read_body(const std::function &callback) const = 0; @@ -126,11 +136,11 @@ namespace webframe /** * @brief set the HTTP status code of the response * @param status_code the HTTP status code - * @details The status code is not checked against valid HTTP status codes. It can be set to + * @details The status code is not checked against valid HTTP status codes. It can be set to * any integer value, but there is no guarantee that the runtime implementation will accept it. */ virtual void set_status(int status_code) = 0; - + /** * @brief set a header of the response * @param key the header key @@ -138,13 +148,13 @@ namespace webframe * @details The headers are not validated, and there is no standard way to handle duplicate entries. */ virtual void set_header(const std::string &key, const std::string &value) = 0; - + /** * @brief set the body of the response * @param data pointer to the body data * @param size the size of the body data - * @details This must be called once after the status and headers have been set. If it is called - * before, the status will be 200 and the headers will be empty. There is no standard way to handle + * @details This must be called once after the status and headers have been set. If it is called + * before, the status will be 200 and the headers will be empty. There is no standard way to handle * multiple calls. */ virtual void set_body(const uint8_t *data, size_t size) = 0; @@ -152,8 +162,8 @@ namespace webframe /** * @brief write the body of the response using a callback * @param callback a function to be called with a chunk to set the body data and size. - * @details The callback will be called with chunks of the body data until it returns false to indicate - * end-of-stream. The callback will always be called at least once, and will only populate the chunk if + * @details The callback will be called with chunks of the body data until it returns false to indicate + * end-of-stream. The callback will always be called at least once, and will only populate the chunk if * the data is not null and the size is greater than zero. */ virtual void write_body(const std::function &)> &callback) = 0; @@ -162,7 +172,7 @@ namespace webframe /** * @class runtime * @brief abstract interface for WebFrame runtimes - * @details Given an application and a router, the runtime is responsible for abstracting HTTP traffic from the + * @details Given an application and a router, the runtime is responsible for abstracting HTTP traffic from the * underlying platform. The user will rarely, if ever, interact with this interface directly. */ class runtime @@ -178,7 +188,7 @@ namespace webframe * @param a the application to dispatch * @param r the router to use for dispatching requests * @return the exit code of the application - * @details This is only used for the Win32 desktop runtime. You should not call this directly. Use the WEBFRAME_MAIN macro + * @details This is only used for the Win32 desktop runtime. You should not call this directly. Use the WEBFRAME_MAIN macro * to define the entry point of your application, and it will call this function for you. */ virtual int dispatch(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow, application *a, router *r) = 0; @@ -190,7 +200,7 @@ namespace webframe * @param a the application to dispatch * @param r the router to use for dispatching requests * @return the exit code of the application - * @details This is used for all non-Win32 runtimes, including Windows servers. Like the other dispatch function, it + * @details This is used for all non-Win32 runtimes, including Windows servers. Like the other dispatch function, it * should not be called directly. */ virtual int dispatch(int argc, const char **argv, application *a, router *r) = 0; diff --git a/include/webframe/router.hpp b/include/webframe/router.hpp index 18be3a6..6e40ed3 100644 --- a/include/webframe/router.hpp +++ b/include/webframe/router.hpp @@ -19,8 +19,11 @@ #ifndef WEBFRAME_ROUTER_HPP #define WEBFRAME_ROUTER_HPP +#include +#include #include #include +#include namespace webframe { @@ -28,17 +31,43 @@ namespace webframe class response; class handler; + class tree_node + { + public: + tree_node() = default; + ~tree_node() = default; + void add(const std::string& path, handler* h); + handler* find(const std::string& path, std::vector& variables) const; + private: + enum class node_type + { + leaf, + token, + wildcard + }; + static void split_path(const std::string& path, std::queue& tokens); + tree_node *push_token(const std::string& token); + tree_node *push_wildcard(); + tree_node *next(const std::string& token, std::vector& variables) const; + tree_node *next_token(const std::string& token) const; + tree_node *next_wildcard(const std::string& token, std::vector& variables) const; + std::unique_ptr>> _token_nodes; + std::unique_ptr _wildcard_node; + node_type _type = node_type::leaf; + handler *_handler = nullptr; + }; + class router { public: router() = default; ~router() = default; void add_route(const std::string& path, handler* h); - handler *find_route(const std::string& path); + handler *find_route(const std::string& path, std::vector& variables) const; void set_default(handler* h); - handler* get_default(); + handler* get_default() const; private: - std::unordered_map _handlers; + tree_node _root; handler* _default_handler = nullptr; }; } diff --git a/src/router.cpp b/src/router.cpp index 1bb0d21..85689af 100644 --- a/src/router.cpp +++ b/src/router.cpp @@ -21,22 +21,22 @@ class global_default_handler : public webframe::handler { protected: - void handle_get(const webframe::request* req, webframe::response* res) override + void handle_get(const webframe::request *req, webframe::response *res) override { throw webframe::exception::not_found; } - void handle_post(const webframe::request* req, webframe::response* res) override + void handle_post(const webframe::request *req, webframe::response *res) override { throw webframe::exception::not_found; } - void handle_put(const webframe::request* req, webframe::response* res) override + void handle_put(const webframe::request *req, webframe::response *res) override { throw webframe::exception::not_found; } - void handle_delete(const webframe::request* req, webframe::response* res) override + void handle_delete(const webframe::request *req, webframe::response *res) override { throw webframe::exception::not_found; } @@ -46,27 +46,146 @@ static global_default_handler g_default_handler; namespace webframe { - void router::add_route(const std::string& path, handler* h) + void tree_node::add(const std::string &path, handler *h) { - _handlers[path] = h; + std::queue tokens; + split_path(path, tokens); + tree_node *current = this; + while (!tokens.empty()) + { + const std::string &token = tokens.front(); + if (token == "*") + { + current = current->push_wildcard(); + } + else + { + current = current->push_token(token); + } + tokens.pop(); + } + current->_handler = h; } - void router::set_default(handler* h) + handler *tree_node::find(const std::string &path, std::vector &variables) const { - _default_handler = h; + std::queue tokens; + split_path(path, tokens); + const tree_node *current = this; + while (!tokens.empty() && current) + { + const std::string &token = tokens.front(); + current = current->next(token, variables); + tokens.pop(); + } + return current ? current->_handler : nullptr; + } + + void tree_node::split_path(const std::string &path, std::queue &tokens) + { + size_t start, end; + if (!path.empty() && path != "/") + { + start = 0; + while (start < path.size()) + { + size_t end = path.find('/', start); + if (end == std::string::npos) + { + end = path.size(); + } + if (end > start) + { + tokens.push(path.substr(start, end - start)); + } + start = end + 1; + } + } + } + + tree_node *tree_node::push_token(const std::string &token) + { + _type = node_type::token; + if (!_token_nodes) + { + _token_nodes = std::make_unique>>(); + } + auto &node_ptr = (*_token_nodes)[token]; + if (!node_ptr) + { + node_ptr = std::make_unique(); + } + return node_ptr.get(); + } + + tree_node *tree_node::push_wildcard() + { + _type = node_type::wildcard; + if (!_wildcard_node) + { + _wildcard_node = std::make_unique(); + } + return _wildcard_node.get(); + } + + tree_node *tree_node::next(const std::string &token, std::vector &variables) const + { + tree_node *node(nullptr); + switch(_type) + { + case node_type::token: + node = next_token(token); + break; + case node_type::wildcard: + node = next_wildcard(token, variables); + break; + default: + break; + } + return node; } - handler* router::find_route(const std::string& path) + tree_node *tree_node::next_token(const std::string &token) const { - auto it = _handlers.find(path); - if (it != _handlers.end()) { - return it->second; + tree_node *node(nullptr); + if (_token_nodes) + { + auto it = _token_nodes->find(token); + if (it != _token_nodes->end()) + { + node = it->second.get(); + } } - return get_default(); + return node; + } + + tree_node *tree_node::next_wildcard(const std::string& token, std::vector &variables) const + { + if (_wildcard_node) + { + variables.push_back(token); + } + return _wildcard_node ? _wildcard_node.get() : nullptr; + } + + void router::add_route(const std::string &path, handler *h) + { + _root.add(path, h); + } + + void router::set_default(handler *h) + { + _default_handler = h; + } + + handler *router::find_route(const std::string &path, std::vector &variables) const + { + handler *h = _root.find(path, variables); + return h ? h : get_default(); } - handler* router::get_default() + handler *router::get_default() const { - return _default_handler ? _default_handler : &g_default_handler; + return _default_handler ? _default_handler : &g_default_handler; } } \ No newline at end of file diff --git a/src/runtimes/desktop/webview.cpp b/src/runtimes/desktop/webview.cpp index f815372..039b834 100644 --- a/src/runtimes/desktop/webview.cpp +++ b/src/runtimes/desktop/webview.cpp @@ -18,7 +18,7 @@ namespace webframe bool sent(false); webframe::desktop::request req(&request); webframe::desktop::response res(response.get(), sent); - webframe::handler *handler = _router->find_route(req.get_path()); + webframe::handler *handler = _router->find_route(req.get_path(), req.path_variables); handler->handle_request(&req, &res); if (!sent) { diff --git a/src/runtimes/server/runtime.cpp b/src/runtimes/server/runtime.cpp index 9f92a06..5af5f6a 100644 --- a/src/runtimes/server/runtime.cpp +++ b/src/runtimes/server/runtime.cpp @@ -39,7 +39,7 @@ static void evhttp_callback(struct evhttp_request *req, void *arg) webframe::router *runtime = static_cast(arg); webframe::server::request request(req); webframe::server::response response(req, sent_response, status); - webframe::handler *handler = runtime->find_route(request.get_path()); + webframe::handler *handler = runtime->find_route(request.get_path(), request.path_variables); handler->handle_request(&request, &response); if (!sent_response) { diff --git a/tests/src/client.cpp b/tests/src/client.cpp index c940c9e..2396ece 100644 --- a/tests/src/client.cpp +++ b/tests/src/client.cpp @@ -26,7 +26,8 @@ namespace webframe::test void client::run(const webframe::request *request, webframe::response *response) { - webframe::handler *handler = _router->find_route(request->get_path()); + std::vector variables; + webframe::handler *handler = _router->find_route(request->get_path(), variables); handler->handle_request(request, response); } } \ No newline at end of file