LCOV - code coverage report
Current view: top level - http/client - request_builder.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 98.8 % 84 83
Test Date: 2026-04-21 17:49:55 Functions: 100.0 % 61 61

            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          131 :     request_builder(Client* c, const std::string& url)
      36          131 :         : client_(c)
      37          131 :         , request_(std::make_shared<http_request>()) {
      38          131 :         request_->set_url(url);
      39          131 :     }
      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           87 :     request_builder& header(const std::string& name, const std::string& value) {
      52           87 :         request_->add_header(name, value);
      53           87 :         return *this;
      54              :     }
      55              : 
      56            4 :     request_builder& headers(const std::map<std::string, std::string>& hdrs) {
      57           14 :         for (const auto& [name, value] : hdrs) {
      58           10 :             request_->add_header(name, value);
      59              :         }
      60            4 :         return *this;
      61              :     }
      62              : 
      63           29 :     request_builder& body(std::string content, std::string content_type = "application/json") {
      64           29 :         body_content_ = std::move(content);
      65           29 :         body_content_type_ = std::move(content_type);
      66           29 :         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              :     request_builder& unix_socket(const std::string& path) {
      76              :         request_->set_unix_socket(path);
      77              :         return *this;
      78              :     }
      79              : 
      80            8 :     request_builder& protocol(std::string proto) {
      81            8 :         protocol_ = std::move(proto);
      82            8 :         return *this;
      83              :     }
      84              : 
      85              :     // ============================================
      86              :     // Terminal methods - return type depends on Client
      87              :     // sync client: returns value directly
      88              :     // async client: returns awaitable
      89              :     // ============================================
      90              : 
      91           30 :     auto get() {
      92           30 :         return execute(method::GET);
      93              :     }
      94              : 
      95            2 :     auto get(stream_callback callback) {
      96            2 :         return execute_streaming(method::GET, std::move(callback));
      97              :     }
      98              : 
      99           17 :     auto post() {
     100           17 :         return execute(method::POST);
     101              :     }
     102              : 
     103              :     auto post(stream_callback callback) {
     104              :         return execute_streaming(method::POST, std::move(callback));
     105              :     }
     106              : 
     107            4 :     auto put() {
     108            4 :         return execute(method::PUT);
     109              :     }
     110              : 
     111              :     auto put(stream_callback callback) {
     112              :         return execute_streaming(method::PUT, std::move(callback));
     113              :     }
     114              : 
     115            2 :     auto patch() {
     116            2 :         return execute(method::PATCH);
     117              :     }
     118              : 
     119              :     auto patch(stream_callback callback) {
     120              :         return execute_streaming(method::PATCH, std::move(callback));
     121              :     }
     122              : 
     123            4 :     auto del() {
     124            4 :         return execute(method::DELETE);
     125              :     }
     126              : 
     127              :     auto del(stream_callback callback) {
     128              :         return execute_streaming(method::DELETE, std::move(callback));
     129              :     }
     130              : 
     131            2 :     auto head() {
     132            2 :         return execute(method::HEAD);
     133              :     }
     134              : 
     135            2 :     auto options() {
     136            2 :         return execute(method::OPTIONS);
     137              :     }
     138              : 
     139              :     /**
     140              :      * Download response body to a file.
     141              :      * For sync client: blocks and returns stream_result
     142              :      * For async client: returns awaitable<stream_result>
     143              :      */
     144              :     auto download(const std::filesystem::path& path, progress_callback progress = {}) {
     145              :         // Open file for writing
     146              :         auto file = std::make_shared<std::ofstream>(path, std::ios::binary);
     147              :         if (!*file) {
     148              :             if constexpr (requires { client_->send(request_).body(); }) {
     149              :                 // Sync client - return error directly
     150              :                 stream_result result;
     151              :                 result.error = "Cannot open file for writing: " + path.string();
     152              :                 return result;
     153              :             } else {
     154              :                 // Async client - this path won't be taken at runtime due to the check above
     155              :                 // but we need to return the right type
     156              :             }
     157              :         }
     158              : 
     159              :         // Stream to file
     160              :         return get([file, progress](const stream_info& info) {
     161              :             file->write(info.data.data(), static_cast<std::streamsize>(info.data.size()));
     162              :             if (progress) {
     163              :                 progress(info.downloaded, info.total);
     164              :             }
     165              :             return true;
     166              :         });
     167              :     }
     168              : 
     169              :     /**
     170              :      * Upgrade connection to WebSocket.
     171              :      * For sync client: returns std::optional<websocket_client>
     172              :      * For async client: returns awaitable<std::optional<websocket_client>>
     173              :      */
     174           12 :     auto websocket() {
     175           12 :         return client_->websocket(request_, protocol_);
     176              :     }
     177              : 
     178              :     // ============================================
     179              :     // Callback-based terminal methods (async_client only)
     180              :     // ============================================
     181              :     // These are only available for async_client, which has a run() method.
     182              :     // They allow using the builder pattern without coroutines.
     183              :     //
     184              :     // Usage:
     185              :     //   client.request("https://api.com/data")
     186              :     //       .header("Authorization", "Bearer xxx")
     187              :     //       .get([](auto& response) {
     188              :     //           std::cout << response.body() << std::endl;
     189              :     //       });
     190              :     //   client.wait();
     191              : 
     192              :     template<typename Callback>
     193              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     194           30 :     void get(Callback&& callback) {
     195           30 :         execute_with_callback(method::GET, std::forward<Callback>(callback));
     196           30 :     }
     197              : 
     198              :     template<typename Callback>
     199              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     200            2 :     void post(Callback&& callback) {
     201            2 :         execute_with_callback(method::POST, std::forward<Callback>(callback));
     202            2 :     }
     203              : 
     204              :     template<typename Callback>
     205              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     206            2 :     void put(Callback&& callback) {
     207            2 :         execute_with_callback(method::PUT, std::forward<Callback>(callback));
     208            2 :     }
     209              : 
     210              :     template<typename Callback>
     211              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     212            2 :     void patch(Callback&& callback) {
     213            2 :         execute_with_callback(method::PATCH, std::forward<Callback>(callback));
     214            2 :     }
     215              : 
     216              :     template<typename Callback>
     217              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     218            2 :     void del(Callback&& callback) {
     219            2 :         execute_with_callback(method::DELETE, std::forward<Callback>(callback));
     220            2 :     }
     221              : 
     222              :     template<typename Callback>
     223              :         requires requires { std::declval<Client>().run(std::declval<std::function<awaitable<void>()>>()); }
     224            2 :     void head(Callback&& callback) {
     225            2 :         execute_with_callback(method::HEAD, 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 options(Callback&& callback) {
     231            2 :         execute_with_callback(method::OPTIONS, 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            4 :     void websocket(Callback&& callback) {
     237            4 :         auto req = request_;
     238            4 :         auto proto = protocol_;
     239            8 :         client_->run([client = client_, req, proto, cb = std::forward<Callback>(callback)]() mutable -> awaitable<void> {
     240              :             auto result = co_await client->http_client_base::upgrade_websocket(std::move(req), proto);
     241              :             if (result) {
     242              :                 auto ws = std::make_shared<websocket_client>(std::move(*result));
     243              :                 cb(ws);
     244              :             } else {
     245              :                 cb(std::shared_ptr<websocket_client>{});
     246              :             }
     247              :         });
     248            4 :     }
     249              : 
     250              : private:
     251              :     Client* client_;
     252              :     std::shared_ptr<http_request> request_;
     253              :     std::string body_content_;
     254              :     std::string body_content_type_;
     255              :     std::string protocol_;
     256              : 
     257           61 :     auto execute(method m) {
     258           61 :         request_->set_method(m);
     259           61 :         if (!body_content_.empty()) {
     260           23 :             request_->set_content(std::move(body_content_), std::move(body_content_type_));
     261              :         }
     262           61 :         return client_->send(request_);
     263              :     }
     264              : 
     265            2 :     auto execute_streaming(method m, stream_callback callback) {
     266            2 :         request_->set_method(m);
     267            2 :         if (!body_content_.empty()) {
     268            0 :             request_->set_content(std::move(body_content_), std::move(body_content_type_));
     269              :         }
     270            2 :         return client_->send_streaming(request_, std::move(callback));
     271              :     }
     272              : 
     273              :     template<typename Callback>
     274           42 :     void execute_with_callback(method m, Callback&& callback) {
     275           42 :         request_->set_method(m);
     276           42 :         if (!body_content_.empty()) {
     277            6 :             request_->set_content(std::move(body_content_), std::move(body_content_type_));
     278              :         }
     279           42 :         auto req = request_;
     280           84 :         client_->run([client = client_, req, cb = std::forward<Callback>(callback)]() mutable -> awaitable<void> {
     281              :             auto response = co_await client->http_client_base::send(std::move(req));
     282              :             cb(response);
     283              :         });
     284           42 :     }
     285              : };
     286              : 
     287              : } // namespace thinger::http
     288              : 
     289              : #endif // THINGER_HTTP_CLIENT_REQUEST_BUILDER_HPP
        

Generated by: LCOV version 2.0-1