fhttp_core/execution/
client.rs1use 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}