ehttp/
types.rs

1#[cfg(feature = "json")]
2use serde::Serialize;
3
4#[cfg(feature = "multipart")]
5use crate::multipart::MultipartBuilder;
6
7/// Headers in a [`Request`] or [`Response`].
8///
9/// Note that the same header key can appear twice.
10#[derive(Clone, Debug, Default)]
11pub struct Headers {
12    /// Name-value pairs.
13    pub headers: Vec<(String, String)>,
14}
15
16impl Headers {
17    /// ```
18    /// use ehttp::Request;
19    /// let request = Request {
20    ///     headers: ehttp::Headers::new(&[
21    ///         ("Accept", "*/*"),
22    ///         ("Content-Type", "text/plain; charset=utf-8"),
23    ///     ]),
24    ///     ..Request::get("https://www.example.com")
25    /// };
26    /// ```
27    pub fn new(headers: &[(&str, &str)]) -> Self {
28        Self {
29            headers: headers
30                .iter()
31                .map(|e| (e.0.to_owned(), e.1.to_owned()))
32                .collect(),
33        }
34    }
35
36    /// Will add the key/value pair to the headers.
37    ///
38    /// If the key already exists, it will also be kept,
39    /// so the same key can appear twice.
40    pub fn insert(&mut self, key: impl ToString, value: impl ToString) {
41        self.headers.push((key.to_string(), value.to_string()));
42    }
43
44    /// Get the value of the first header with the given key.
45    ///
46    /// The lookup is case-insensitive.
47    pub fn get(&self, key: &str) -> Option<&str> {
48        let key = key.to_string().to_lowercase();
49        self.headers
50            .iter()
51            .find(|(k, _)| k.to_lowercase() == key)
52            .map(|(_, v)| v.as_str())
53    }
54
55    /// Get all the values that match the given key.
56    ///
57    /// The lookup is case-insensitive.
58    pub fn get_all(&self, key: &str) -> impl Iterator<Item = &str> {
59        let key = key.to_string().to_lowercase();
60        self.headers
61            .iter()
62            .filter(move |(k, _)| k.to_lowercase() == key)
63            .map(|(_, v)| v.as_str())
64    }
65
66    /// Sort the headers by key.
67    ///
68    /// This makes the headers easier to read when printed out.
69    ///
70    /// `ehttp` will sort the headers in the responses.
71    pub fn sort(&mut self) {
72        self.headers.sort_by(|a, b| a.0.cmp(&b.0));
73    }
74}
75
76impl IntoIterator for Headers {
77    type Item = (String, String);
78    type IntoIter = std::vec::IntoIter<Self::Item>;
79
80    fn into_iter(self) -> Self::IntoIter {
81        self.headers.into_iter()
82    }
83}
84
85impl<'h> IntoIterator for &'h Headers {
86    type Item = &'h (String, String);
87    type IntoIter = std::slice::Iter<'h, (String, String)>;
88
89    fn into_iter(self) -> Self::IntoIter {
90        self.headers.iter()
91    }
92}
93
94// ----------------------------------------------------------------------------
95
96/// Determine if cross-origin requests lead to valid responses.
97/// Based on <https://developer.mozilla.org/en-US/docs/Web/API/Request/mode>
98#[cfg(target_arch = "wasm32")]
99#[derive(Default, Clone, Copy, Debug)]
100pub enum Mode {
101    /// If a request is made to another origin with this mode set, the result is an error.
102    SameOrigin = 0,
103
104    /// The request will not include the Origin header in a request.
105    /// The server's response will be opaque, meaning that JavaScript code cannot access its contents
106    NoCors = 1,
107
108    /// Includes an Origin header in the request and expects the server to respond with an
109    /// "Access-Control-Allow-Origin" header that indicates whether the request is allowed.
110    #[default]
111    Cors = 2,
112
113    /// A mode for supporting navigation
114    Navigate = 3,
115}
116
117#[cfg(target_arch = "wasm32")]
118impl From<Mode> for web_sys::RequestMode {
119    fn from(mode: Mode) -> Self {
120        match mode {
121            Mode::SameOrigin => web_sys::RequestMode::SameOrigin,
122            Mode::NoCors => web_sys::RequestMode::NoCors,
123            Mode::Cors => web_sys::RequestMode::Cors,
124            Mode::Navigate => web_sys::RequestMode::Navigate,
125        }
126    }
127}
128
129/// A simple HTTP request.
130#[derive(Clone, Debug)]
131pub struct Request {
132    /// "GET", "POST", …
133    pub method: String,
134
135    /// https://…
136    pub url: String,
137
138    /// The data you send with e.g. "POST".
139    pub body: Vec<u8>,
140
141    /// ("Accept", "*/*"), …
142    pub headers: Headers,
143
144    /// Request mode used on fetch. Only available on wasm builds
145    #[cfg(target_arch = "wasm32")]
146    pub mode: Mode,
147}
148
149impl Request {
150    /// Create a `GET` request with the given url.
151    #[allow(clippy::needless_pass_by_value)]
152    pub fn get(url: impl ToString) -> Self {
153        Self {
154            method: "GET".to_owned(),
155            url: url.to_string(),
156            body: vec![],
157            headers: Headers::new(&[("Accept", "*/*")]),
158            #[cfg(target_arch = "wasm32")]
159            mode: Mode::default(),
160        }
161    }
162
163    /// Create a `HEAD` request with the given url.
164    #[allow(clippy::needless_pass_by_value)]
165    pub fn head(url: impl ToString) -> Self {
166        Self {
167            method: "HEAD".to_owned(),
168            url: url.to_string(),
169            body: vec![],
170            headers: Headers::new(&[("Accept", "*/*")]),
171            #[cfg(target_arch = "wasm32")]
172            mode: Mode::default(),
173        }
174    }
175
176    /// Create a `POST` request with the given url and body.
177    #[allow(clippy::needless_pass_by_value)]
178    pub fn post(url: impl ToString, body: Vec<u8>) -> Self {
179        Self {
180            method: "POST".to_owned(),
181            url: url.to_string(),
182            body,
183            headers: Headers::new(&[
184                ("Accept", "*/*"),
185                ("Content-Type", "text/plain; charset=utf-8"),
186            ]),
187            #[cfg(target_arch = "wasm32")]
188            mode: Mode::default(),
189        }
190    }
191
192    /// Multipart HTTP for both native and WASM.
193    ///
194    /// Requires the `multipart` feature to be enabled.
195    ///
196    /// Example:
197    /// ```
198    /// use std::io::Cursor;
199    /// use ehttp::multipart::MultipartBuilder;
200    /// let url = "https://www.example.com";
201    /// let request = ehttp::Request::multipart(
202    ///     url,
203    ///     MultipartBuilder::new()
204    ///         .add_text("label", "lorem ipsum")
205    ///         .add_stream(
206    ///             &mut Cursor::new(vec![0, 0, 0, 0]),
207    ///             "4_empty_bytes",
208    ///             Some("4_empty_bytes.png"),
209    ///             None,
210    ///         )
211    ///         .unwrap(),
212    /// );
213    /// ehttp::fetch(request, |result| {});
214    #[cfg(feature = "multipart")]
215    pub fn multipart(url: impl ToString, builder: MultipartBuilder) -> Self {
216        let (content_type, data) = builder.finish();
217        Self {
218            method: "POST".to_string(),
219            url: url.to_string(),
220            body: data,
221            headers: Headers::new(&[("Accept", "*/*"), ("Content-Type", content_type.as_str())]),
222            #[cfg(target_arch = "wasm32")]
223            mode: Mode::default(),
224        }
225    }
226
227    #[cfg(feature = "json")]
228    /// Create a `POST` request with the given url and json body.
229    #[allow(clippy::needless_pass_by_value)]
230    pub fn json<T>(url: impl ToString, body: &T) -> serde_json::error::Result<Self>
231    where
232        T: ?Sized + Serialize,
233    {
234        Ok(Self {
235            method: "POST".to_owned(),
236            url: url.to_string(),
237            body: serde_json::to_string(body)?.into_bytes(),
238            headers: Headers::new(&[("Accept", "*/*"), ("Content-Type", "application/json")]),
239            #[cfg(target_arch = "wasm32")]
240            mode: Mode::default(),
241        })
242    }
243}
244
245/// Response from a completed HTTP request.
246#[derive(Clone)]
247pub struct Response {
248    /// The URL we ended up at. This can differ from the request url when we have followed redirects.
249    pub url: String,
250
251    /// Did we get a 2xx response code?
252    pub ok: bool,
253
254    /// Status code (e.g. `404` for "File not found").
255    pub status: u16,
256
257    /// Status text (e.g. "File not found" for status code `404`).
258    pub status_text: String,
259
260    /// The returned headers.
261    pub headers: Headers,
262
263    /// The raw bytes of the response body.
264    pub bytes: Vec<u8>,
265}
266
267impl Response {
268    pub fn text(&self) -> Option<&str> {
269        std::str::from_utf8(&self.bytes).ok()
270    }
271
272    #[cfg(feature = "json")]
273    /// Convenience for getting json body
274    pub fn json<T: serde::de::DeserializeOwned>(&self) -> serde_json::Result<T> {
275        serde_json::from_slice(self.bytes.as_slice())
276    }
277
278    /// Convenience for getting the `content-type` header.
279    pub fn content_type(&self) -> Option<&str> {
280        self.headers.get("content-type")
281    }
282}
283
284impl std::fmt::Debug for Response {
285    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        let Self {
287            url,
288            ok,
289            status,
290            status_text,
291            headers,
292            bytes,
293        } = self;
294
295        fmt.debug_struct("Response")
296            .field("url", url)
297            .field("ok", ok)
298            .field("status", status)
299            .field("status_text", status_text)
300            .field("headers", headers)
301            .field("bytes", &format!("{} bytes", bytes.len()))
302            .finish_non_exhaustive()
303    }
304}
305
306/// An HTTP response status line and headers used for the [`streaming`](crate::streaming) API.
307#[derive(Clone, Debug)]
308pub struct PartialResponse {
309    /// The URL we ended up at. This can differ from the request url when we have followed redirects.
310    pub url: String,
311
312    /// Did we get a 2xx response code?
313    pub ok: bool,
314
315    /// Status code (e.g. `404` for "File not found").
316    pub status: u16,
317
318    /// Status text (e.g. "File not found" for status code `404`).
319    pub status_text: String,
320
321    /// The returned headers.
322    pub headers: Headers,
323}
324
325impl PartialResponse {
326    pub fn complete(self, bytes: Vec<u8>) -> Response {
327        let Self {
328            url,
329            ok,
330            status,
331            status_text,
332            headers,
333        } = self;
334        Response {
335            url,
336            ok,
337            status,
338            status_text,
339            headers,
340            bytes,
341        }
342    }
343}
344
345/// A description of an error.
346///
347/// This is only used when we fail to make a request.
348/// Any response results in `Ok`, including things like 404 (file not found).
349pub type Error = String;
350
351/// A type-alias for `Result<T, ehttp::Error>`.
352pub type Result<T> = std::result::Result<T, Error>;