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
9const 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 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 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 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 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 => {} }
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}