Skip to main content

coman/cli/
request.rs

1//! CLI commands for making HTTP requests
2//!
3//! This module provides the command-line interface for making HTTP requests,
4//! including progress bars, colored output, and interactive prompts.
5
6use crate::cli::request_data::RequestData;
7use crate::HttpResponse;
8use clap::Subcommand;
9use colored::{ColoredString, Colorize};
10use serde_json::Value;
11use std::fmt;
12use std::io::{self, Write};
13
14#[derive(Subcommand, Clone, Debug)]
15pub enum RequestCommands {
16    Get {
17        #[clap(flatten)]
18        data: RequestData,
19    },
20    Post {
21        #[clap(flatten)]
22        data: RequestData,
23    },
24    Put {
25        #[clap(flatten)]
26        data: RequestData,
27    },
28    Delete {
29        #[clap(flatten)]
30        data: RequestData,
31    },
32    Patch {
33        #[clap(flatten)]
34        data: RequestData,
35    },
36}
37
38impl fmt::Display for RequestCommands {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            Self::Get { .. } => write!(f, "GET"),
42            Self::Post { .. } => write!(f, "POST"),
43            Self::Put { .. } => write!(f, "PUT"),
44            Self::Delete { .. } => write!(f, "DELETE"),
45            Self::Patch { .. } => write!(f, "PATCH"),
46        }
47    }
48}
49
50impl RequestCommands {
51    pub fn get_data(&self) -> &RequestData {
52        // assuming RequestData is the type of 'data'
53        match self {
54            Self::Get { data }
55            | Self::Post { data }
56            | Self::Put { data }
57            | Self::Delete { data }
58            | Self::Patch { data } => data,
59        }
60    }
61
62    pub fn print_request_method(&self, url: &str, status: u16, elapsed: u128) {
63        println!(
64            "\n[{}] {} - {} ({} ms)\n",
65            self.to_string().bold().bright_yellow(),
66            url.to_string().bold().bright_white(),
67            Self::colorize_status(status.to_string().parse().unwrap()),
68            elapsed
69        );
70    }
71
72    pub fn print_request_headers(headers: &[(String, String)]) {
73        println!("{}", "Request Headers:".to_string().bold().bright_blue());
74        for (key, value) in headers.iter() {
75            println!("  {}: {:?}", key.to_string().bright_white(), value);
76        }
77    }
78
79    pub fn print_request_body(body: &str) {
80        println!("{}", "Request Body:".to_string().bold().bright_blue());
81        println!("{}", body.italic());
82    }
83
84    pub fn print_lines_with_numbers(lines: &Vec<&str>, line_numbers: &[usize]) {
85        for (i, line) in lines.iter().enumerate() {
86            if line_numbers.contains(&(i + 1)) {
87                println!("{}: {}", (i + 1).to_string().bright_cyan(), line);
88            }
89        }
90    }
91
92    pub fn print_response_body(body: &str, output: &str) {
93        if output.starts_with("lines") {
94            let parts: Vec<&str> = output.split(',').collect();
95            let lines: Vec<&str> = body.lines().collect();
96            if parts.len() >= 2 {
97                // try to parse line numbers 2-3-6 or 3-6 range ..etc
98                let range_parts: Vec<&str> = parts[1].split('-').collect();
99                if range_parts.len() == 1 {
100                    if let Ok(line_num) = range_parts[0].parse::<usize>() {
101                        if line_num > 0 && line_num <= lines.len() {
102                            println!(
103                                "{}: {}",
104                                line_num.to_string().bright_cyan(),
105                                lines[line_num - 1]
106                            );
107                        } else {
108                            eprintln!(
109                                "Line number {} is out of range. The response body has {} lines.",
110                                line_num,
111                                lines.len()
112                            );
113                        }
114                    } else {
115                        eprintln!("Invalid line number specified. Expected format: 'lines,line' e.g. 'lines,34'");
116                    }
117                } else if range_parts.len() == 2 {
118                    if let (Ok(start), Ok(end)) = (
119                        range_parts[0].parse::<usize>(),
120                        range_parts[1].parse::<usize>(),
121                    ) {
122                        if start > 0 && end <= lines.len() && start <= end {
123                            for i in start..=end {
124                                println!("{}: {}", i.to_string().bright_cyan(), lines[i - 1]);
125                            }
126                        } else {
127                            eprintln!("Invalid line range specified. Ensure that start and end are within the range of the response body lines and that start is less than or equal to end.");
128                        }
129                    } else {
130                        eprintln!("Invalid line range specified. Expected format: 'lines,start-end' e.g. 'lines,34-35'");
131                    }
132                } else if range_parts.len() > 2 {
133                    // print specified lines e.g. 'lines,34-35-40' to print lines 34, 35 and 40
134                    let mut line_numbers = Vec::new();
135                    for part in range_parts {
136                        if let Ok(line_num) = part.parse::<usize>() {
137                            if line_num > 0 && line_num <= lines.len() {
138                                line_numbers.push(line_num);
139                            }
140                        } else {
141                            eprintln!("Invalid output format. Expected format: 'lines,start-end' or 'lines'");
142                        }
143                    }
144                    Self::print_lines_with_numbers(&lines, &line_numbers);
145                } else {
146                    eprintln!(
147                        "Invalid output format. Expected format: 'lines,start-end' or 'lines'"
148                    );
149                }
150            } else {
151                for (i, line) in lines.iter().enumerate() {
152                    println!("{}: {}", (i + 1).to_string().bright_cyan(), line);
153                }
154            }
155        } else if output.starts_with("json") {
156            // try to parse the body as JSON and look for a specific key e.g. 'json,data' to print the value of the 'data' key in the JSON response
157            let parts: Vec<&str> = output.split(',').collect();
158            if let Ok(json) = serde_json::from_str::<Value>(body) {
159                if parts.len() == 2 {
160                    let key = parts[1];
161                    if let Some(value) = json.get(key) {
162                        let pretty = serde_json::to_string_pretty(value)
163                            .unwrap_or_else(|_| value.to_string());
164                        println!("{}", pretty.green());
165                    } else {
166                        eprintln!("Key '{}' not found in JSON response.", key);
167                    }
168                } else {
169                    let pretty =
170                        serde_json::to_string_pretty(&json).unwrap_or_else(|_| body.to_string());
171                    println!("{}", pretty.green());
172                }
173            } else {
174                eprintln!("Failed to parse response body as JSON.");
175            }
176        } else {
177            println!("{}", body.italic());
178        }
179    }
180
181    pub fn print_request_response(
182        response: &HttpResponse,
183        verbose: bool,
184        stream: bool,
185        output: &Option<String>,
186    ) -> Result<(), Box<dyn std::error::Error>> {
187        if verbose && !stream {
188            println!("{}", "Response Headers:".to_string().bold().bright_blue());
189            for (key, value) in response.headers.iter() {
190                println!("  {}: {:?}", key.to_string().bright_white(), value);
191            }
192            println!("\n{}", "Response Body:".to_string().bold().bright_blue());
193        }
194
195        if !stream {
196            if let Some(output) = output {
197                Self::print_response_body(&response.body, output);
198            } else {
199                //Try parsing the body as JSON
200                if let Ok(json) = response.json::<Value>() {
201                    let pretty = serde_json::to_string_pretty(&json)?;
202                    println!("{}", pretty.green());
203                } else {
204                    println!("{}", response.body.italic());
205                }
206            }
207        }
208
209        Ok(())
210    }
211
212    pub fn colorize_status(status: u16) -> ColoredString {
213        match status {
214            200..=299 => status.to_string().bold().bright_green(),
215            300..=499 => status.to_string().bold().bright_yellow(),
216            500..=599 => status.to_string().bold().bright_red(),
217            _ => status.to_string().white(),
218        }
219    }
220
221    pub fn prompt_missing_header_data(mut headers: Vec<(String, String)>) -> Vec<(String, String)> {
222        for header in headers.iter_mut() {
223            if header.1.contains(":?") {
224                eprint!(
225                    "Header value for key '{}' is missing data. Please provide the correct value: ",
226                    header.0
227                );
228                io::stdout().flush().ok();
229                let mut new_value = String::new();
230                std::io::stdin()
231                    .read_line(&mut new_value)
232                    .expect("Failed to read header value");
233                header.1 = new_value.trim().to_string();
234            }
235        }
236        headers
237    }
238
239    pub fn prompt_missing_body_data(mut body: String) -> String {
240        while let Some(idx) = body.find(":?") {
241            eprint!(
242                "Missing data at position {} - {}. Please provide the correct value: ",
243                idx, body
244            );
245            io::stdout().flush().ok();
246            let mut replacement = String::new();
247            std::io::stdin()
248                .read_line(&mut replacement)
249                .expect("Failed to read body placeholder");
250            let replacement = replacement.trim();
251            body.replace_range(idx..idx + 2, replacement);
252        }
253        body
254    }
255
256    /// Checks if the Vec<u8> is valid UTF-8 (likely text) or not (binary).
257    pub fn is_text_data(data: &[u8]) -> bool {
258        std::str::from_utf8(data).is_ok()
259    }
260}