rust_fetch/
lib.rs

1mod error;
2mod network_error;
3mod fetch_config;
4mod fetch_options;
5mod fetch_response;
6mod utils;
7
8use anyhow::anyhow;
9use bytes::Bytes;
10pub use error::{DeserializationError, FetchError, FetchResult, SerializationError};
11pub use network_error::NetworkError;
12pub use fetch_config::FetchConfig;
13pub use fetch_options::{ContentType, FetchOptions};
14pub use fetch_response::FetchResponse;
15pub use reqwest;
16pub use reqwest::StatusCode;
17use reqwest::{header::HeaderMap, Client, ClientBuilder, RequestBuilder, Response, Url};
18use serde::{Deserialize, Serialize};
19use std::str::FromStr;
20use std::{collections::HashMap, time::Duration};
21use utils::{map_to_reqwest_headers, reqwest_headers_to_map};
22
23pub type FetchHeaders = HashMap<String, String>;
24pub const USER_AGENT: &'static str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
25
26#[derive(Debug)]
27pub struct Fetch {
28    client: Client,
29    pub config: Option<FetchConfig>,
30    base_url: String,
31}
32
33impl Default for Fetch {
34    fn default() -> Self {
35        let mut headers: FetchHeaders = HashMap::new();
36        Self::insert_default_headers(&mut headers, Default::default());
37
38        Self {
39            client: ClientBuilder::default()
40                .default_headers(map_to_reqwest_headers(&headers).unwrap())
41                .build()
42                .unwrap(),
43            config: Some(FetchConfig {
44                headers: Some(headers),
45                ..Default::default()
46            }),
47            base_url: Default::default(),
48        }
49    }
50}
51
52impl Fetch {
53    /// Creates a new instance of Fetch with a set base url and optional Options
54    ///
55    /// # Example
56    /// ```rust
57    /// use rust_fetch::Fetch;
58    /// let client = Fetch::new("http://localhost", None);
59    /// assert_ne!(true, client.is_err());
60    ///
61    /// ```
62    pub fn new(base_url: &str, options: Option<FetchConfig>) -> FetchResult<Self> {
63        let mut options = options.unwrap_or_default();
64        let mut headers = options
65            .headers
66            .as_ref()
67            .map(|r| r.clone())
68            .unwrap_or_default();
69
70        Self::insert_default_headers(&mut headers, Some(&options));
71        options.headers = Some(headers);
72
73        let default_headers: HeaderMap;
74        let mut client = ClientBuilder::default();
75        if let Some(headers) = &options.headers {
76            default_headers = map_to_reqwest_headers(&headers)?;
77            client = client.default_headers(default_headers);
78        }
79        if let Some(timeout) = &options.timeout_ms {
80            client = client.timeout(Duration::from_millis(timeout.to_owned()))
81        }
82
83        Ok(Self {
84            base_url: base_url.to_string(),
85            config: Some(options),
86            client: client
87                .build()
88                .map_err(|e| FetchError::Unknown(anyhow!(e)))?,
89            ..Default::default()
90        })
91    }
92
93    fn insert_default_headers(headers: &mut FetchHeaders, config: Option<&FetchConfig>) {
94        headers.insert("user-agent".to_string(), USER_AGENT.to_string());
95        if let Some(config) = config {
96            headers.insert(
97                reqwest::header::CONTENT_TYPE.to_string(),
98                config.content_type.clone().to_string(),
99            );
100            headers.insert(
101                reqwest::header::ACCEPT.to_string(),
102                config.accept.clone().to_string(),
103            );
104        }
105    }
106
107    /// Sets the default headers for this instance of Fetch.
108    ///
109    /// # Example
110    /// ```rust
111    /// use rust_fetch::{Fetch, map_string};
112    ///
113    /// let mut client = Fetch::new("http://localhost", None).unwrap();
114    /// let set_header_result = client.set_default_headers(Some(map_string!{ header1 : "header 1 value" }));
115    /// assert_ne!(true, set_header_result.is_err());
116    ///
117    /// ```
118    pub fn set_default_headers(&mut self, headers: Option<FetchHeaders>) -> FetchResult<()> {
119        let mut headers = headers.unwrap_or_default();
120
121        Self::insert_default_headers(&mut headers, self.config.as_ref());
122
123        let opts: FetchConfig = FetchConfig {
124            headers: Some(headers),
125            ..self.config.clone().unwrap_or(Default::default())
126        };
127
128        let new_fetch = Self::new(&self.base_url, Some(opts))?;
129        self.client = new_fetch.client;
130        self.config = new_fetch.config;
131
132        Ok(())
133    }
134
135    pub fn build_url(&self, endpoint: &str, options: Option<&FetchOptions>) -> FetchResult<Url> {
136        let mut built_string = String::new();
137        built_string += &self.base_url;
138
139        if built_string.chars().nth(built_string.chars().count() - 1) != Some('/')
140            && endpoint.chars().nth(0) != Some('/')
141        {
142            built_string += "/";
143        }
144
145        built_string += endpoint;
146        if let Some(options) = options {
147            if let Some(params) = &options.params {
148                let mut added_param = false;
149                for (index, (key, value)) in params.iter().enumerate() {
150                    if !added_param {
151                        built_string += "?";
152                        added_param = true;
153                    }
154                    built_string += &format!("{key}={value}");
155                    if index < params.len() - 1 {
156                        built_string += "&";
157                    }
158                }
159            }
160        }
161
162        let url: Url = built_string
163            .parse()
164            .map_err(|_| FetchError::InvalidUrl(built_string))?;
165
166        Ok(url)
167    }
168
169    fn make_body<U>(
170        &self,
171        data: U,
172        options: Option<&FetchOptions>,
173    ) -> FetchResult<(Vec<u8>, ContentType)>
174    where
175        U: Serialize,
176    {
177        let mut content_type: ContentType = Default::default();
178
179        if let Some(opts) = options {
180            if let Some(ref c_type) = opts.content_type {
181                content_type = c_type.clone();
182            } else {
183                if let Some(ref config) = self.config {
184                    content_type = config.content_type.clone();
185                }
186            }
187        } else {
188            if let Some(ref config) = self.config {
189                content_type = config.content_type.clone();
190            }
191        }
192
193        let data_to_return = match content_type {
194            ContentType::Json => serde_json::to_vec(&data)
195                .map_err(|e| FetchError::SerializationError(SerializationError::Json(e)))?,
196            ContentType::TextXml | ContentType::ApplicationXml => serde_xml_rs::to_string(&data)
197                .map_err(|e| FetchError::SerializationError(SerializationError::Xml(e)))?
198                .into_bytes(),
199            ContentType::UrlEncoded => serde_urlencoded::to_string(&data)
200                .map_err(|e| FetchError::SerializationError(SerializationError::UrlEncoded(e)))?
201                .into_bytes(),
202        };
203
204        return Ok((data_to_return, content_type));
205    }
206
207    fn build_request<U>(
208        &self,
209        data: Option<U>,
210        options: Option<&FetchOptions>,
211        original_builder: RequestBuilder,
212    ) -> FetchResult<RequestBuilder>
213    where
214        U: Serialize,
215    {
216        let mut builder = original_builder;
217        if let Some(options) = options {
218            if let Some(headers) = &options.headers {
219                builder = builder.headers(map_to_reqwest_headers(headers)?);
220            }
221        };
222        if let Some(body) = data {
223            let (body, content_type) = self.make_body(body, options)?;
224            builder = builder.body(body);
225            builder = builder.header(reqwest::header::CONTENT_TYPE, format!("{content_type}"));
226        }
227        if let Some(opts) = options {
228            if let Some(ref accept) = opts.accept {
229                builder = builder.header(reqwest::header::ACCEPT.to_string(), accept.to_string());
230            }
231        }
232
233        return Ok(builder);
234    }
235
236    fn deserialize_response<T>(
237        &self,
238        raw_body: &Bytes,
239        content_type: ContentType,
240    ) -> FetchResult<T>
241    where
242        T: for<'de> Deserialize<'de>,
243    {
244        return match content_type {
245            ContentType::Json => Ok(serde_json::from_slice::<T>(raw_body)
246                .map_err(|e| FetchError::DeserializationError(DeserializationError::Json(e)))?),
247            ContentType::TextXml | ContentType::ApplicationXml => {
248                let body_string = String::from_utf8(raw_body.to_vec()).map_err(|_| {
249                    FetchError::DeserializationError(DeserializationError::Unknown(String::from(
250                        "Response body does not contain valid Utf8",
251                    )))
252                })?;
253                Ok(serde_xml_rs::from_str::<T>(&body_string)
254                    .map_err(|e| FetchError::DeserializationError(DeserializationError::Xml(e)))?)
255            }
256            ContentType::UrlEncoded => {
257                Ok(serde_urlencoded::from_bytes::<T>(raw_body).map_err(|e| {
258                    FetchError::DeserializationError(DeserializationError::UrlEncoded(e))
259                })?)
260            }
261        };
262    }
263
264    async fn check_response_and_return_err(&self, response: Response) -> FetchResult<Response> {
265        if response.status().is_client_error() || response.status().is_server_error() {
266            return Err(FetchError::NetworkError(NetworkError::new(response).await));
267        }
268        Ok(response)
269    }
270
271    async fn response_to_fetch_response<T>(
272        &self,
273        response: Response,
274        deserialize_body: bool,
275    ) -> FetchResult<FetchResponse<T>>
276    where
277        T: for<'de> Deserialize<'de>,
278    {
279        let response = self.check_response_and_return_err(response).await?;
280        let remote_content_type = response
281            .headers()
282            .get(reqwest::header::CONTENT_TYPE)
283            .map(|c_type| {
284                c_type
285                    .to_str()
286                    .ok()
287                    .map(|s| s.to_owned())
288                    .unwrap_or_default()
289            })
290            .map(|string| ContentType::from_str(&string).ok().unwrap_or_default());
291
292        let headers = response.headers().clone();
293        let remote_address = response.remote_addr();
294        let status = response.status();
295
296        let raw_body = response.bytes().await.ok();
297        let mut body: Option<T> = None;
298
299        if let Some(raw_body) = &raw_body {
300            if deserialize_body {
301                if let Some(response_content_type) = remote_content_type {
302                    body = Some(self.deserialize_response::<T>(raw_body, response_content_type)?);
303                } else {
304                    body = Some(self.deserialize_response::<T>(raw_body, ContentType::Json)?);
305                }
306            }
307        }
308
309        return Ok(FetchResponse {
310            body,
311            raw_body,
312            status,
313            response_headers: reqwest_headers_to_map(&headers)?,
314            remote_address,
315        });
316    }
317
318    /// Sends an HTTP Post request to the configured remote server
319    ///
320    /// * `endpoint` - The remote endpoint. This gets joined with the base_url configured in the ::new() method
321    /// * `data` - Optional data to send to the remote endpoint (to be serialized as JSON). If `None`, then no data is sent instead of `null`
322    /// * `options` - The `FetchOptions` for this call. Allows setting of headers and/or query params
323    ///
324    /// # Example
325    /// ```rust
326    /// use httpmock::prelude::*;
327    /// use rust_fetch::Fetch;
328    ///
329    /// #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
330    /// struct ToReturn {}
331    ///
332    /// #[derive(serde::Serialize, serde::Deserialize, Debug)]
333    /// struct ToSend {
334    ///     test_key: String,
335    /// }
336    ///
337    /// #[tokio::main]
338    /// async fn main() {
339    ///     let server = MockServer::start();
340    ///
341    ///     server.mock(|when, then| {
342    ///         when.path("/test").method(POST);
343    ///         then.status(200).json_body(serde_json::json!({}));
344    ///     });
345    ///
346    ///     let fetch = Fetch::new(&server.base_url(), None).unwrap();
347    ///
348    ///      let response = fetch
349    ///         .post::<ToReturn, ToSend>(
350    ///             "/test",
351    ///             Some(ToSend {
352    ///                 test_key: "Testing".to_string(),
353    ///             }),
354    ///             Some(rust_fetch::FetchOptions {
355    ///                 params: Some(rust_fetch::map_string! {param1 : "value1"}),
356    ///                 ..Default::default()
357    ///             }),
358    ///         )
359    ///         .await.unwrap();
360    ///     assert_eq!(&200, &response.status);
361    ///     assert_eq!(ToReturn {}, response.body.unwrap());
362    /// }
363    /// ```
364    pub async fn post<T, U>(
365        &self,
366        endpoint: &str,
367        data: Option<U>,
368        options: Option<FetchOptions>,
369    ) -> FetchResult<FetchResponse<T>>
370    where
371        T: for<'de> Deserialize<'de>,
372        U: Serialize,
373    {
374        let options = options.unwrap_or_default();
375        let response = self
376            .build_request(
377                data,
378                Some(&options),
379                self.client.post(self.build_url(endpoint, Some(&options))?),
380            )?
381            .send()
382            .await
383            .map_err(|e| FetchError::UnableToSendRequest { err: e })?;
384
385        return Ok(self
386            .response_to_fetch_response(response, options.deserialize_body)
387            .await?);
388    }
389
390    /// Sends an HTTP GET request to the configured remote server
391    ///
392    /// * `endpoint` - The remote endpoint. This gets joined with the base_url configured in the ::new() method
393    /// * `options` - The `FetchOptions` for this call. Allows setting of headers and/or query params
394    ///
395    /// # Example
396    ///
397    /// ```rust
398    ///     use rust_fetch::Fetch;
399    ///     use httpmock::prelude::*;
400    ///
401    ///     #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
402    ///     struct ToReturn {
403    ///         
404    ///     }
405    ///
406    ///     #[tokio::main]
407    ///     async fn main() {
408    ///         let server = MockServer::start();
409    ///         
410    ///         server.mock(|when, then|{
411    ///             when.path("/test");
412    ///             then.status(200).json_body(serde_json::json!({}));
413    ///         });
414    ///
415    ///         let fetch = Fetch::new(&server.base_url(), None).unwrap();
416    ///
417    ///         let response = fetch.get::<ToReturn>("/test", Some(rust_fetch::FetchOptions
418    ///         {
419    ///             params: Some(rust_fetch::map_string!{param1 : "value1"}),
420    ///             ..Default::default()
421    ///         })).await.unwrap();
422    ///         assert_eq!(&200, &response.status);
423    ///         assert_eq!(ToReturn{}, response.body.unwrap());
424    ///     }
425    ///     
426    /// ```
427    pub async fn get<T>(
428        &self,
429        endpoint: &str,
430        options: Option<FetchOptions>,
431    ) -> FetchResult<FetchResponse<T>>
432    where
433        T: for<'de> Deserialize<'de>,
434    {
435        let options = options.unwrap_or_default();
436        let response = self
437            .build_request::<()>(
438                None,
439                Some(&options),
440                self.client.get(self.build_url(endpoint, Some(&options))?),
441            )?
442            .send()
443            .await
444            .map_err(|e| FetchError::UnableToSendRequest { err: e })?;
445
446        return Ok(self
447            .response_to_fetch_response(response, options.deserialize_body)
448            .await?);
449    }
450
451    /// Sends an HTTP DELETE request to the configured remote server
452    ///
453    /// * `endpoint` - The remote endpoint. This gets joined with the base_url configured in the ::new() method
454    /// * `data` - The optional data to send the the remote endpoint
455    /// * `options` - The `FetchOptions` for this call. Allows setting of headers and/or query params
456    ///
457    /// # Example
458    /// ```rust
459    ///     use rust_fetch::Fetch;
460    ///     use httpmock::prelude::*;
461    ///
462    ///     #[derive(serde::Deserialize, Debug, PartialEq)]
463    ///     struct ToReturn {}
464    ///
465    ///     #[tokio::main]
466    ///     async fn main() {
467    ///         let server = MockServer::start();
468    ///
469    ///         server.mock(| when, then | {
470    ///             when.path("/test").method(DELETE);
471    ///             then.status(200).json_body(serde_json::json!({}));
472    ///         });
473    ///
474    ///         let client = Fetch::new(&server.base_url(), None).unwrap();
475    ///
476    ///         let res = client.delete::<(), ToReturn>("/test", None, None).await.unwrap();
477    ///         assert_eq!(&200, &res.status);
478    ///         assert_eq!(ToReturn {}, res.body.unwrap());
479    ///     }
480    /// ```
481    pub async fn delete<T, U>(
482        &self,
483        endpoint: &str,
484        data: Option<T>,
485        options: Option<FetchOptions>,
486    ) -> FetchResult<FetchResponse<U>>
487    where
488        T: Serialize,
489        U: for<'de> Deserialize<'de>,
490    {
491        let options = options.unwrap_or_default();
492        let response = self
493            .build_request(
494                data,
495                Some(&options),
496                self.client
497                    .delete(self.build_url(endpoint, Some(&options))?),
498            )?
499            .send()
500            .await
501            .map_err(|e| FetchError::UnableToSendRequest { err: e })?;
502
503        return Ok(self
504            .response_to_fetch_response(response, options.deserialize_body)
505            .await?);
506    }
507
508    /// Sends an HTTP PUT request to the configured remote server
509    ///
510    /// * `endpoint` - The remote endpoint. This gets joined with the base_url configured in the ::new() method
511    /// * `data` - The optional data to send the the remote endpoint
512    /// * `options` - The `FetchOptions` for this call. Allows setting of headers and/or query params
513    ///
514    /// # Example
515    /// ```rust
516    ///     use rust_fetch::Fetch;
517    ///     use httpmock::prelude::*;
518    ///
519    ///     #[derive(serde::Deserialize, Debug, PartialEq)]
520    ///     struct ToReturn {}
521    ///
522    ///     #[tokio::main]
523    ///     async fn main() {
524    ///         let server = MockServer::start();
525    ///
526    ///         server.mock(| when, then | {
527    ///             when.path("/test").method(PUT);
528    ///             then.status(200).json_body(serde_json::json!({}));
529    ///         });
530    ///
531    ///         let client = Fetch::new(&server.base_url(), None).unwrap();
532    ///
533    ///         let res = client.put::<(), ToReturn>("/test", None, None).await.unwrap();
534    ///         assert_eq!(&200, &res.status);
535    ///         assert_eq!(ToReturn {}, res.body.unwrap());
536    ///     }
537    /// ```
538    pub async fn put<T, U>(
539        &self,
540        endpoint: &str,
541        data: Option<T>,
542        options: Option<FetchOptions>,
543    ) -> FetchResult<FetchResponse<U>>
544    where
545        T: Serialize,
546        U: for<'de> Deserialize<'de>,
547    {
548        let options = options.unwrap_or_default();
549        let response = self
550            .build_request(
551                data,
552                Some(&options),
553                self.client.put(self.build_url(endpoint, Some(&options))?),
554            )?
555            .send()
556            .await
557            .map_err(|e| FetchError::UnableToSendRequest { err: e })?;
558
559        return Ok(self
560            .response_to_fetch_response(response, options.deserialize_body)
561            .await?);
562    }
563
564    /// Sends an HTTP PATCH request to the configured remote server
565    ///
566    /// * `endpoint` - The remote endpoint. This gets joined with the base_url configured in the ::new() method
567    /// * `data` - The optional data to send the the remote endpoint
568    /// * `options` - The `FetchOptions` for this call. Allows setting of headers and/or query params
569    ///
570    /// # Example
571    /// ```rust
572    ///     use rust_fetch::Fetch;
573    ///     use httpmock::prelude::*;
574    ///
575    ///     #[derive(serde::Deserialize, Debug, PartialEq)]
576    ///     struct ToReturn {}
577    ///
578    ///     #[tokio::main]
579    ///     async fn main() {
580    ///         let server = MockServer::start();
581    ///
582    ///         server.mock(| when, then | {
583    ///             when.path("/test").method(httpmock::Method::PATCH);
584    ///             then.status(200).json_body(serde_json::json!({}));
585    ///         });
586    ///
587    ///         let client = Fetch::new(&server.base_url(), None).unwrap();
588    ///
589    ///         let res = client.patch::<(), ToReturn>("/test", None, None).await.unwrap();
590    ///         assert_eq!(&200, &res.status);
591    ///         assert_eq!(ToReturn {}, res.body.unwrap());
592    ///     }
593    /// ```
594    pub async fn patch<T, U>(
595        &self,
596        endpoint: &str,
597        data: Option<T>,
598        options: Option<FetchOptions>,
599    ) -> FetchResult<FetchResponse<U>>
600    where
601        T: Serialize,
602        U: for<'de> Deserialize<'de>,
603    {
604        let options = options.unwrap_or_default();
605        let response = self
606            .build_request(
607                data,
608                Some(&options),
609                self.client.patch(self.build_url(endpoint, Some(&options))?),
610            )?
611            .send()
612            .await
613            .map_err(|e| FetchError::UnableToSendRequest { err: e })?;
614
615        return Ok(self
616            .response_to_fetch_response(response, options.deserialize_body)
617            .await?);
618    }
619}