forked from FastLED/FastLED
-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathfetch.h
More file actions
326 lines (269 loc) · 11 KB
/
fetch.h
File metadata and controls
326 lines (269 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#pragma once
/// @file fetch.h
/// @brief Unified HTTP fetch API for FastLED (cross-platform)
///
/// This API provides both simple callback-based and JavaScript-like promise-based interfaces
/// for HTTP requests. Works on WASM/browser platforms with real fetch, provides stubs on embedded.
///
/// **WASM Optimization:** On WASM platforms, `delay()` automatically pumps all async tasks
/// (fetch, timers, etc.) in 1ms intervals, making delay time useful for processing async operations.
///
/// @section Simple Callback Usage
/// @code
/// #include "fl/fetch.h"
///
/// void setup() {
/// // Simple callback-based fetch (backward compatible)
/// fl::fetch("http://fastled.io", [](const fl::response& resp) {
/// if (resp.ok()) {
/// FL_WARN("Success: " << resp.text());
/// }
/// });
/// }
/// @endcode
///
/// @section Promise Usage
/// @code
/// #include "fl/fetch.h"
///
/// void setup() {
/// // JavaScript-like fetch with promises
/// fl::fetch_get("http://fastled.io")
/// .then([](const fl::response& resp) {
/// if (resp.ok()) {
/// FL_WARN("Success: " << resp.text());
/// } else {
/// FL_WARN("HTTP Error: " << resp.status() << " " << resp.status_text());
/// }
/// })
/// .catch_([](const fl::Error& err) {
/// FL_WARN("Fetch Error: " << err.message);
/// });
/// }
///
/// void loop() {
/// // Fetch promises are automatically updated through FastLED's engine events!
/// // On WASM platforms, delay() also pumps all async tasks automatically.
/// // No manual updates needed - just use normal FastLED loop
/// FastLED.show();
/// delay(16); // delay() automatically pumps all async tasks on WASM
/// }
/// @endcode
#include "fl/namespace.h"
#include "fl/promise.h"
#include "fl/string.h"
#include "fl/vector.h"
#include "fl/map.h"
#include "fl/hash_map.h"
#include "fl/optional.h"
#include "fl/function.h"
#include "fl/ptr.h"
#include "fl/async.h"
#include "fl/mutex.h"
#include "fl/warn.h"
#include "fl/json.h" // Add JSON support for response.json() method
namespace fl {
// Forward declarations
class fetch_options;
class FetchManager;
class response;
#ifdef __EMSCRIPTEN__
// Forward declarations for WASM-specific types (defined in platforms/wasm/js_fetch.h)
class WasmFetchRequest;
class WasmFetch;
extern WasmFetch wasm_fetch;
#endif
/// HTTP response class (unified interface)
class response {
public:
response() : mStatusCode(200), mStatusText("OK") {}
response(int status_code) : mStatusCode(status_code), mStatusText(get_default_status_text(status_code)) {}
response(int status_code, const fl::string& status_text)
: mStatusCode(status_code), mStatusText(status_text) {}
/// HTTP status code (like JavaScript response.status)
int status() const { return mStatusCode; }
/// HTTP status text (like JavaScript response.statusText)
const fl::string& status_text() const { return mStatusText; }
/// Check if response is successful (like JavaScript response.ok)
bool ok() const { return mStatusCode >= 200 && mStatusCode < 300; }
/// Response body as text (like JavaScript response.text())
const fl::string& text() const { return mBody; }
/// Get header value (like JavaScript response.headers.get())
fl::optional<fl::string> get_header(const fl::string& name) const {
auto it = mHeaders.find(name);
if (it != mHeaders.end()) {
return fl::make_optional(it->second);
}
return fl::nullopt;
}
/// Get content type convenience method
fl::optional<fl::string> get_content_type() const {
return get_header("content-type");
}
/// Response body as text (alternative to text())
const fl::string& get_body_text() const { return mBody; }
/// Response body parsed as JSON (JavaScript-like API)
/// @return fl::Json object for safe, ergonomic access
/// @note Automatically parses JSON on first call, caches result
/// @note Returns null JSON object for non-JSON or malformed content
fl::Json json() const;
/// Check if response appears to contain JSON content
/// @return true if Content-Type header indicates JSON or body contains JSON markers
bool is_json() const {
auto content_type = get_content_type();
if (content_type.has_value()) {
fl::string ct = *content_type;
// Check for various JSON content types (case-insensitive)
return ct.find("json") != fl::string::npos;
}
return false;
}
/// Set methods (internal use)
void set_status(int status_code) { mStatusCode = status_code; }
void set_status_text(const fl::string& status_text) { mStatusText = status_text; }
void set_text(const fl::string& body) { mBody = body; } // Backward compatibility
void set_body(const fl::string& body) { mBody = body; }
void set_header(const fl::string& name, const fl::string& value) {
mHeaders[name] = value;
}
private:
int mStatusCode;
fl::string mStatusText;
fl::string mBody;
fl_map<fl::string, fl::string> mHeaders;
// JSON parsing cache
mutable fl::optional<fl::Json> mCachedJson; // Lazy-loaded JSON cache
mutable bool mJsonParsed = false; // Track parsing attempts
/// Parse JSON from response body with error handling
fl::Json parse_json_body() const {
fl::Json parsed = fl::Json::parse(mBody);
if (parsed.is_null() && (!mBody.empty())) {
// If parsing failed but we have content, return null JSON
// This allows safe chaining: resp.json()["key"] | default
return fl::Json(nullptr);
}
return parsed;
}
static fl::string get_default_status_text(int status) {
switch (status) {
case 200: return "OK";
case 400: return "Bad Request";
case 401: return "Unauthorized";
case 403: return "Forbidden";
case 404: return "Not Found";
case 500: return "Internal Server Error";
case 501: return "Not Implemented";
case 502: return "Bad Gateway";
case 503: return "Service Unavailable";
default: return "Unknown";
}
}
};
/// Callback type for simple fetch responses (backward compatible)
using FetchCallback = fl::function<void(const response&)>;
/// Request options (matches JavaScript fetch RequestInit)
struct RequestOptions {
fl::string method = "GET";
fl_map<fl::string, fl::string> headers;
fl::string body;
int timeout_ms = 10000; // 10 second default
RequestOptions() = default;
RequestOptions(const fl::string& method_name) : method(method_name) {}
};
/// Fetch options builder (fluent interface)
class fetch_options {
public:
explicit fetch_options(const fl::string& url) : mUrl(url) {}
fetch_options(const fl::string& url, const RequestOptions& options)
: mUrl(url), mOptions(options) {}
/// Set HTTP method
fetch_options& method(const fl::string& http_method) {
mOptions.method = http_method;
return *this;
}
/// Add header
fetch_options& header(const fl::string& name, const fl::string& value) {
mOptions.headers[name] = value;
return *this;
}
/// Set request body
fetch_options& body(const fl::string& data) {
mOptions.body = data;
return *this;
}
/// Set JSON body with proper content type
fetch_options& json(const fl::string& json_data) {
mOptions.body = json_data;
mOptions.headers["Content-Type"] = "application/json";
return *this;
}
/// Set timeout in milliseconds
fetch_options& timeout(int timeout_ms) {
mOptions.timeout_ms = timeout_ms;
return *this;
}
/// Get the URL for this request
const fl::string& url() const { return mUrl; }
/// Get the options for this request
const RequestOptions& options() const { return mOptions; }
private:
fl::string mUrl;
RequestOptions mOptions;
friend class FetchManager;
};
class FetchEngineListener;
/// Internal fetch manager for promise tracking
class FetchManager : public async_runner {
public:
static FetchManager& instance();
void register_promise(const fl::promise<response>& promise);
// async_runner interface
void update() override;
bool has_active_tasks() const override;
size_t active_task_count() const override;
// Legacy API
fl::size active_requests() const;
void cleanup_completed_promises();
private:
fl::vector<fl::promise<response>> mActivePromises;
fl::unique_ptr<FetchEngineListener> mEngineListener;
};
// ========== Simple Callback API (Backward Compatible) ==========
/// @brief Make an HTTP GET request (cross-platform, backward compatible)
/// @param url The URL to fetch
/// @param callback Function to call with the response
///
/// On WASM/browser platforms: Uses native JavaScript fetch() API
/// On Arduino/embedded platforms: Immediately calls callback with error response
void fetch(const fl::string& url, const FetchCallback& callback);
/// @brief Make an HTTP GET request with URL string literal (cross-platform)
/// @param url The URL to fetch (C-string)
/// @param callback Function to call with the response
inline void fetch(const char* url, const FetchCallback& callback) {
fetch(fl::string(url), callback);
}
// ========== Promise-Based API (JavaScript-like) ==========
/// HTTP GET request
fl::promise<response> fetch_get(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP POST request
fl::promise<response> fetch_post(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP PUT request
fl::promise<response> fetch_put(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP DELETE request
fl::promise<response> fetch_delete(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP HEAD request
fl::promise<response> fetch_head(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP OPTIONS request
fl::promise<response> fetch_http_options(const fl::string& url, const fetch_options& request = fetch_options(""));
/// HTTP PATCH request
fl::promise<response> fetch_patch(const fl::string& url, const fetch_options& request = fetch_options(""));
/// Generic request with options (like fetch(url, options))
fl::promise<response> fetch_request(const fl::string& url, const RequestOptions& options = RequestOptions());
/// Legacy manual update for fetch promises (use fl::async_run() for new code)
/// @deprecated Use fl::async_run() instead - this calls async_run() internally
void fetch_update();
/// Get number of active requests
fl::size fetch_active_requests();
/// Internal helper to execute a fetch request and return a promise
fl::promise<response> execute_fetch_request(const fl::string& url, const fetch_options& request);
} // namespace fl