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
|