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

Generated by: LCOV version 2.0-1