rest_cli/
lib.rs

1pub mod cli;
2
3use ansi_term::Colour::*;
4use colored_json::prelude::*;
5use colored_json::{Color, Styler};
6use reqwest::blocking::{Client, RequestBuilder};
7use std::fs;
8
9// options not supported by reqwest
10const METHODS: [&str; 6] = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"];
11
12#[derive(Clone)]
13struct Line {
14    text: String,
15    number: usize,
16}
17
18impl Line {
19    fn new(text: &str, number: usize) -> Line {
20        Line {
21            text: text.to_string(),
22            number,
23        }
24    }
25}
26
27struct Request {
28    lines: Vec<Line>,
29    method: String,
30}
31
32impl Request {
33    fn new(lines: Vec<Line>, method: &str) -> Request {
34        Request {
35            lines,
36            method: method.to_string(),
37        }
38    }
39
40    fn error(&self, line: usize, msg: &str) -> String {
41        format!("Error (line {}): {}", line, msg)
42    }
43
44    fn get_uri(&self) -> Result<String, String> {
45        let mut host = self.lines[0].text.to_string();
46        let last_line = &self.lines[self.lines.len() - 1];
47        let mut words = last_line.text.split(' ');
48        words.next();
49        let location = match words.next() {
50            Some(location) if !location.is_empty() => location,
51            _ => {
52                return Err(self.error(last_line.number, "Expected location"));
53            }
54        };
55        host.push_str(&location);
56        Ok(host)
57    }
58
59    fn parse(&self, client: &Client, color: bool) -> Result<RequestBuilder, String> {
60        let uri = match self.get_uri() {
61            Ok(uri) => uri,
62            Err(e) => return Err(e),
63        };
64
65        let mut in_body = false;
66        let mut body = "".to_string();
67
68        let mut headers: Vec<&Line> = vec![];
69
70        for line in self.lines[1..self.lines.len() - 1].iter() {
71            // Don't like this. Very hacky
72            if in_body || line.text.starts_with('{') || line.text.contains('}') {
73                body.push_str(&line.text);
74                in_body = !line.text.ends_with('}');
75            } else {
76                // Assume headers
77                headers.push(line);
78            }
79        }
80
81        let mut req: RequestBuilder = match &self.method[..] {
82            "GET" => client.get(&uri),
83            "POST" => client.post(&uri),
84            "PUT" => client.put(&uri),
85            "DELETE" => client.delete(&uri),
86            "HEAD" => client.head(&uri),
87            "PATCH" => client.patch(&uri),
88            //"OPTIONS" => client.options(uri), // Uh oh
89            method => {
90                return Err(
91                    self.error(self.lines[0].number, &format!("Invalid method: {}", method))
92                );
93            }
94        };
95
96        for header in headers {
97            let mut parts = header.text.split(':');
98            let name = match parts.next() {
99                Some(name) => name,
100                _ => {
101                    return Err(self.error(header.number, "Invalid header syntax"));
102                }
103            };
104            let value = match parts.next() {
105                Some(name) => name.trim(),
106                _ => {
107                    return Err(self.error(header.number, "Invalid header syntax"));
108                }
109            };
110
111            req = req.header(name, value);
112        }
113
114        req = match &body[..] {
115            "" => req,
116            _ => req.body(body),
117        };
118
119        if color {
120            println!("{} {}\n", self.method, Yellow.paint(uri));
121        } else {
122            println!("{} {}\n", self.method, uri);
123        };
124
125        Ok(req)
126    }
127}
128
129pub fn run(config: cli::Cli) {
130    #[cfg(windows)]
131    let enabled = ansi_term::enable_ansi_support();
132
133    let contents = fs::read_to_string(config.path).expect("Something went wrong reading the file");
134    let client = Client::new();
135
136    let mut n = 0;
137    let mut lines: Vec<Line> = vec![];
138
139    for line in contents.lines() {
140        n += 1;
141        if !line.is_empty() && !line.starts_with('#') {
142            lines.push(Line::new(line, n));
143        }
144    }
145
146    let mut start_line = 0;
147    n = 0;
148
149    for line in &lines {
150        n += 1;
151        for method in METHODS.iter() {
152            if line.text.starts_with(method) {
153                println!("\n---------------");
154                let req = Request::new(lines[start_line..n].to_vec(), method)
155                    .parse(&client, !config.no_color);
156                match req {
157                    Ok(req) => {
158                        send_req(req, config.verbose, !config.no_color).unwrap_or_else(|e| {
159                            println!("{}", e);
160                        });
161                    }
162                    Err(e) => {
163                        println!("{}", e);
164                    }
165                }
166
167                start_line = n;
168            }
169        }
170    }
171}
172
173fn send_req(req: RequestBuilder, verbose: bool, color: bool) -> Result<(), reqwest::Error> {
174    let res = match req.send() {
175        Ok(res) => res,
176        Err(e) => {
177            return Err(e);
178        }
179    };
180
181    let status = res.status();
182    let reason = match status.canonical_reason() {
183        Some(reason) => reason,
184        None => "",
185    };
186    let code = status.as_str();
187    if color {
188        let code = if status.is_success() {
189            Green.paint(code)
190        } else if status.is_redirection() {
191            Blue.paint(code)
192        } else if status.is_informational() {
193            Yellow.paint(code)
194        } else {
195            Red.paint(code)
196        };
197        println!("{} {}", code, reason);
198    } else {
199        println!("{} {}", code, reason);
200    }
201
202    if verbose {
203        for (key, value) in res.headers().iter() {
204            if color {
205                println!("{}: {:?}", Cyan.paint(key.as_str()), value);
206            } else {
207                println!("{}: {:?}", key.as_str(), value);
208            }
209        }
210        println!();
211    }
212
213    let default = &reqwest::header::HeaderValue::from_str("").unwrap();
214    let content_type = res.headers().get("content-type").unwrap_or(default);
215    // Regex?
216    if content_type == "application/json; charset=utf-8" {
217        let color_mode = match color {
218            true => ColorMode::On,
219            false => ColorMode::Off,
220        };
221        let res_body = res
222            .text()
223            .unwrap()
224            .to_colored_json_with_styler(
225                color_mode,
226                Styler {
227                    key: Color::Green.normal(),
228                    string_value: Color::Cyan.normal(),
229                    integer_value: Color::Yellow.normal(),
230                    float_value: Color::Yellow.normal(),
231                    object_brackets: Default::default(),
232                    array_brackets: Default::default(),
233                    bool_value: Color::Red.normal(),
234                    ..Default::default()
235                },
236            )
237            .unwrap();
238        println!("{}", res_body);
239    } else {
240        println!("{}", res.text().unwrap());
241    }
242
243    Ok(())
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    fn setup(text: &str, method: &str) -> Result<RequestBuilder, String> {
251        let mut n = 0;
252        let mut lines = vec![];
253        for line in text.split("\n") {
254            n += 1;
255            lines.push(Line::new(line, n));
256        }
257        let client = Client::new();
258        Request::new(lines, method).parse(&client)
259    }
260
261    #[test]
262    fn get() {
263        let req = setup("https://example.com\nGET /route", "GET")
264            .unwrap()
265            .build()
266            .unwrap();
267
268        assert_eq!(req.method().as_str(), "GET");
269        assert_eq!(
270            *req.url(),
271            reqwest::Url::parse("https://example.com/route").unwrap()
272        );
273        assert!(req.headers().is_empty());
274        match req.body() {
275            Some(_) => panic!("Body should be empty"),
276            None => {} // OK
277        }
278    }
279
280    #[test]
281    fn no_location_specified() {
282        let req = setup("http://localhost:8080\nPUT ", "PUT");
283        match req {
284            Ok(_) => {
285                panic!("Expected error");
286            }
287            Err(e) => {
288                assert_eq!(e, "Error (line 2): Expected location");
289            }
290        }
291    }
292
293    #[test]
294    fn post() {
295        let text = r#"http://localhost
296Content-Type: application/json
297{
298    "key": "value"
299}
300POST /"#;
301        let req = setup(text, "POST").unwrap().build().unwrap();
302
303        assert_eq!(req.method().as_str(), "POST");
304        assert_eq!(
305            *req.url(),
306            reqwest::Url::parse("http://localhost/").unwrap()
307        );
308        let headers = req.headers();
309        assert_eq!(headers.len(), 1);
310        assert_eq!(headers.get("content-type").unwrap(), &"application/json");
311        match req.body() {
312            Some(body) => {
313                let expected_body = r#"{    "key": "value"}"#;
314                assert_eq!(
315                    body.as_bytes().unwrap(),
316                    reqwest::blocking::Body::from(expected_body)
317                        .as_bytes()
318                        .unwrap()
319                );
320            }
321            None => panic!("Expected body"),
322        }
323    }
324
325    #[test]
326    fn invalid_header() {
327        let text = "https://www.example.com
328Content-Type: text/html
329other header
330DELETE /api/thing";
331        let req = setup(text, "DELETE");
332        match req {
333            Ok(_) => {
334                panic!("Expected error");
335            }
336            Err(e) => {
337                assert_eq!(e, "Error (line 3): Invalid header syntax");
338            }
339        }
340    }
341}