fetch_happen/
lib.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4use wasm_bindgen::prelude::*;
5use wasm_bindgen_futures::JsFuture;
6use web_sys::{Request as WebRequest, RequestInit, Response as WebResponse};
7
8pub use web_sys::RequestMode;
9
10pub type Result<T> = std::result::Result<T, Error>;
11
12/// Errors that can occur when making a request
13#[derive(Debug)]
14pub enum Error {
15    /// JavaScript error
16    JsError(JsValue),
17    /// HTTP error with status code
18    HttpError(u16, String),
19    /// JSON parsing error
20    JsonError(String),
21}
22
23impl From<JsValue> for Error {
24    fn from(value: JsValue) -> Self {
25        Error::JsError(value)
26    }
27}
28
29impl From<serde_json::Error> for Error {
30    fn from(err: serde_json::Error) -> Self {
31        Error::JsonError(err.to_string())
32    }
33}
34
35/// HTTP methods
36#[derive(Debug, Clone, Copy)]
37pub enum Method {
38    GET,
39    POST,
40    PUT,
41    DELETE,
42    PATCH,
43    HEAD,
44    OPTIONS,
45}
46
47impl Method {
48    fn as_str(&self) -> &'static str {
49        match self {
50            Method::GET => "GET",
51            Method::POST => "POST",
52            Method::PUT => "PUT",
53            Method::DELETE => "DELETE",
54            Method::PATCH => "PATCH",
55            Method::HEAD => "HEAD",
56            Method::OPTIONS => "OPTIONS",
57        }
58    }
59}
60
61/// A builder for HTTP requests
62pub struct RequestBuilder {
63    url: String,
64    method: Method,
65    headers: HashMap<String, String>,
66    body: Option<String>,
67    mode: RequestMode,
68}
69
70impl RequestBuilder {
71    fn new(method: Method, url: impl Into<String>) -> Self {
72        Self {
73            url: url.into(),
74            method,
75            headers: HashMap::new(),
76            body: None,
77            mode: RequestMode::Cors,
78        }
79    }
80
81    /// Set a header
82    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
83        self.headers.insert(key.into(), value.into());
84        self
85    }
86
87    /// Set multiple headers
88    pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
89        self.headers.extend(headers);
90        self
91    }
92
93    /// Set the request mode (Cors, NoCors, SameOrigin)
94    pub fn mode(mut self, mode: RequestMode) -> Self {
95        self.mode = mode;
96        self
97    }
98
99    /// Set the request body as a string
100    pub fn body(mut self, body: impl Into<String>) -> Self {
101        self.body = Some(body.into());
102        self
103    }
104
105    /// Set the request body as JSON
106    pub fn json<T: Serialize>(mut self, json: &T) -> Result<Self> {
107        let body = serde_json::to_string(json)?;
108        self.body = Some(body);
109        self.headers
110            .insert("Content-Type".to_string(), "application/json".to_string());
111        Ok(self)
112    }
113
114    /// Send the request and get a Response
115    pub async fn send(self) -> Result<Response> {
116        let opts = RequestInit::new();
117        opts.set_method(self.method.as_str());
118        opts.set_mode(self.mode);
119
120        if let Some(body) = &self.body {
121            opts.set_body(&JsValue::from_str(body));
122        }
123
124        let request = WebRequest::new_with_str_and_init(&self.url, &opts)?;
125        let headers = request.headers();
126
127        for (key, value) in &self.headers {
128            headers.set(key, value)?;
129        }
130
131        let window = web_sys::window()
132            .ok_or_else(|| Error::JsError(JsValue::from_str("Failed to get window")))?;
133
134        let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
135        let web_response: WebResponse = resp_value
136            .dyn_into()
137            .map_err(|_| Error::JsError(JsValue::from_str("Response conversion failed")))?;
138
139        Ok(Response::from_web_response(web_response))
140    }
141}
142
143/// A response from a fetch request
144pub struct Response {
145    inner: WebResponse,
146}
147
148impl Response {
149    fn from_web_response(response: WebResponse) -> Self {
150        Self { inner: response }
151    }
152
153    /// Get the status code
154    pub fn status(&self) -> u16 {
155        self.inner.status()
156    }
157
158    /// Check if the response was successful (status 200-299)
159    pub fn ok(&self) -> bool {
160        self.inner.ok()
161    }
162
163    /// Get a header value
164    pub fn header(&self, name: &str) -> Result<Option<String>> {
165        Ok(self.inner.headers().get(name)?)
166    }
167
168    /// Get the response body as text
169    pub async fn text(&self) -> Result<String> {
170        let promise = self.inner.text().map_err(Error::JsError)?;
171        let text = JsFuture::from(promise).await?;
172
173        text.as_string()
174            .ok_or_else(|| Error::JsError(JsValue::from_str("Failed to convert to string")))
175    }
176
177    /// Get the response body as JSON
178    pub async fn json<T: for<'de> Deserialize<'de>>(&self) -> Result<T> {
179        let text = self.text().await?;
180        Ok(serde_json::from_str(&text)?)
181    }
182
183    /// Get the response body as a dynamic JSON value
184    pub async fn json_value(&self) -> Result<Value> {
185        self.json().await
186    }
187
188    /// Get the response body as bytes
189    pub async fn bytes(&self) -> Result<Vec<u8>> {
190        let promise = self.inner.array_buffer().map_err(Error::JsError)?;
191        let array_buffer = JsFuture::from(promise).await?;
192        let uint8_array = js_sys::Uint8Array::new(&array_buffer);
193        Ok(uint8_array.to_vec())
194    }
195
196    /// Ensure the response was successful, returning an error if not
197    pub fn error_for_status(self) -> Result<Self> {
198        if self.ok() {
199            Ok(self)
200        } else {
201            let status = self.status();
202            let text = format!("HTTP Error {}", status);
203            Err(Error::HttpError(status, text))
204        }
205    }
206}
207
208/// Main client for making HTTP requests
209pub struct Client;
210
211impl Client {
212    /// Make a GET request
213    pub fn get(&self, url: impl Into<String>) -> RequestBuilder {
214        RequestBuilder::new(Method::GET, url)
215    }
216
217    /// Make a POST request
218    pub fn post(&self, url: impl Into<String>) -> RequestBuilder {
219        RequestBuilder::new(Method::POST, url)
220    }
221
222    /// Make a PUT request
223    pub fn put(&self, url: impl Into<String>) -> RequestBuilder {
224        RequestBuilder::new(Method::PUT, url)
225    }
226
227    /// Make a DELETE request
228    pub fn delete(&self, url: impl Into<String>) -> RequestBuilder {
229        RequestBuilder::new(Method::DELETE, url)
230    }
231
232    /// Make a PATCH request
233    pub fn patch(&self, url: impl Into<String>) -> RequestBuilder {
234        RequestBuilder::new(Method::PATCH, url)
235    }
236
237    /// Make a HEAD request
238    pub fn head(&self, url: impl Into<String>) -> RequestBuilder {
239        RequestBuilder::new(Method::HEAD, url)
240    }
241}
242
243/// Convenience function for making a GET request
244pub async fn get(url: impl Into<String>) -> Result<Response> {
245    Client.get(url).send().await
246}
247
248/// Convenience function for making a POST request with JSON body
249pub async fn post_json<T: Serialize>(url: impl Into<String>, json: &T) -> Result<Response> {
250    Client.post(url).json(json)?.send().await
251}