LCOV - code coverage report
Current view: top level - http/server/routing - route.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 55 55
Test Date: 2026-11-10 10:38:07 Functions: 92.5 % 2107 1948

            Line data    Source code
       1              : #ifndef THINGER_HTTP_ROUTE_DESCRIPTOR_HPP
       2              : #define THINGER_HTTP_ROUTE_DESCRIPTOR_HPP
       3              : 
       4              : #include <chrono>
       5              : #include <concepts>
       6              : #include <cstdint>
       7              : #include <type_traits>
       8              : #include <map>
       9              : #include <functional>
      10              : #include <initializer_list>
      11              : #include <regex>
      12              : #include <string>
      13              : #include <vector>
      14              : #include <memory>
      15              : #include <optional>
      16              : #include "../request.hpp"
      17              : #include "../body_decoders.hpp"
      18              : #include "../../../util/types.hpp"
      19              : 
      20              : #ifdef THINGER_HTTP_VALIJSON_ENABLED
      21              : #include <valijson/schema.hpp>
      22              : #include <valijson/schema_parser.hpp>
      23              : #include <valijson/validator.hpp>
      24              : #include <valijson/validation_results.hpp>
      25              : #include <valijson/adapters/nlohmann_json_adapter.hpp>
      26              : #endif
      27              : 
      28              : namespace thinger::http {
      29              : 
      30              : // Route parameters syntax:
      31              : // 1. Simple parameters: :param_name
      32              : //    Example: "/api/v1/users/:user/devices/:device"
      33              : //    Matches any non-empty text without slashes
      34              : //
      35              : // 2. Parameters with custom regex: :param_name(regex)
      36              : //    Example: "/api/v1/users/:id([0-9]+)"         - numeric ID only
      37              : //    Example: "/api/v1/users/:user([a-zA-Z0-9_-]{1,32})" - alphanumeric with length limit
      38              : //    Example: "/files/:path(.+)"                   - match everything including slashes
      39              : //    The regex extends to the matching closing parenthesis, so it may contain groups.
      40              : //
      41              : // Matching (see route_tree): the path is matched segment by segment (the text between
      42              : // slashes), with this priority whatever the registration order: static segments, then
      43              : // segments with a regex constraint (or mixing text and parameters, as ":name.:ext"), then
      44              : // simple parameters, then parameters that may match slashes (as ":path(.+)"), which take
      45              : // the rest of the path. If the rest of the path does not match below the chosen option,
      46              : // the next one is tried. Different constraints at the same position are tried in the
      47              : // order they were first registered there, and wildcards in registration order. Registering
      48              : // a route with the same structure as another one logs a warning, and so does adding a
      49              : // constraint (or wildcard) next to one that may overlap with it (a cheap check: unless
      50              : // they cannot start with the same character). A parameter regex that
      51              : // depends on what surrounds it (anchors ^ $, word boundaries, lookaheads, back-references)
      52              : // makes its route match the whole path with its regex, after all the other options.
      53              : // - The path is matched as received, before percent-decoding: "%2F" is not a slash, and
      54              : //   parameters get the encoded text ("/files/:name" on "/files/a%20b" gives "a%20b").
      55              : // - Trailing slashes and empty segments are significant: "/users" does not match
      56              : //   "/users/", and a parameter does not match an empty segment ("/users//devices").
      57              : //
      58              : // Common patterns:
      59              : #define ID_PATTERN      "[0-9]+"                      // Numeric ID
      60              : #define ALPHANUM_ID     "[a-zA-Z0-9_-]{1,32}"        // Alphanumeric ID (1-32 chars)
      61              : #define UUID_PATTERN    "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
      62              : #define EMAIL_PATTERN   "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
      63              : #define SLUG_PATTERN    "[a-z0-9]+(?:-[a-z0-9]+)*"   // URL-friendly slug
      64              : 
      65              : // Forward declarations
      66              : class request;
      67              : class response;
      68              : 
      69              : // Callback types for route handlers - supporting multiple signatures
      70              : using route_callback_response_only = std::function<void(response&)>;
      71              : using route_callback_json_response = std::function<void(nlohmann::json&, response&)>;
      72              : using route_callback_request_response = std::function<void(request&, response&)>;
      73              : using route_callback_request_json_response = std::function<void(request&, nlohmann::json&, response&)>;
      74              : // What every route callback is turned into: a coroutine taking (request&, response&)
      75              : using route_callback_awaitable = std::function<thinger::awaitable<void>(request&, response&)>;
      76              : 
      77              : // Route callback signatures, synchronous or coroutines returning awaitable<void>. Those
      78              : // taking a JSON body get it read, decoded (from JSON or the other types the route accepts,
      79              : // see route::accepts) and validated against the route schema before they run; coroutines
      80              : // taking (request&, response&) or (response&) read the body themselves (deferred body)
      81              : // unless deferred_body(false) is set on the route.
      82              : template<typename F>
      83              : concept request_response_callback = std::invocable<F&, request&, response&>;
      84              : 
      85              : template<typename F>
      86              : concept response_callback = !request_response_callback<F> && std::invocable<F&, response&>;
      87              : 
      88              : template<typename F>
      89              : concept request_json_callback = std::invocable<F&, request&, nlohmann::json&, response&>;
      90              : 
      91              : // Generic lambdas taking two arguments are treated as (request&, response&)
      92              : template<typename F>
      93              : concept json_callback = !request_response_callback<F> && std::invocable<F&, nlohmann::json&, response&>;
      94              : 
      95              : // Callbacks that do not take the JSON body (they may read the body themselves), as the
      96              : // not found handlers
      97              : template<typename F>
      98              : concept request_handler_callable = request_response_callback<F> || response_callback<F>;
      99              : 
     100              : template<typename F>
     101              : concept route_callable = request_handler_callable<F> || request_json_callback<F> || json_callback<F>;
     102              : 
     103              : // The same signatures, as coroutines
     104              : template<typename F, typename... Args>
     105              : concept returns_awaitable = std::same_as<std::invoke_result_t<F&, Args...>, thinger::awaitable<void>>;
     106              : 
     107              : template<typename F>
     108              : concept awaitable_handler = request_response_callback<F> && returns_awaitable<F, request&, response&>;
     109              : 
     110              : template<typename F>
     111              : concept awaitable_response_handler = response_callback<F> && returns_awaitable<F, response&>;
     112              : 
     113              : template<typename F>
     114              : concept awaitable_request_json_handler = request_json_callback<F>
     115              :                                       && returns_awaitable<F, request&, nlohmann::json&, response&>;
     116              : 
     117              : template<typename F>
     118              : concept awaitable_json_handler = json_callback<F> && returns_awaitable<F, nlohmann::json&, response&>;
     119              : 
     120              : namespace detail {
     121              : 
     122              :     using thinger::is_awaitable_v;
     123              : 
     124              :     template<typename T>
     125              :     inline constexpr bool is_std_function_v = false;
     126              : 
     127              :     template<typename R, typename... Args>
     128              :     inline constexpr bool is_std_function_v<std::function<R(Args...)>> = true;
     129              : 
     130              :     // Whether a callback is empty: a null std::function or function pointer
     131              :     template<typename F>
     132          848 :     bool is_empty_callback(const F& callback) {
     133              :         using type = std::remove_cvref_t<F>;
     134              :         if constexpr (is_std_function_v<type> || std::is_pointer_v<type>) {
     135            9 :             return !callback;
     136              :         } else {
     137          839 :             return false;
     138              :         }
     139              :     }
     140              : 
     141              :     template<typename F, typename... Args>
     142          154 :     thinger::awaitable<void> call_synchronously(F& callback, Args&... args) {
     143              :         callback(args...);
     144              :         co_return;
     145          308 :     }
     146              : 
     147              :     // Call a route callback as a coroutine: coroutines as they are, synchronous callbacks
     148              :     // from one (their result, if any, is ignored)
     149              :     template<typename F, typename... Args>
     150          952 :     thinger::awaitable<void> call_callback(F& callback, Args&... args) {
     151              :         using result = std::invoke_result_t<F&, Args&...>;
     152              :         if constexpr (std::same_as<result, thinger::awaitable<void>>) {
     153          798 :             return callback(args...);
     154              :         } else {
     155              :             static_assert(!is_awaitable_v<result>, "coroutine route callbacks must return thinger::awaitable<void>");
     156          154 :             return call_synchronously(callback, args...);
     157              :         }
     158              :     }
     159              : 
     160              : }
     161              : 
     162              : // A callback taking (request&, response&) or (response&), as the coroutine every route
     163              : // callback is turned into; empty if the callback is (a null std::function or pointer)
     164              : template<request_handler_callable F>
     165          848 : route_callback_awaitable make_route_callback(F&& callback) {
     166          848 :     if (detail::is_empty_callback(callback)) return nullptr;
     167              :     if constexpr (request_response_callback<F>) {
     168         2177 :         return [callback = std::forward<F>(callback)](request& req, response& res) mutable {
     169          862 :             return detail::call_callback(callback, req, res);
     170          701 :         };
     171              :     } else {
     172          360 :         return [callback = std::forward<F>(callback)](request&, response& res) mutable {
     173           90 :             return detail::call_callback(callback, res);
     174          138 :         };
     175              :     }
     176              : }
     177              : 
     178              : // Documented request parameter (path or query), for API documentation
     179              : struct route_parameter {
     180              :     std::string name;
     181              :     std::string in;             // "path" or "query"
     182              :     std::string description;
     183              :     nlohmann::json schema;      // JSON Schema of the value
     184              :     bool required = false;
     185              : };
     186              : 
     187              : // Documented response for a status code, for API documentation
     188              : struct route_response {
     189              :     std::string description;
     190              :     nlohmann::json schema;      // JSON Schema of the body (null if none)
     191              :     nlohmann::json example;     // example body (null if none)
     192              : };
     193              : 
     194              : class route {
     195              : public:
     196              :     route(const std::string& pattern);
     197              :     
     198              :     // Set the callback: any route_callable signature, synchronous or coroutine. It is
     199              :     // stored as a callable taking (request&, response&), synchronous (called directly,
     200              :     // without a coroutine frame per request) or coroutine, and whether it takes the JSON body.
     201              :     template<route_callable F>
     202        34250 :     route& operator=(F&& callback) {
     203        34250 :         takes_json_body_ = !request_handler_callable<F>;
     204        34250 :         decodes_body_ = takes_json_body_ || has_schema_;
     205        58475 :         auto adapted = [callback = std::forward<F>(callback)](request& req, response& res) mutable -> decltype(auto) {
     206        25049 :             return invoke_callback(callback, req, res);
     207              :         };
     208              :         using result = std::invoke_result_t<decltype(adapted)&, request&, response&>;
     209              :         if constexpr (std::same_as<result, thinger::awaitable<void>>) {
     210         5916 :             callback_ = std::move(adapted);
     211         5916 :             sync_callback_ = nullptr;
     212              :             // coroutines not taking the JSON body read it themselves; the others get it read first
     213         5916 :             if (!takes_json_body_) deferred_body_ = true;
     214              :         } else {
     215              :             static_assert(!detail::is_awaitable_v<result>, "coroutine route callbacks must return thinger::awaitable<void>");
     216        28334 :             sync_callback_ = std::move(adapted);
     217        28334 :             callback_ = nullptr;
     218              :         }
     219        34250 :         return *this;
     220         2204 :     }
     221              : 
     222              :     // Deferred body mode - handler reads body at its discretion. Ignored by handlers
     223              :     // that take the JSON body, and by routes with a schema, which always need it read first.
     224              :     route& deferred_body(bool enabled = true);
     225        25696 :     bool is_deferred_body() const { return deferred_body_ && !decodes_body_; }
     226              : 
     227              :     // Set JSON Schema for request body validation. The body is read, decoded and validated
     228              :     // before the callback runs, whatever its signature: a callback taking only (request&,
     229              :     // response&) or (response&) finds it in req.body(), already validated; one not valid is
     230              :     // answered as for callbacks taking the JSON body (400, or 415 for a type that cannot be
     231              :     // decoded). If it cannot be parsed (e.g. a $ref to a schema component not registered
     232              :     // yet), requests to the route are answered with 500.
     233              :     route& schema(const nlohmann::json& json_schema);
     234              : 
     235              :     // Body types the route accepts: names of body decoders ("json", "msgpack", "cbor",
     236              :     // "ubjson", "form", "text", "binary", or custom ones, see body_decoder_registry) or media
     237              :     // types ("application/cbor", "image/png"). A body of another type is answered with 415
     238              :     // Unsupported Media Type before it is read; a body without Content-Type is taken as
     239              :     // JSON. Routes taking the body decoded accept, without this, the types of the
     240              :     // application (http_application::accept_body_types: JSON only by default), and only
     241              :     // those with a decoder. Routes reading the body themselves accept any type without
     242              :     // this, or the listed ones, decoder or not (they get the raw body).
     243              :     route& accepts(std::vector<std::string> types);
     244              : 
     245              :     // Types set with accepts(), lowercase; empty if none (the application ones apply)
     246          146 :     const std::vector<std::string>& get_accepted_types() const { return accepted_types_; }
     247              : 
     248              :     // --- Limits (a group sets them on its routes; a route can override them) ---
     249              : 
     250              :     // Maximum request body size, instead of the one of its virtual host or the application
     251              :     // (see virtual_host::max_body_size, http_application::set_max_body_size): a larger body is answered with 413 Payload Too
     252              :     // Large, a Content-Length one before reading it, whether the server reads it (callbacks
     253              :     // taking the body decoded, or deferred_body(false)) or the callback does (read_body(),
     254              :     // read(), read_some(), a reverse proxy). Also once decompressed.
     255              :     // std::numeric_limits<std::uint64_t>::max() for no limit.
     256              :     route& max_body_size(std::uint64_t size);
     257              : 
     258              :     // Maximum body size set for the route; nullopt if none (the host or application one applies)
     259        26381 :     const std::optional<std::uint64_t>& get_max_body_size() const { return max_body_size_; }
     260              : 
     261              :     // Maximum time from the dispatch of a request (its headers received and the route
     262              :     // matched) until its response starts, middlewares and body reading included. On expiry
     263              :     // the request is answered with `status` (504 Gateway Timeout by default, or e.g. 503
     264              :     // Service Unavailable) through the error formatter, and later answers from the callback
     265              :     // are dropped. Then the scope of the request is cancelled with cancel_reason::timeout: the
     266              :     // callback goes on until it ends, unless the route declared cancel_on(timeout), which
     267              :     // interrupts it (see cancel_on). Responses started
     268              :     // in time (streams, chunked responses, SSE, WebSocket and other takeovers) are never cut.
     269              :     // If the body was not read whole, the connection is closed after the response. Unset,
     270              :     // the timeout of its virtual host applies, if any (see virtual_host::timeout); zero for
     271              :     // no timeout.
     272              :     route& timeout(std::chrono::milliseconds timeout,
     273              :                    http_response::status status = http_response::status::gateway_timeout);
     274              : 
     275              :     // Timeout set for the route (nullopt if unset, zero for none), and its status
     276        26306 :     const std::optional<std::chrono::milliseconds>& get_timeout() const { return timeout_; }
     277          231 :     http_response::status get_timeout_status() const { return timeout_status_; }
     278              : 
     279              :     // Maximum time to receive a request, its body read to its end, from its start (its first
     280              :     // byte), instead of the one of its virtual host or the connection (see
     281              :     // virtual_host::request_timeout, connection_options::request_timeout): e.g. longer for a
     282              :     // route receiving large uploads. On expiry its scope is cancelled (cancel_reason::timeout),
     283              :     // a pending body read fails, and the request is answered 408 Request Timeout unless its
     284              :     // response started (the connection is closed then); later answers from the callback are
     285              :     // dropped, and the connection is closed. A request without a body is received with its
     286              :     // head: it never applies to it (its response time is what timeout() bounds). Zero for no
     287              :     // deadline.
     288              :     route& request_timeout(std::chrono::milliseconds timeout);
     289              : 
     290              :     // Request deadline set for the route (nullopt if unset, zero for none)
     291        25977 :     const std::optional<std::chrono::milliseconds>& get_request_timeout() const { return request_timeout_; }
     292              : 
     293              :     // The reasons for which the library interrupts the callback (see cancel_reason): when
     294              :     // the scope of its request is cancelled for one of them, the awaits of the callback end
     295              :     // with operation_aborted (asio cancels its coroutine; its next co_await throws). None
     296              :     // by default: the callback goes on and finds the reason in request::cancelled(), or binds
     297              :     // request::cancellation_slot() to the awaits it can abandon. Shutdown at the drain
     298              :     // deadline interrupts every callback, declared or not. none is ignored, so {} or {none}
     299              :     // keep the default. Set on each route: groups and hosts do not carry it. Work that must
     300              :     // not be interrupted on such a route runs outside the cancellation state of the callback:
     301              :     // after co_await boost::asio::this_coro::reset_cancellation_state(
     302              :     // boost::asio::disable_cancellation()) (without a filter, the reset lets the terminal
     303              :     // cancellation through), or in a coroutine of its own, co_spawned with no slot
     304              :     // (bind_cancellation_slot(boost::asio::cancellation_slot{}, use_awaitable)). The scope is
     305              :     // cancelled once: check request::cancelled() afterwards
     306              :     route& cancel_on(std::initializer_list<cancel_reason> reasons);
     307              : 
     308              :     // The reasons set with cancel_on(), as bits 1 << reason (0 for none)
     309        25953 :     uint8_t get_cancel_on() const { return cancel_on_; }
     310              : 
     311              :     // Limits of the body decoded for the callback, instead of those of its virtual host or
     312              :     // the application (see virtual_host::decode_limits, http_application::set_body_limits)
     313              :     route& decode_limits(body_limits limits);
     314              : 
     315              :     // Decode limits set for the route; nullopt if none (the host or application ones apply)
     316              :     const std::optional<body_limits>& get_decode_limits() const { return decode_limits_; }
     317              : 
     318              :     // --- API documentation (read by tools such as OpenAPI generators) ---
     319              :     route& summary(const std::string& text);
     320              :     route& description(const std::string& desc);
     321              :     route& tag(const std::string& name);
     322              :     route& tags(const std::vector<std::string>& names);
     323              :     route& operation_id(const std::string& id);
     324              :     route& deprecated(bool value = true);
     325              :     route& path_param(const std::string& name, const std::string& description,
     326              :                       nlohmann::json schema = {{"type", "string"}});
     327              :     route& query_param(const std::string& name, const std::string& description,
     328              :                        nlohmann::json schema = {{"type", "string"}}, bool required = false);
     329              :     route& returns(int status, const std::string& description,
     330              :                    nlohmann::json schema = nullptr, nlohmann::json example = nullptr);
     331              :     route& example(nlohmann::json body);
     332              :     route& hidden(bool value = true);   // leave the route out of the API documentation
     333              : 
     334          154 :     const std::string& get_summary() const { return summary_; }
     335          131 :     const std::string& get_description() const { return description_; }
     336          277 :     const std::vector<std::string>& get_tags() const { return tags_; }
     337          131 :     const std::string& get_operation_id() const { return operation_id_; }
     338          119 :     bool is_deprecated() const { return deprecated_; }
     339          302 :     const std::vector<route_parameter>& get_param_docs() const { return param_docs_; }
     340          125 :     const std::map<int, route_response>& get_responses() const { return responses_; }
     341          119 :     const std::vector<nlohmann::json>& get_examples() const { return examples_; }
     342          119 :     const nlohmann::json& get_schema() const { return json_schema_; }
     343          157 :     bool is_hidden() const { return hidden_; }
     344              : 
     345              :     // Whether the callback receives the request body decoded as JSON
     346          131 :     bool takes_json_body() const { return takes_json_body_; }
     347              : 
     348              :     // Check the type of the request body (before reading it) against the accepted types:
     349              :     // false, after answering 415, if it is not accepted. Requests without a body pass.
     350              :     bool accepts_body(request& req, response& res) const;
     351              : 
     352              :     // Shared schemas (OpenAPI components) that route schemas can reference with
     353              :     // {"$ref": "#/components/schemas/<name>"}; set by the router that owns the route
     354        34196 :     void set_schema_components(const nlohmann::json* components) { schema_components_ = components; }
     355              : 
     356              :     // Custom regex of a path parameter declared as :name(regex), empty if it has none
     357              :     std::string get_parameter_pattern(const std::string& name) const;
     358              : 
     359              :     // --- Free metadata for the application (e.g. the permission a route requires). ---
     360              :     // The library does not interpret it; middlewares can read it via request::get_matched_route().
     361              :     route& meta(const std::string& key, nlohmann::json value);
     362              :     bool has_meta(const std::string& key) const;
     363              :     const nlohmann::json& get_meta(const std::string& key) const;  // null if missing
     364            3 :     const nlohmann::json& get_metadata() const { return metadata_; }
     365              :     
     366              :     // Check if route matches the given path. The regex of the whole pattern is compiled by
     367              :     // the first call (routing does not use it); calls from several threads are safe
     368              :     bool matches(const std::string& path, std::smatch& matches) const;
     369              :     
     370              :     // Get route parameters from regex
     371          131 :     const std::vector<std::string>& get_parameters() const { return parameters_; }
     372              :     
     373              :     // Handle the request: decode and validate the body for callbacks taking it, then run
     374              :     // the callback
     375              :     thinger::awaitable<void> handle_request_coro(request& req, response& res) const;
     376              :     
     377              :     // Get the original pattern
     378        66588 :     const std::string& get_pattern() const { return pattern_; }
     379              :     
     380              : private:
     381              :     struct lazy_regex;
     382              : 
     383              :     std::string pattern_;
     384              :     std::shared_ptr<lazy_regex> regex_;  // shared by the copies of the route
     385              :     std::vector<std::string> parameters_;
     386              :     std::map<std::string, std::string> parameter_patterns_;
     387              :     nlohmann::json json_schema_;
     388              :     std::vector<std::string> accepted_types_;
     389              : 
     390              :     // Limits (unset: those of the virtual host, or the application)
     391              :     std::optional<std::uint64_t> max_body_size_;
     392              :     std::optional<std::chrono::milliseconds> timeout_;
     393              :     http_response::status timeout_status_ = http_response::status::gateway_timeout;
     394              :     std::optional<std::chrono::milliseconds> request_timeout_;
     395              :     std::optional<body_limits> decode_limits_;
     396              :     uint8_t cancel_on_ = 0;
     397              : 
     398              :     // Documentation and metadata
     399              :     std::string summary_;
     400              :     std::string description_;
     401              :     std::vector<std::string> tags_;
     402              :     std::string operation_id_;
     403              :     bool deprecated_ = false;
     404              :     std::vector<route_parameter> param_docs_;
     405              :     std::map<int, route_response> responses_;
     406              :     std::vector<nlohmann::json> examples_;
     407              :     bool hidden_ = false;
     408              :     nlohmann::json metadata_ = nlohmann::json::object();
     409              :     const nlohmann::json* schema_components_ = nullptr;
     410              :     bool deferred_body_ = false;
     411              :     bool takes_json_body_ = false;
     412              :     bool has_schema_ = false;
     413              :     // The body is decoded (and validated) before the callback: it takes it, or there is a schema
     414              :     bool decodes_body_ = false;
     415              :     // The callback, either synchronous or coroutine (the other one is empty)
     416              :     route_callback_request_response sync_callback_;
     417              :     route_callback_awaitable callback_;
     418              : 
     419              :     // Call a callback with the arguments it takes; those taking the JSON body get the body
     420              :     // parsed by handle_request_coro()
     421              :     template<typename F>
     422        25049 :     static decltype(auto) invoke_callback(F& callback, request& req, response& res) {
     423              :         if constexpr (request_response_callback<F>) {
     424         9416 :             return callback(req, res);
     425              :         } else if constexpr (response_callback<F>) {
     426        15391 :             return callback(res);
     427              :         } else if constexpr (request_json_callback<F>) {
     428           51 :             return callback(req, req.json_body_, res);
     429              :         } else {
     430          191 :             return callback(req.json_body_, res);
     431              :         }
     432              :     }
     433              : 
     434              :     // Type of the request body against the accepted types (matched once per request, and
     435              :     // kept in it): false, after answering 415, if it is not accepted
     436              :     bool match_body_type(request& req, response& res) const;
     437              : 
     438              :     // Decode the request body and validate it against the schema; responds with an error
     439              :     // and returns false if it cannot be decoded or is not valid
     440              :     bool decode_body(request& req, response& res, nlohmann::json& json) const;
     441              : 
     442              : #ifdef THINGER_HTTP_VALIJSON_ENABLED
     443              :     std::shared_ptr<valijson::Schema> schema_;
     444              :     std::string schema_error_;  // why the schema could not be parsed (requests are rejected)
     445              : 
     446              :     bool validate_json(const nlohmann::json& json, response& res) const;
     447              : #endif
     448              : };
     449              : 
     450              : } // namespace thinger::http
     451              : 
     452              : #endif // THINGER_HTTP_ROUTE_DESCRIPTOR_HPP
        

Generated by: LCOV version 2.0-1