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
|