fhttp_core/execution/
curl.rs

1use crate::parsers::Request;
2use crate::request::body::{Body, MultipartPart};
3
4pub trait Curl {
5    fn curl(&self) -> String;
6}
7
8impl Curl for Request {
9    fn curl(&self) -> String {
10        let mut parts = vec![format!("curl -X {}", self.method.as_str())];
11
12        for (name, value) in self.headers.iter() {
13            parts.push(format!(
14                "-H \"{}: {}\"",
15                name.as_str().replace('"', r#"\""#),
16                value.to_str().unwrap().replace('"', r#"\""#)
17            ))
18        }
19
20        match &self.body {
21            Body::Plain(body) => {
22                if !body.is_empty() {
23                    parts.push(format!("-d \"{}\"", escape_body(body)));
24                }
25            }
26            Body::Multipart(multiparts) => {
27                for prt in multiparts {
28                    parts.push(match prt {
29                        MultipartPart::File {
30                            name,
31                            file_path,
32                            mime_str,
33                        } => {
34                            let type_and_end = match mime_str {
35                                None => "\"".to_string(),
36                                Some(mime) => format!("; type={}\"", mime),
37                            };
38                            format!(
39                                "-F {name}=\"@{filepath}{type_and_end}",
40                                name = name,
41                                filepath = file_path.to_str(),
42                                type_and_end = type_and_end,
43                            )
44                        }
45                        MultipartPart::Text {
46                            name,
47                            text,
48                            mime_str,
49                        } => {
50                            let type_and_end = match mime_str {
51                                None => "\"".to_string(),
52                                Some(mime) => format!("; type={}\"", mime),
53                            };
54                            format!(
55                                "-F {name}=\"{text}{type_and_end}",
56                                name = name,
57                                text = text.replace('"', "\\\""),
58                                type_and_end = type_and_end,
59                            )
60                        }
61                    });
62                }
63            }
64        }
65
66        parts.push(format!("--url \"{}\"", &self.url.replace('"', r#"\""#)));
67
68        parts.join(" \\\n")
69    }
70}
71
72fn escape_body<S: Into<String>>(input: S) -> String {
73    input.into().replace('\n', "\\\n").replace('"', "\\\"")
74}
75
76#[cfg(test)]
77mod test {
78    use indoc::{formatdoc, indoc};
79    use serde_json::json;
80
81    use crate::request::body::MultipartPart;
82    use crate::test_utils::root;
83
84    use super::*;
85
86    #[test]
87    fn should_print_command_for_simple_request() {
88        let result = Request::basic("GET", "http://localhost/123").curl();
89
90        assert_eq!(
91            result,
92            indoc!(
93                r#"
94                curl -X GET \
95                --url "http://localhost/123""#
96            )
97        );
98    }
99
100    #[test]
101    fn should_print_command_with_headers() {
102        let result = Request::basic("GET", "http://localhost/123")
103            .add_header("accept", "application/json")
104            .add_header("content-type", "application/json")
105            .curl();
106
107        assert_eq!(
108            result,
109            indoc!(
110                r#"
111                curl -X GET \
112                -H "accept: application/json" \
113                -H "content-type: application/json" \
114                --url "http://localhost/123""#
115            )
116        );
117    }
118
119    #[test]
120    fn should_print_command_with_headers_and_body() {
121        let body = "{\n    \"foo\": \"bar\",\n    \"bar\": \"escape'me\"\n}";
122
123        let result = Request::basic("GET", "http://localhost/555")
124            .add_header("content-type", "application/json")
125            .body(body)
126            .curl();
127
128        assert_eq!(
129            result,
130            indoc!(
131                r#"
132                curl -X GET \
133                -H "content-type: application/json" \
134                -d "{\
135                    \"foo\": \"bar\",\
136                    \"bar\": \"escape'me\"\
137                }" \
138                --url "http://localhost/555""#
139            )
140        );
141    }
142
143    #[test]
144    fn should_print_command_with_plain_text_body() {
145        let result = Request::basic("GET", "http://localhost/555")
146            .add_header("content-type", "application/json")
147            .body("this is a so-called \"test\"")
148            .curl();
149
150        assert_eq!(
151            result,
152            indoc!(
153                r#"
154                curl -X GET \
155                -H "content-type: application/json" \
156                -d "this is a so-called \"test\"" \
157                --url "http://localhost/555""#
158            )
159        );
160    }
161
162    #[test]
163    fn should_print_command_with_body_with_newlines() {
164        let result = Request::basic("GET", "http://localhost/555")
165            .add_header("content-type", "application/json")
166            .body("one\ntwo\nthree")
167            .curl();
168
169        assert_eq!(
170            result,
171            indoc!(
172                r#"
173                curl -X GET \
174                -H "content-type: application/json" \
175                -d "one\
176                two\
177                three" \
178                --url "http://localhost/555""#
179            )
180        );
181    }
182
183    #[test]
184    fn should_print_command_with_gql_body() {
185        let result = Request::basic("GET", "http://localhost/555")
186            .add_header("content-type", "application/json")
187            .gql_body(json!({
188                "query": "query { search(filter: \"foobar\") { id } }",
189            }))
190            .curl();
191
192        assert_eq!(
193            result,
194            indoc!(
195                r#"
196                curl -X GET \
197                -H "content-type: application/json" \
198                -d "{\"query\":\"query { search(filter: \\"foobar\\") { id } }\"}" \
199                --url "http://localhost/555""#
200            )
201        );
202    }
203
204    #[test]
205    fn should_print_command_with_headers_and_files() {
206        let result = Request::basic("GET", "http://localhost/555")
207            .add_header("content-type", "application/json")
208            .multipart(&[
209                MultipartPart::File {
210                    name: "file1".to_string(),
211                    file_path: root().join("Cargo.toml"),
212                    mime_str: None,
213                },
214                MultipartPart::File {
215                    name: "file2".to_string(),
216                    file_path: root().join("Cargo.lock"),
217                    mime_str: None,
218                },
219            ])
220            .curl();
221
222        assert_eq!(
223            result,
224            formatdoc!(
225                r#"
226                curl -X GET \
227                -H "content-type: application/json" \
228                -F file1="@{base}/Cargo.toml" \
229                -F file2="@{base}/Cargo.lock" \
230                --url "http://localhost/555""#,
231                base = root().to_str().to_string(),
232            )
233        );
234    }
235
236    #[test]
237    fn should_print_command_with_multiparts() {
238        let filepath = root().join("resources/image.jpg");
239        let result = Request::basic("GET", "http://localhost/555")
240            .multipart(&[
241                MultipartPart::Text {
242                    name: "textpart1".to_string(),
243                    text: "this is a text".to_string(),
244                    mime_str: Some("plain/text".to_string()),
245                },
246                MultipartPart::Text {
247                    name: "textpart2".to_string(),
248                    text: "{\"a\": 5}".to_string(),
249                    mime_str: Some("application/json".to_string()),
250                },
251                MultipartPart::Text {
252                    name: "textpart3".to_string(),
253                    text: "this is a text".to_string(),
254                    mime_str: None,
255                },
256                MultipartPart::File {
257                    name: "filepart".to_string(),
258                    file_path: filepath.clone(),
259                    mime_str: None,
260                },
261            ])
262            .curl();
263
264        assert_eq!(
265            result,
266            formatdoc!(
267                r#"
268                curl -X GET \
269                -F textpart1="this is a text; type=plain/text" \
270                -F textpart2="{{\"a\": 5}}; type=application/json" \
271                -F textpart3="this is a text" \
272                -F filepart="@{filepath}" \
273                --url "http://localhost/555""#,
274                filepath = filepath.to_str(),
275            )
276        );
277    }
278}