LCOV - code coverage report
Current view: top level - http/client - request_builder.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 98.9 % 94 93
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 81 81

            Line data    Source code
       1              : #ifndef THINGER_HTTP_CLIENT_REQUEST_BUILDER_HPP
       2              : #define THINGER_HTTP_CLIENT_REQUEST_BUILDER_HPP
       3              : 
       4              : #include "../common/http_request.hpp"
       5              : #include "client_response.hpp"
       6              : #include "stream_types.hpp"
       7              : #include "websocket_client.hpp"
       8              : #include "form.hpp"
       9              : #include <filesystem>
      10              : #include <fstream>
      11              : #include <string>
      12              : #include <map>
      13              : #include <functional>
      14              : #include <optional>
      15              : 
      16              : namespace thinger::http {
      17              : 
      18              : /**
      19              :  * Fluent builder for HTTP requests with streaming support.
      20              :  * Works with both sync (client) and async (async_client) clients.
      21              :  *
      22              :  * Usage with sync client:
      23              :  *   auto res = client.request("https://api.com/data")
      24              :  *       .header("Authorization", "Bearer xxx")
      25              :  *       .get();  // Returns client_response
      26              :  *
      27              :  * Usage with async client:
      28              :  *   auto res = co_await async_client.request("https://api.com/data")
      29              :  *       .header("Authorization", "Bearer xxx")
      30              :  *       .get();  // Returns awaitable<client_response>
      31              :  */
      32              : template<typename Client>
      33              : class request_builder {
      34              : public:
      35          308 :     request_builder(Client* c, const std::string& url)
      36          308 :         : client_(c)
      37          308 :         , request_(std::make_shared<http_request>()) {
      38          308 :         request_->set_url(url);
      39          308 :     }
      40              : 
      41              :     // Non-copyable, movable
      42              :     request_builder(const request_builder&) = delete;
      43              :     request_builder& operator=(const request_builder&) = delete;
      44              :     request_builder(request_builder&&) = default;
      45              :     request_builder& operator=(request_builder&&) = default;
      46              : 
      47              :     // ============================================
      48              :     // Configuration methods (chainable)
      49              :     // ============================================
      50              : 
      51          162 :     request_builder& header(const std::string& name, const std::string& value) {
      52          162 :         request_->add_header(name, value);
      53          162 :         return *this;
      54              :     }
      55              : 
      56            7 :     request_builder& headers(const std::map<std::string, std::string>& hdrs) {
      57           23 :         for (const auto& [name, value] : hdrs) {
      58           16 :             request_->add_header(name, value);
      59              :         }
      60            7 :         return *this;
      61              :     }
      62              : 
      63           44 :     request_builder& body(std::string content, std::string content_type = "application/json") {
      64           44 :         body_content_ = std::move(content);
      65           44 :         body_content_type_ = std::move(content_type);
      66           44 :         return *this;
      67              :     }
      68              : 
      69            4 :     request_builder& body(const form& f) {
      70            4 :         body_content_ = f.body();
      71            4 :         body_content_type_ = f.content_type();
      72            4 :         return *this;
      73              :     }
      74              : 
      75            3 :     request_builder& unix_socket(const std::string& path) {
      76            3 :         request_->set_unix_socket(path);
      77            3 :         return *this;
      78              :     }
      79              : 
      80              :     // Proxy for this request (and its redirects), instead of the one of the client: one URL
      81              :     // for every scheme ("" to connect directly), and the hosts reached directly
      82            9 :     request_builder& proxy(const std::string& url, const std::string& no_proxy = "") {
      83           18 :         request_->set_proxy_config(proxy_config{url, url, no_proxy});
      84            9 :         return *this;
      85            9 :     }
      86              : 
      87              :     request_builder& proxy(proxy_config config) {
      88              :         request_->set_proxy_config(std::move(config));
      89              :         return *this;
      90              :     }
      91              : 
      92              :     // CAs trusted to verify the server of this request, besides those of the client: a PEM
      93              :     // file, or PEM text
      94            3 :     request_builder& ca_file(std::string path) {
      95            3 :         request_->add_ca_file(std::move(path));
      96            3 :         return *this;
      97              :     }
      98              : 
      99           12 :     request_builder& ca_pem(std::string pem) {
     100           12 :         request_->add_ca_pem(std::move(pem));
     101           12 :         return *this;
     102              :     }
     103              : 
     104           11 :     request_builder& protocol(std::string proto) {
     105           11 :         protocol_ = std::move(proto);
     106           11 :         return *this;
     107              :     }
     108              : 
     109              :     // ============================================
     110              :     // Terminal methods - return type depends on Client
     111              :     // sync client: returns value directly
     112              :     // async client: returns awaitable
     113              :     // ============================================
     114              : 
     115          102 :     auto get() {
     116          102 :         return execute(method::GET);
     117              :     }
     118              : 
     119           62 :     auto get(stream_callback callback) {
     120           62 :         return execute_streaming(method::GET, std::move(callback));
     121              :     }
     122              : 
     123           32 :     auto post() {
     124           32 :         return execute(method::POST);
     125              :     }
     126              : 
     127              :     auto post(stream_callback callback) {
     128              :         return execute_streaming(method::POST, std::move(callback));
     129              :     }
     130              : 
     131            4 :     auto put() {
     132            4 :         return execute(method::PUT);
     133              :     }
     134              : 
     135              :     auto put(stream_callback callback) {
     136              :         return execute_streaming(method::PUT, std::move(callback));
     137              :     }
     138              : 
     139            2 :     auto patch() {
     140            2 :         return execute(method::PATCH);
     141              :     }
     142              : 
     143              :     auto patch(stream_callback callback) {
     144              :         return execute_streaming(method::PATCH, std::move(callback));
     145              :     }
     146              : 
     147            4 :     auto del() {
     148            4 :         return execute(method::DELETE);
     149              :     }
     150              : 
     151              :     auto del(stream_callback callback) {
     152              :         return execute_streaming(method::DELETE, std::move(callback));
     153              :     }
     154              : 
     155            2 :     auto head() {
     156            2 :         return execute(method::HEAD);
     157              :     }
     158              : 
     159            2 :     auto options() {
     160            2 :         return execute(method::OPTIONS);
     161              :     }
     162              : 
     163              :     /**
     164              :      * Download response body to a file.
     165              :      * For sync client: blocks and returns stream_result
     166              :      * For async client: returns awaitable<stream_result>
     167              :      */
     168              :     auto download(const std::filesystem::path& path, progress_callback progress = {}) {
     169              :         // Open file for writing
     170              :         auto file = std::make_shared<std::ofstream>(path, std::ios::binary);
     171              :         if (!*file) {
     172              :             if constexpr (requires { client_->send(request_).body(); }) {
     173              :                 // Sync client - return error directly
     174              :                 stream_result result;
     175              :                 result.error = "Cannot open file for writing: " + path.string();
     176              :                 return result;
     177              :             } else {
     178              :                 // Async client - this path won't be taken at runtime due to the check above
     179              :                 // but we need to return the right type
     180              :             }
     181              :         }
     182              : 
     183              :         // Stream to file
     184              :         return get([file, progress](const stream_info& info) {
     185              :             file->write(info.data.data(), static_cast<std::streamsize>(info.data.size()));
     186              :             if (progress) {
     187              :                 progress(info.downloaded, info.total);
     188              :             }
     189              :             return true;
     190              :         });
     191              :     }
     192              : 
     193              :     /**
     194              :      * Upgrade connection to WebSocket.
     195              :      * For sync client: returns std::optional<websocket_client>
     196              :      * For async client: returns awaitable<std::optional<websocket_client>>
     197              :      */
     198           21 :     auto websocket() {
     199           21 :         return client_->websocket(request_, protocol_);
     200              :     }
     201              : 
     202              :     // ============================================
     203              :     // Callback-based terminal methods (async_client only)
     204              :     // ============================================
     205              :     // These are only available for async_client, which has a run() method.
     206              :     // They allow using the builder pattern without coroutines.
     207              :     //
     208              :     // Usage:
     209              :     //   client.request("https://api.com/data")
     210              :     //       .header("Authorization", "Bearer xxx")
     211              :     //       .get([](auto& response) {
     212              :     //           std::cout << response.body() << std::endl;
     213              :     //       });
     214              :     //   client.wait();
     215              : 
     216              :     template<typename Callback>
     217              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     218           45 :     void get(Callback&& callback) {
     219           45 :         execute_with_callback(method::GET, std::forward<Callback>(callback));
     220           45 :     }
     221              : 
     222              :     template<typename Callback>
     223              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     224            2 :     void post(Callback&& callback) {
     225            2 :         execute_with_callback(method::POST, std::forward<Callback>(callback));
     226            2 :     }
     227              : 
     228              :     template<typename Callback>
     229              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     230            2 :     void put(Callback&& callback) {
     231            2 :         execute_with_callback(method::PUT, std::forward<Callback>(callback));
     232            2 :     }
     233              : 
     234              :     template<typename Callback>
     235              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     236            2 :     void patch(Callback&& callback) {
     237            2 :         execute_with_callback(method::PATCH, std::forward<Callback>(callback));
     238            2 :     }
     239              : 
     240              :     template<typename Callback>
     241              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     242            2 :     void del(Callback&& callback) {
     243            2 :         execute_with_callback(method::DELETE, std::forward<Callback>(callback));
     244            2 :     }
     245              : 
     246              :     template<typename Callback>
     247              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     248            2 :     void head(Callback&& callback) {
     249            2 :         execute_with_callback(method::HEAD, std::forward<Callback>(callback));
     250            2 :     }
     251              : 
     252              :     template<typename Callback>
     253              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     254            2 :     void options(Callback&& callback) {
     255            2 :         execute_with_callback(method::OPTIONS, std::forward<Callback>(callback));
     256            2 :     }
     257              : 
     258              :     template<typename Callback>
     259              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     260           10 :     void websocket(Callback&& callback) {
     261           10 :         client_->websocket_with_callback(request_, protocol_, std::forward<Callback>(callback));
     262           10 :     }
     263              : 
     264              : private:
     265              :     Client* client_;
     266              :     std::shared_ptr<http_request> request_;
     267              :     std::string body_content_;
     268              :     std::string body_content_type_;
     269              :     std::string protocol_;
     270              : 
     271          148 :     auto execute(method m) {
     272          148 :         request_->set_method(m);
     273          148 :         if (!body_content_.empty()) {
     274           38 :             request_->set_content(std::move(body_content_), std::move(body_content_type_));
     275              :         }
     276          148 :         return client_->send(request_);
     277              :     }
     278              : 
     279           62 :     auto execute_streaming(method m, stream_callback callback) {
     280           62 :         request_->set_method(m);
     281           62 :         if (!body_content_.empty()) {
     282            0 :             request_->set_content(std::move(body_content_), std::move(body_content_type_));
     283              :         }
     284           62 :         return client_->send_streaming(request_, std::move(callback));
     285              :     }
     286              : 
     287              :     template<typename Callback>
     288           57 :     void execute_with_callback(method m, Callback&& callback) {
     289           57 :         request_->set_method(m);
     290           57 :         if (!body_content_.empty()) {
     291            6 :             request_->set_content(std::move(body_content_), std::move(body_content_type_));
     292              :         }
     293          111 :         client_->run_with_callback([req = request_](auto c) mutable { return c.client.send(std::move(req)); },
     294              :                                    std::forward<Callback>(callback));
     295           57 :     }
     296              : };
     297              : 
     298              : } // namespace thinger::http
     299              : 
     300              : #endif // THINGER_HTTP_CLIENT_REQUEST_BUILDER_HPP
        

Generated by: LCOV version 2.0-1