LCOV - code coverage report
Current view: top level - http/server/routing - route.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 95.5 % 132 126
Test Date: 2026-04-21 17:49:55 Functions: 93.3 % 15 14

            Line data    Source code
       1              : #include "route.hpp"
       2              : #include "../response.hpp"
       3              : #include <regex>
       4              : #include "../../../util/logger.hpp"
       5              : 
       6              : namespace thinger::http {
       7              : 
       8         6925 : route::route(const std::string& pattern)
       9         6925 :     : pattern_(pattern)
      10              : {
      11              :     // Convert route pattern to regex
      12              :     // Support two syntaxes:
      13              :     // 1. :param_name - matches any non-slash characters
      14              :     // 2. :param_name(regex) - matches the specified regex pattern
      15              :     
      16         6925 :     std::string regex_pattern = pattern;
      17              :     
      18              :     // First, handle parameters with custom regex: :param(regex)
      19         6925 :     std::regex custom_param_regex(":([a-zA-Z_][a-zA-Z0-9_]*)\\(([^)]+)\\)");
      20         6925 :     std::smatch match;
      21         6925 :     std::string temp = pattern;
      22              :     
      23              :     // Find all :param(regex) patterns
      24         6981 :     while (std::regex_search(temp, match, custom_param_regex)) {
      25           56 :         parameters_.push_back(match[1]);
      26           56 :         temp = match.suffix();
      27              :     }
      28              :     
      29              :     // Then, handle simple parameters: :param
      30         6925 :     std::regex simple_param_regex(":([a-zA-Z_][a-zA-Z0-9_]*)(?![\\(])");
      31         6925 :     temp = pattern;
      32         8461 :     while (std::regex_search(temp, match, simple_param_regex)) {
      33              :         // Only add if not already added (avoid duplicates with custom regex params)
      34         1536 :         std::string param_name = match[1];
      35         1536 :         if (std::find(parameters_.begin(), parameters_.end(), param_name) == parameters_.end()) {
      36         1536 :             parameters_.push_back(param_name);
      37              :         }
      38         1536 :         temp = match.suffix();
      39         1536 :     }
      40              :     
      41              :     // Escape special regex characters in the pattern (but not in our parameter patterns)
      42         6925 :     std::string escaped = pattern;
      43              :     
      44              :     // First, temporarily replace our parameter patterns to protect them
      45         6925 :     escaped = std::regex_replace(escaped, custom_param_regex, "__CUSTOM_PARAM_$1__");
      46         6925 :     escaped = std::regex_replace(escaped, simple_param_regex, "__SIMPLE_PARAM_$1__");
      47              :     
      48              :     // Escape special characters
      49         6925 :     escaped = std::regex_replace(escaped, std::regex("([.^$*+?{}\\[\\]\\\\|])"), "\\\\$1");
      50              :     
      51              :     // Now replace parameters with their regex groups
      52              :     // Custom parameters: restore the custom regex
      53         6925 :     temp = pattern;
      54         6925 :     std::string result = escaped;
      55         6981 :     while (std::regex_search(temp, match, custom_param_regex)) {
      56           56 :         std::string param_name = match[1];
      57           56 :         std::string param_regex = match[2];
      58           56 :         std::string placeholder = "__CUSTOM_PARAM_" + param_name + "__";
      59           56 :         result = std::regex_replace(result, std::regex(placeholder), "(" + param_regex + ")");
      60           56 :         temp = match.suffix();
      61           56 :     }
      62              :     
      63              :     // Simple parameters: use default regex
      64         6925 :     result = std::regex_replace(result, std::regex("__SIMPLE_PARAM_([a-zA-Z_][a-zA-Z0-9_]*)__"), "([^/]+)");
      65              :     
      66              :     // Add anchors
      67         6925 :     regex_pattern = "^" + result + "$";
      68              :     
      69         6925 :     regex_ = std::regex(regex_pattern);
      70         6925 : }
      71              : 
      72          354 : route& route::operator=(route_callback_response_only callback) {
      73          354 :     callback_ = std::move(callback);
      74          354 :     return *this;
      75              : }
      76              : 
      77          344 : route& route::operator=(route_callback_json_response callback) {
      78          344 :     callback_ = std::move(callback);
      79          344 :     return *this;
      80              : }
      81              : 
      82         6086 : route& route::operator=(route_callback_request_response callback) {
      83         6086 :     callback_ = std::move(callback);
      84         6086 :     return *this;
      85              : }
      86              : 
      87           79 : route& route::operator=(route_callback_request_json_response callback) {
      88           79 :     callback_ = std::move(callback);
      89           79 :     return *this;
      90              : }
      91              : 
      92           20 : route& route::operator=(route_callback_awaitable callback) {
      93           20 :     callback_ = std::move(callback);
      94           20 :     deferred_body_ = true;  // auto-enable deferred body for awaitable callbacks
      95           20 :     return *this;
      96              : }
      97              : 
      98            3 : route& route::deferred_body(bool enabled) {
      99            3 :     deferred_body_ = enabled;
     100            3 :     return *this;
     101              : }
     102              : 
     103            3 : route& route::auth(auth_level level) {
     104            3 :     auth_level_ = level;
     105            3 :     return *this;
     106              : }
     107              : 
     108            3 : route& route::description(const std::string& desc) {
     109            3 :     description_ = desc;
     110            3 :     return *this;
     111              : }
     112              : 
     113         2748 : bool route::matches(const std::string& path, std::smatch& matches) const {
     114         2748 :     return std::regex_match(path, matches, regex_);
     115              : }
     116              : 
     117         1019 : void route::handle_request(request& req, response& res) const {
     118              :     // Handle response-only callback
     119         1019 :     if (std::holds_alternative<route_callback_response_only>(callback_)) {
     120          132 :         std::get<route_callback_response_only>(callback_)(res);
     121              :     }
     122              :     // Handle JSON + response callback (json is parsed from request body)
     123          887 :     else if (std::holds_alternative<route_callback_json_response>(callback_)) {
     124           61 :         auto http_req = req.get_http_request();
     125           61 :         if (!http_req->get_body().empty()) {
     126           53 :             auto json = nlohmann::json::parse(http_req->get_body(), nullptr, false);
     127           53 :             if (json.is_discarded()) {
     128            6 :                 res.error(http_response::status::bad_request, "Invalid JSON");
     129              :             } else {
     130              : #ifdef THINGER_HTTP_VALIJSON_ENABLED
     131           50 :                 if (!validate_json(json, res)) return;
     132              : #endif
     133           29 :                 std::get<route_callback_json_response>(callback_)(json, res);
     134              :             }
     135           53 :         } else {
     136            8 :             nlohmann::json empty_json;
     137              : #ifdef THINGER_HTTP_VALIJSON_ENABLED
     138            8 :             if (!validate_json(empty_json, res)) return;
     139              : #endif
     140            5 :             std::get<route_callback_json_response>(callback_)(empty_json, res);
     141            8 :         }
     142           61 :     }
     143              :     // Handle request + response callback
     144          826 :     else if (std::holds_alternative<route_callback_request_response>(callback_)) {
     145          799 :         std::get<route_callback_request_response>(callback_)(req, res);
     146              :     }
     147              :     // Handle request + JSON + response callback (json is parsed from request body)
     148           27 :     else if (std::holds_alternative<route_callback_request_json_response>(callback_)) {
     149           24 :         auto http_req = req.get_http_request();
     150           24 :         if (!http_req->get_body().empty()) {
     151           18 :             auto json = nlohmann::json::parse(http_req->get_body(), nullptr, false);
     152           18 :             if (json.is_discarded()) {
     153           12 :                 res.error(http_response::status::bad_request, "Invalid JSON");
     154              :             } else {
     155              : #ifdef THINGER_HTTP_VALIJSON_ENABLED
     156           12 :                 if (!validate_json(json, res)) return;
     157              : #endif
     158            9 :                 std::get<route_callback_request_json_response>(callback_)(req, json, res);
     159              :             }
     160           18 :         } else {
     161            6 :             nlohmann::json empty_json;
     162              : #ifdef THINGER_HTTP_VALIJSON_ENABLED
     163            6 :             if (!validate_json(empty_json, res)) return;
     164              : #endif
     165            6 :             std::get<route_callback_request_json_response>(callback_)(req, empty_json, res);
     166            6 :         }
     167           24 :     }
     168              :     // Awaitable callback — cannot be called synchronously
     169            3 :     else if (std::holds_alternative<route_callback_awaitable>(callback_)) {
     170            6 :         res.error(http_response::status::internal_server_error,
     171              :                   "Awaitable route handler invoked synchronously; use handle_request_coro() instead");
     172              :     }
     173              : }
     174              : 
     175           10 : thinger::awaitable<void> route::handle_request_coro(request& req, response& res) const {
     176              :     if (std::holds_alternative<route_callback_awaitable>(callback_)) {
     177              :         co_await std::get<route_callback_awaitable>(callback_)(req, res);
     178              :     } else {
     179              :         handle_request(req, res);
     180              :     }
     181           20 : }
     182              : 
     183            0 : void route::parse_parameters() {
     184              :     // Parameters are now parsed in the constructor
     185            0 : }
     186              : 
     187          324 : route& route::schema(const nlohmann::json& json_schema) {
     188              : #ifdef THINGER_HTTP_VALIJSON_ENABLED
     189          324 :     json_schema_ = json_schema;
     190          324 :     schema_ = std::make_shared<valijson::Schema>();
     191          324 :     valijson::SchemaParser parser;
     192          324 :     valijson::adapters::NlohmannJsonAdapter adapter(json_schema_);
     193              :     try {
     194          324 :         parser.populateSchema(adapter, *schema_);
     195            0 :     } catch (const std::exception& e) {
     196            0 :         LOG_ERROR("Failed to parse JSON Schema: {}", e.what());
     197            0 :         schema_.reset();
     198            0 :     }
     199              : #else
     200              :     LOG_WARNING("JSON Schema validation requested but Valijson is not enabled");
     201              : #endif
     202          324 :     return *this;
     203          324 : }
     204              : 
     205              : #ifdef THINGER_HTTP_VALIJSON_ENABLED
     206           76 : bool route::validate_json(const nlohmann::json& json, response& res) const {
     207           76 :     if (!schema_) return true;
     208              : 
     209           48 :     valijson::Validator validator;
     210           48 :     valijson::adapters::NlohmannJsonAdapter adapter(json);
     211           48 :     valijson::ValidationResults results;
     212              : 
     213           48 :     if (validator.validate(*schema_, adapter, &results)) {
     214           21 :         return true;
     215              :     }
     216              : 
     217              :     // Build error response with first validation error
     218           27 :     valijson::ValidationResults::Error error;
     219          243 :     nlohmann::json error_response = {{"error", {{"message", "Schema validation failed"}}}};
     220           27 :     if (results.popError(error)) {
     221           27 :         error_response["error"]["message"] = error.description;
     222           27 :         nlohmann::json context = nlohmann::json::array();
     223           72 :         for (const auto& c : error.context) {
     224           45 :             context.push_back(c);
     225              :         }
     226           27 :         if (!context.empty()) {
     227           27 :             error_response["error"]["context"] = std::move(context);
     228              :         }
     229           27 :     }
     230              : 
     231           27 :     res.json(error_response, http_response::status::bad_request);
     232           27 :     return false;
     233          291 : }
     234              : #endif
     235              : 
     236              : } // namespace thinger::http
        

Generated by: LCOV version 2.0-1