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
|