1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2024 Orange
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *          http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18

            
19
use std::fmt;
20

            
21
mod args;
22
mod commands;
23
mod matches;
24

            
25
#[derive(Clone, Debug, PartialEq, Eq)]
26
pub struct HurlOption {
27
    name: String,
28
    value: String,
29
}
30

            
31
impl HurlOption {
32
9
    pub fn new(name: &str, value: &str) -> HurlOption {
33
9
        HurlOption {
34
9
            name: name.to_string(),
35
9
            value: value.to_string(),
36
        }
37
    }
38
}
39

            
40
impl fmt::Display for HurlOption {
41
9
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42
9
        write!(f, "{}: {}", self.name, self.value)
43
    }
44
}
45

            
46
3
pub fn parse(s: &str) -> Result<String, String> {
47
3
    let lines: Vec<&str> = regex::Regex::new(r"\n|\r\n")
48
3
        .unwrap()
49
3
        .split(s)
50
31
        .filter(|s| !s.is_empty())
51
3
        .collect();
52
3
    let mut s = String::new();
53
24
    for (i, line) in lines.iter().enumerate() {
54
24
        let hurl_str = parse_line(line).map_err(|message| {
55
            format!("Can not parse curl command at line {}: {message}", i + 1)
56
24
        })?;
57
24
        s.push_str(format!("{hurl_str}\n").as_str());
58
    }
59
3
    Ok(s)
60
}
61

            
62
24
fn parse_line(s: &str) -> Result<String, String> {
63
24
    let mut command = clap::Command::new("curl")
64
24
        .arg(commands::compressed())
65
24
        .arg(commands::data())
66
24
        .arg(commands::headers())
67
24
        .arg(commands::insecure())
68
24
        .arg(commands::location())
69
24
        .arg(commands::max_redirects())
70
24
        .arg(commands::method())
71
24
        .arg(commands::retry())
72
24
        .arg(commands::url())
73
24
        .arg(commands::url_param());
74

            
75
24
    let params = args::split(s)?;
76
24
    let arg_matches = match command.try_get_matches_from_mut(params) {
77
24
        Ok(r) => r,
78
        Err(e) => return Err(e.to_string()),
79
    };
80

            
81
24
    let method = matches::method(&arg_matches);
82
24
    let url = matches::url(&arg_matches);
83
24
    let headers = matches::headers(&arg_matches);
84
24
    let options = matches::options(&arg_matches);
85
24
    let body = matches::body(&arg_matches);
86
24
    let s = format(&method, &url, headers, &options, body);
87
24
    Ok(s)
88
}
89

            
90
24
fn format(
91
24
    method: &str,
92
24
    url: &str,
93
24
    headers: Vec<String>,
94
24
    options: &[HurlOption],
95
24
    body: Option<String>,
96
24
) -> String {
97
24
    let mut s = format!("{method} {url}");
98
45
    for header in headers {
99
21
        s.push_str(format!("\n{header}").as_str());
100
    }
101
24
    if !options.is_empty() {
102
9
        s.push_str("\n[Options]");
103
18
        for option in options {
104
9
            s.push_str(format!("\n{option}").as_str());
105
        }
106
    }
107
24
    if let Some(body) = body {
108
6
        s.push('\n');
109
6
        s.push_str(body.as_str());
110
    }
111
24
    let asserts = additional_asserts(options);
112
24
    if !asserts.is_empty() {
113
3
        s.push_str("\nHTTP *");
114
3
        s.push_str("\n[Asserts]");
115
6
        for assert in asserts {
116
3
            s.push_str(format!("\n{assert}").as_str());
117
        }
118
    }
119
24
    s.push('\n');
120
24
    s
121
}
122

            
123
24
fn has_option(options: &[HurlOption], name: &str) -> bool {
124
30
    for option in options {
125
9
        if option.name == name {
126
3
            return true;
127
        }
128
    }
129
21
    false
130
}
131

            
132
24
fn additional_asserts(options: &[HurlOption]) -> Vec<String> {
133
24
    let mut asserts = vec![];
134
24
    if has_option(options, "retry") {
135
3
        asserts.push("status < 500".to_string());
136
    }
137
24
    asserts
138
}
139

            
140
#[cfg(test)]
141
mod test {
142
    use crate::curl::*;
143

            
144
    #[test]
145
    fn test_parse() {
146
        let hurl_str = r#"GET http://localhost:8000/hello
147

            
148
GET http://localhost:8000/custom-headers
149
Fruit:Raspberry
150

            
151
"#;
152
        assert_eq!(
153
            parse(
154
                r#"curl http://localhost:8000/hello
155
curl http://localhost:8000/custom-headers -H 'Fruit:Raspberry'
156
"#
157
            )
158
            .unwrap(),
159
            hurl_str
160
        );
161
    }
162

            
163
    #[test]
164
    fn test_hello() {
165
        let hurl_str = r#"GET http://localhost:8000/hello
166
"#;
167
        assert_eq!(
168
            parse_line("curl http://localhost:8000/hello").unwrap(),
169
            hurl_str
170
        );
171
    }
172

            
173
    #[test]
174
    fn test_headers() {
175
        let hurl_str = r#"GET http://localhost:8000/custom-headers
176
Fruit:Raspberry
177
Fruit: Banana
178
Test: '
179
"#;
180
        assert_eq!(
181
            parse_line("curl http://localhost:8000/custom-headers -H 'Fruit:Raspberry' -H 'Fruit: Banana' -H $'Test: \\''").unwrap(),
182
            hurl_str
183
        );
184
        assert_eq!(
185
            parse_line("curl http://localhost:8000/custom-headers   --header Fruit:Raspberry -H 'Fruit: Banana' -H $'Test: \\''  ").unwrap(),
186
            hurl_str
187
        );
188
    }
189

            
190
    #[test]
191
    fn test_post_hello() {
192
        let hurl_str = r#"POST http://localhost:8000/hello
193
Content-Type: text/plain
194
```
195
hello
196
```
197
"#;
198
        assert_eq!(
199
            parse_line(r#"curl -d $'hello'  -H 'Content-Type: text/plain' -X POST http://localhost:8000/hello"#).unwrap(),
200
            hurl_str
201
        );
202
    }
203

            
204
    #[test]
205
    fn test_post_format_params() {
206
        let hurl_str = r#"POST http://localhost:3000/data
207
Content-Type: application/x-www-form-urlencoded
208
```
209
param1=value1&param2=value2
210
```
211
"#;
212
        assert_eq!(
213
            parse_line("curl http://localhost:3000/data -d 'param1=value1&param2=value2'").unwrap(),
214
            hurl_str
215
        );
216
        assert_eq!(
217
            parse_line("curl -X POST http://localhost:3000/data -H 'Content-Type: application/x-www-form-urlencoded' --data 'param1=value1&param2=value2'").unwrap(),
218
            hurl_str
219
        );
220
    }
221

            
222
    #[test]
223
    fn test_post_json() {
224
        let hurl_str = r#"POST http://localhost:3000/data
225
Content-Type: application/json
226
```
227
{"key1":"value1", "key2":"value2"}
228
```
229
"#;
230
        assert_eq!(
231
                hurl_str,
232
                parse_line(r#"curl -d '{"key1":"value1", "key2":"value2"}' -H 'Content-Type: application/json' -X POST http://localhost:3000/data"#).unwrap()
233
            );
234

            
235
        let hurl_str = r#"POST http://localhost:3000/data
236
Content-Type: application/json
237
```
238
{
239
  "key1": "value1",
240
  "key2": "value2"
241
}
242
```
243
"#;
244
        assert_eq!(
245
            parse_line(r#"curl -d $'{\n  "key1": "value1",\n  "key2": "value2"\n}' -H 'Content-Type: application/json' -X POST http://localhost:3000/data"#).unwrap(),
246
            hurl_str
247
        );
248
    }
249

            
250
    #[test]
251
    fn test_post_file() {
252
        let hurl_str = r#"POST http://example.com/
253
file, filename;
254
"#;
255
        assert_eq!(
256
            parse_line(r#"curl --data @filename http://example.com/"#).unwrap(),
257
            hurl_str
258
        );
259
    }
260

            
261
    #[test]
262
    fn test_redirect() {
263
        let hurl_str = r#"GET http://localhost:8000/redirect-absolute
264
[Options]
265
location: true
266
"#;
267
        assert_eq!(
268
            parse_line(r#"curl -L http://localhost:8000/redirect-absolute"#).unwrap(),
269
            hurl_str
270
        );
271
    }
272

            
273
    #[test]
274
    fn test_insecure() {
275
        let hurl_str = r#"GET https://localhost:8001/hello
276
[Options]
277
insecure: true
278
"#;
279
        assert_eq!(
280
            parse_line(r#"curl -k https://localhost:8001/hello"#).unwrap(),
281
            hurl_str
282
        );
283
    }
284

            
285
    #[test]
286
    fn test_max_redirects() {
287
        let hurl_str = r#"GET https://localhost:8001/hello
288
[Options]
289
max-redirs: 10
290
"#;
291
        assert_eq!(
292
            parse_line(r#"curl https://localhost:8001/hello --max-redirs 10"#).unwrap(),
293
            hurl_str
294
        );
295
    }
296
}