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::core::http_client::{HttpClient, HttpMethod};
7use crate::HttpResponse;
8use clap::{Args, Subcommand};
9use colored::{ColoredString, Colorize};
10use indicatif::{ProgressBar, ProgressStyle};
11use infer;
12use reqwest::multipart::Part;
13use serde_json::Value;
14use std::fmt;
15use std::io::{self, Write};
16use std::time::Duration;
17
18#[derive(Args, Clone, Debug)]
19pub struct RequestData {
20    pub url: String,
21
22    #[clap(
23        short = 'H',
24        long = "header",
25        value_parser = RequestData::parse_header,
26        value_name = "KEY:VALUE",
27        num_args = 1..,
28        required = false
29    )]
30    pub headers: Vec<(String, String)>,
31
32    #[clap(short, long, default_value = "", required = false)]
33    pub body: String,
34}
35
36impl RequestData {
37    pub fn parse_header(s: &str) -> Result<(String, String), String> {
38        let parts: Vec<&str> = s.splitn(2, ':').collect();
39        if parts.len() != 2 {
40            return Err(format!("Invalid header format: '{}'. Use KEY:VALUE", s));
41        }
42        Ok((parts[0].trim().to_string(), parts[1].trim().to_string()))
43    }
44}
45
46#[derive(Subcommand, Clone, Debug)]
47pub enum RequestCommands {
48    Get {
49        #[clap(flatten)]
50        data: RequestData,
51    },
52    Post {
53        #[clap(flatten)]
54        data: RequestData,
55    },
56    Put {
57        #[clap(flatten)]
58        data: RequestData,
59    },
60    Delete {
61        #[clap(flatten)]
62        data: RequestData,
63    },
64    Patch {
65        #[clap(flatten)]
66        data: RequestData,
67    },
68}
69
70impl fmt::Display for RequestCommands {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            Self::Get { .. } => write!(f, "GET"),
74            Self::Post { .. } => write!(f, "POST"),
75            Self::Put { .. } => write!(f, "PUT"),
76            Self::Delete { .. } => write!(f, "DELETE"),
77            Self::Patch { .. } => write!(f, "PATCH"),
78        }
79    }
80}
81
82impl RequestCommands {
83    pub fn get_data(&self) -> &RequestData {
84        // assuming RequestData is the type of 'data'
85        match self {
86            Self::Get { data }
87            | Self::Post { data }
88            | Self::Put { data }
89            | Self::Delete { data }
90            | Self::Patch { data } => data,
91        }
92    }
93
94    pub fn print_request_method(&self, url: &str, status: u16, elapsed: u128) {
95        println!(
96            "\n[{}] {} - {} ({} ms)\n",
97            self.to_string().bold().bright_yellow(),
98            url.to_string().bold().bright_white(),
99            Self::colorize_status(status.to_string().parse().unwrap()),
100            elapsed
101        );
102    }
103
104    fn print_request_headers(headers: &[(String, String)]) {
105        println!("{}", "Request Headers:".to_string().bold().bright_blue());
106        for (key, value) in headers.iter() {
107            println!("  {}: {:?}", key.to_string().bright_white(), value);
108        }
109    }
110
111    fn print_request_body(body: &str) {
112        println!("{}", "Request Body:".to_string().bold().bright_blue());
113        println!("{}", body.italic());
114    }
115
116    async fn print_request_response(
117        response: &HttpResponse,
118        verbose: bool,
119        stream: bool,
120    ) -> Result<(), Box<dyn std::error::Error>> {
121        if verbose && !stream {
122            println!("{}", "Response Headers:".to_string().bold().bright_blue());
123            for (key, value) in response.headers.iter() {
124                println!("  {}: {:?}", key.to_string().bright_white(), value);
125            }
126            println!("\n{}", "Response Body:".to_string().bold().bright_blue());
127        }
128
129        if !stream {
130            //Try parsing the body as JSON
131            if let Ok(json) = response.json::<Value>() {
132                let pretty = serde_json::to_string_pretty(&json)?;
133                println!("{}", pretty.green());
134            } else {
135                println!("{}", response.body.italic());
136            }
137        }
138
139        Ok(())
140    }
141
142    pub fn colorize_status(status: u16) -> ColoredString {
143        match status {
144            200..=299 => status.to_string().bold().bright_green(),
145            300..=499 => status.to_string().bold().bright_yellow(),
146            500..=599 => status.to_string().bold().bright_red(),
147            _ => status.to_string().white(),
148        }
149    }
150
151    fn prompt_missing_header_data(mut headers: Vec<(String, String)>) -> Vec<(String, String)> {
152        for header in headers.iter_mut() {
153            if header.1.contains(":?") {
154                eprint!(
155                    "Header value for key '{}' is missing data. Please provide the correct value: ",
156                    header.0
157                );
158                io::stdout().flush().ok();
159                let mut new_value = String::new();
160                std::io::stdin()
161                    .read_line(&mut new_value)
162                    .expect("Failed to read header value");
163                header.1 = new_value.trim().to_string();
164            }
165        }
166        headers
167    }
168
169    fn prompt_missing_body_data(mut body: String) -> String {
170        while let Some(idx) = body.find(":?") {
171            eprint!(
172                "Missing data at position {} - {}. Please provide the correct value: ",
173                idx, body
174            );
175            io::stdout().flush().ok();
176            let mut replacement = String::new();
177            std::io::stdin()
178                .read_line(&mut replacement)
179                .expect("Failed to read body placeholder");
180            let replacement = replacement.trim();
181            body.replace_range(idx..idx + 2, replacement);
182        }
183        body
184    }
185
186    /// Checks if the Vec<u8> is valid UTF-8 (likely text) or not (binary).
187    fn is_text_data(data: &[u8]) -> bool {
188        std::str::from_utf8(data).is_ok()
189    }
190
191    pub async fn execute_request(
192        &self,
193        verbose: bool,
194        stdin_input: Vec<u8>,
195        stream: bool,
196    ) -> Result<(HttpResponse, u128), Box<dyn std::error::Error>> {
197        let data = self.get_data();
198
199        let current_url = if !stream {
200            Self::prompt_missing_body_data(data.url.clone())
201        } else {
202            data.url.clone()
203        };
204
205        let headers = if !stream {
206            Self::prompt_missing_header_data(data.headers.clone())
207        } else {
208            data.headers.clone()
209        };
210
211        let is_text = Self::is_text_data(&stdin_input);
212        let body = if stdin_input.is_empty() {
213            Self::prompt_missing_body_data(data.body.clone())
214        } else if is_text {
215            // Convert to string for text processing
216            let text = String::from_utf8_lossy(&stdin_input).to_string();
217            Self::prompt_missing_body_data(text)
218        } else {
219            // Binary: skip text prompts, use as-is (but reqwest body will handle bytes)
220            String::new() // Placeholder; we'll use bytes directly in the request
221        };
222
223        let part = if !stream && !stdin_input.is_empty() && !is_text {
224            // Binary data from stdin
225            let kind = infer::get(&stdin_input).ok_or_else(|| {
226                Box::new(std::io::Error::new(
227                    std::io::ErrorKind::InvalidData,
228                    "Unknown file type",
229                ))
230            })?;
231            let mime_type = kind.mime_type();
232            let extension = kind.extension();
233            let filename = format!("file.{}", extension);
234            Part::bytes(stdin_input.clone())
235                .file_name(filename)
236                .mime_str(mime_type)?
237        } else if !stream && !stdin_input.is_empty() && is_text {
238            // Text data from stdin
239            Part::text(String::from_utf8_lossy(&stdin_input).to_string())
240        } else {
241            // Use body string
242            Part::bytes(body.clone().into_bytes())
243        };
244
245        if verbose && !stream {
246            Self::print_request_headers(&headers);
247            Self::print_request_body(body.as_str());
248        }
249
250        let client = HttpClient::new()
251            .with_follow_redirects(false)
252            .with_timeout(Duration::from_secs(120));
253
254        let method = match self {
255            Self::Get { .. } => HttpMethod::Get,
256            Self::Post { .. } => HttpMethod::Post,
257            Self::Put { .. } => HttpMethod::Put,
258            Self::Delete { .. } => HttpMethod::Delete,
259            Self::Patch { .. } => HttpMethod::Patch,
260        };
261
262        let pb = ProgressBar::new_spinner();
263
264        pb.set_style(
265            ProgressStyle::with_template("{spinner:.green} {elapsed} {msg}")
266                .unwrap()
267                .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
268        );
269
270        pb.enable_steady_tick(Duration::from_millis(80));
271        pb.set_message("Executing Request...");
272
273        let start = std::time::Instant::now();
274
275        let resp = if stream {
276            let body_bytes = if !stdin_input.is_empty() {
277                stdin_input
278            } else {
279                body.clone().into_bytes()
280            };
281            client
282                .request(method, &current_url)
283                .headers(headers.into_iter().collect())
284                .body_bytes(body_bytes)
285                .send_streaming(|chunk| {
286                    std::io::stdout().write_all(chunk)?;
287                    std::io::stdout().flush().unwrap();
288                    Ok(())
289                })
290                .await
291        } else if is_text {
292            client
293                .request(method, &current_url)
294                .headers(headers.into_iter().collect())
295                .body(String::from_utf8_lossy(&stdin_input).as_ref())
296                .send()
297                .await
298        } else {
299            client
300                .request(method, &current_url)
301                .headers(headers.into_iter().collect())
302                .send_multipart(part)
303                .await
304        };
305
306        let elapsed = start.elapsed().as_millis();
307
308        match resp {
309            Ok(response) => {
310                pb.finish_with_message("Request completed");
311                Ok((response, elapsed))
312            }
313            Err(err) => {
314                pb.finish_with_message("Request failed");
315                Err(Box::new(err))
316            }
317        }
318    }
319
320    pub async fn run(
321        &self,
322        verbose: bool,
323        stdin_input: Vec<u8>,
324        stream: bool,
325    ) -> Result<(), Box<dyn std::error::Error>> {
326        let response = Self::execute_request(self, verbose, stdin_input, stream).await;
327
328        match response {
329            Ok((resp, elapsed)) => {
330                if verbose && !stream {
331                    println!("{:?}", resp.version);
332                    self.print_request_method(&resp.url, resp.status, elapsed);
333                }
334                Self::print_request_response(&resp, verbose, stream).await
335            }
336            Err(err) => Err(err),
337        }
338    }
339}