fhttp_core/execution/
client.rs

1use std::time::Duration;
2
3use anyhow::{Context, Result};
4use reqwest::blocking::multipart;
5use reqwest::header::HeaderMap;
6use reqwest::{Method, Url};
7
8use crate::request::body::{Body, MultipartPart};
9use crate::{Response, ResponseHandler};
10
11pub struct Client;
12
13impl Client {
14    pub fn new() -> Self {
15        Client {}
16    }
17
18    pub fn exec(
19        &self,
20        method: Method,
21        url: &str,
22        headers: HeaderMap,
23        body: Body,
24        response_handler: Option<ResponseHandler>,
25        timeout: Option<Duration>,
26    ) -> Result<Response> {
27        let client = reqwest::blocking::Client::new();
28        let url = Url::parse(url).with_context(|| format!("Invalid URL: '{}'", url))?;
29        let mut req_builder = client.request(method, url).headers(headers);
30        if let Some(timeout) = timeout {
31            req_builder = req_builder.timeout(timeout);
32        }
33
34        let req_builder = match body {
35            Body::Plain(body) => req_builder.body(body),
36            Body::Multipart(parts) => {
37                let mut multipart = multipart::Form::new();
38                for part in parts {
39                    match part {
40                        MultipartPart::File {
41                            name,
42                            file_path,
43                            mime_str,
44                        } => {
45                            let path_clone = file_path.clone();
46                            let mut tmp =
47                                multipart::Part::file(file_path.clone()).with_context(|| {
48                                    format!("Error opening file {}", path_clone.to_str())
49                                })?;
50                            if let Some(mime_str) = mime_str {
51                                tmp = tmp.mime_str(&mime_str).with_context(|| {
52                                    format!("error parsing mime string '{}'", &mime_str)
53                                })?;
54                            }
55                            multipart = multipart.part(name, tmp);
56                        }
57                        MultipartPart::Text {
58                            name,
59                            text,
60                            mime_str,
61                        } => {
62                            let mut tmp = multipart::Part::text(text.clone());
63                            if let Some(mime_str) = mime_str {
64                                tmp = tmp.mime_str(&mime_str).with_context(|| {
65                                    format!("error parsing mime string '{}'", &mime_str)
66                                })?;
67                            }
68                            multipart = multipart.part(name, tmp);
69                        }
70                    }
71                }
72                req_builder.multipart(multipart)
73            }
74        };
75
76        let response = req_builder.send()?;
77        let status = response.status();
78        let text = response.text().unwrap();
79
80        let body = match status.is_success() {
81            true => match response_handler {
82                Some(handler) => handler.process_body(&text)?,
83                None => text,
84            },
85            false => text,
86        };
87
88        Ok(Response::new(status, body))
89    }
90}
91
92impl Default for Client {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use rstest::rstest;
101    use wiremock::matchers::method;
102    use wiremock::{Mock, MockServer, ResponseTemplate};
103    use wiremock_multipart::prelude::*;
104
105    use crate::request::body::MultipartPart;
106    use crate::test_utils::root;
107
108    use super::*;
109
110    #[rstest]
111    async fn should_correctly_handle_new_multiparts_async() -> Result<()> {
112        let mock_server = MockServer::start().await;
113        let image_path = root().join("resources/image.jpg");
114        let image_body = std::fs::read(&image_path).unwrap();
115
116        Mock::given(method("POST"))
117            .and(NumberOfParts(3))
118            .and(
119                ContainsPart::new()
120                    .with_name("text")
121                    .with_body("this is a text part".as_bytes())
122                    .with_content_type("text/plain"),
123            )
124            .and(
125                ContainsPart::new()
126                    .with_name("textfile")
127                    .with_filename("Cargo.toml")
128                    .with_content_type("plain/text"),
129            )
130            .and(
131                ContainsPart::new()
132                    .with_name("image")
133                    .with_content_type("image/jpeg")
134                    .with_body(image_body),
135            )
136            .respond_with(ResponseTemplate::new(200))
137            .expect(1)
138            .mount(&mock_server)
139            .await;
140
141        Client::new().exec(
142            Method::POST,
143            &mock_server.uri().to_string(),
144            HeaderMap::new(),
145            Body::Multipart(vec![
146                MultipartPart::Text {
147                    name: "text".to_string(),
148                    text: "this is a text part".to_string(),
149                    mime_str: Some("text/plain".to_string()),
150                },
151                MultipartPart::File {
152                    name: "textfile".to_string(),
153                    file_path: root().join("Cargo.toml"),
154                    mime_str: Some("plain/text".to_string()),
155                },
156                MultipartPart::File {
157                    name: "image".to_string(),
158                    file_path: image_path,
159                    mime_str: Some("image/jpeg".to_string()),
160                },
161            ]),
162            None,
163            None,
164        )?;
165
166        Ok(())
167    }
168}