1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4use std::fmt;
5use wasm_bindgen::prelude::*;
6use wasm_bindgen_futures::JsFuture;
7use web_sys::{AbortSignal, Request as WebRequest, RequestInit, Response as WebResponse};
8
9pub use web_sys::{AbortController, RequestMode};
10
11pub type Result<T> = std::result::Result<T, Error>;
12
13#[derive(Debug)]
15pub enum Error {
16 JsError(JsValue),
18 HttpError(u16, String),
20 JsonError(String),
22 Aborted,
24}
25
26impl From<JsValue> for Error {
27 fn from(value: JsValue) -> Self {
28 Error::JsError(value)
29 }
30}
31
32impl From<serde_json::Error> for Error {
33 fn from(err: serde_json::Error) -> Self {
34 Error::JsonError(err.to_string())
35 }
36}
37
38impl fmt::Display for Error {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 Error::JsError(e) => write!(f, "JavaScript error: {:?}", e),
42 Error::HttpError(status, msg) => write!(f, "HTTP error {}: {}", status, msg),
43 Error::JsonError(e) => write!(f, "JSON error: {}", e),
44 Error::Aborted => write!(f, "Request was aborted"),
45 }
46 }
47}
48
49impl std::error::Error for Error {}
50
51#[derive(Debug, Clone, Copy)]
53pub enum Method {
54 GET,
55 POST,
56 PUT,
57 DELETE,
58 PATCH,
59 HEAD,
60 OPTIONS,
61}
62
63impl Method {
64 fn as_str(&self) -> &'static str {
65 match self {
66 Method::GET => "GET",
67 Method::POST => "POST",
68 Method::PUT => "PUT",
69 Method::DELETE => "DELETE",
70 Method::PATCH => "PATCH",
71 Method::HEAD => "HEAD",
72 Method::OPTIONS => "OPTIONS",
73 }
74 }
75}
76
77pub struct RequestBuilder {
79 url: String,
80 method: Method,
81 headers: HashMap<String, String>,
82 body: Option<String>,
83 mode: RequestMode,
84 signal: Option<AbortSignal>,
85}
86
87impl RequestBuilder {
88 fn new(method: Method, url: impl Into<String>) -> Self {
89 Self {
90 url: url.into(),
91 method,
92 headers: HashMap::new(),
93 body: None,
94 mode: RequestMode::Cors,
95 signal: None,
96 }
97 }
98
99 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
101 self.headers.insert(key.into(), value.into());
102 self
103 }
104
105 pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
107 self.headers.extend(headers);
108 self
109 }
110
111 pub fn mode(mut self, mode: RequestMode) -> Self {
113 self.mode = mode;
114 self
115 }
116
117 pub fn body(mut self, body: impl Into<String>) -> Self {
119 self.body = Some(body.into());
120 self
121 }
122
123 pub fn json<T: Serialize>(mut self, json: &T) -> Result<Self> {
125 let body = serde_json::to_string(json)?;
126 self.body = Some(body);
127 self.headers
128 .insert("Content-Type".to_string(), "application/json".to_string());
129 Ok(self)
130 }
131
132 pub fn abort_signal(mut self, signal: AbortSignal) -> Self {
134 self.signal = Some(signal);
135 self
136 }
137
138 pub async fn send(self) -> Result<Response> {
140 let opts = RequestInit::new();
141 opts.set_method(self.method.as_str());
142 opts.set_mode(self.mode);
143
144 if let Some(body) = &self.body {
145 opts.set_body(&JsValue::from_str(body));
146 }
147
148 if let Some(signal) = &self.signal {
149 opts.set_signal(Some(signal));
150 }
151
152 let request = WebRequest::new_with_str_and_init(&self.url, &opts)?;
153 let headers = request.headers();
154
155 for (key, value) in &self.headers {
156 headers.set(key, value)?;
157 }
158
159 let window = web_sys::window()
160 .ok_or_else(|| Error::JsError(JsValue::from_str("Failed to get window")))?;
161
162 let resp_value = JsFuture::from(window.fetch_with_request(&request))
163 .await
164 .map_err(|e| {
165 if let Some(error) = e.dyn_ref::<js_sys::Error>() {
167 if error.name() == "AbortError" {
168 return Error::Aborted;
169 }
170 }
171 Error::JsError(e)
172 })?;
173 let web_response: WebResponse = resp_value
174 .dyn_into()
175 .map_err(|_| Error::JsError(JsValue::from_str("Response conversion failed")))?;
176
177 Ok(Response::from_web_response(web_response))
178 }
179}
180
181pub struct Response {
183 inner: WebResponse,
184}
185
186impl Response {
187 fn from_web_response(response: WebResponse) -> Self {
188 Self { inner: response }
189 }
190
191 pub fn status(&self) -> u16 {
193 self.inner.status()
194 }
195
196 pub fn ok(&self) -> bool {
198 self.inner.ok()
199 }
200
201 pub fn header(&self, name: &str) -> Result<Option<String>> {
203 Ok(self.inner.headers().get(name)?)
204 }
205
206 pub async fn text(&self) -> Result<String> {
208 let promise = self.inner.text().map_err(Error::JsError)?;
209 let text = JsFuture::from(promise).await?;
210
211 text.as_string()
212 .ok_or_else(|| Error::JsError(JsValue::from_str("Failed to convert to string")))
213 }
214
215 pub async fn json<T: for<'de> Deserialize<'de>>(&self) -> Result<T> {
217 let text = self.text().await?;
218 Ok(serde_json::from_str(&text)?)
219 }
220
221 pub async fn json_value(&self) -> Result<Value> {
223 self.json().await
224 }
225
226 pub async fn bytes(&self) -> Result<Vec<u8>> {
228 let promise = self.inner.array_buffer().map_err(Error::JsError)?;
229 let array_buffer = JsFuture::from(promise).await?;
230 let uint8_array = js_sys::Uint8Array::new(&array_buffer);
231 Ok(uint8_array.to_vec())
232 }
233
234 pub fn error_for_status(self) -> Result<Self> {
236 if self.ok() {
237 Ok(self)
238 } else {
239 let status = self.status();
240 let text = format!("HTTP Error {}", status);
241 Err(Error::HttpError(status, text))
242 }
243 }
244}
245
246pub struct Client;
248
249impl Client {
250 pub fn get(&self, url: impl Into<String>) -> RequestBuilder {
252 RequestBuilder::new(Method::GET, url)
253 }
254
255 pub fn post(&self, url: impl Into<String>) -> RequestBuilder {
257 RequestBuilder::new(Method::POST, url)
258 }
259
260 pub fn put(&self, url: impl Into<String>) -> RequestBuilder {
262 RequestBuilder::new(Method::PUT, url)
263 }
264
265 pub fn delete(&self, url: impl Into<String>) -> RequestBuilder {
267 RequestBuilder::new(Method::DELETE, url)
268 }
269
270 pub fn patch(&self, url: impl Into<String>) -> RequestBuilder {
272 RequestBuilder::new(Method::PATCH, url)
273 }
274
275 pub fn head(&self, url: impl Into<String>) -> RequestBuilder {
277 RequestBuilder::new(Method::HEAD, url)
278 }
279}
280
281pub async fn get(url: impl Into<String>) -> Result<Response> {
283 Client.get(url).send().await
284}
285
286pub async fn post_json<T: Serialize>(url: impl Into<String>, json: &T) -> Result<Response> {
288 Client.post(url).json(json)?.send().await
289}