quest_cli/
cli.rs

1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4use clap::{Args, Parser, Subcommand};
5use duration_string::DurationString;
6use secrecy::SecretString;
7use serde::Deserialize;
8use url::Url;
9
10use crate::{
11    builder::{QuestClientBuilder, QuestRequestBuilder},
12    quest::{QuestCommand, QuestFile, QuestUrl},
13    types::{FormField, StringOrFile},
14};
15
16#[derive(Clone, Debug, Parser)]
17#[command(name = "quest")]
18#[command(version, about = "Cli for all the http fetch (re)quests you may go on.", long_about = None)]
19pub struct QuestCli {
20    #[arg(
21        short,
22        long,
23        global = true,
24        default_value = ".env",
25        help = "Load environment variables from file"
26    )]
27    env: PathBuf,
28    #[clap(flatten)]
29    pub options: RequestOptions,
30
31    #[command(subcommand)]
32    command: Command,
33}
34
35impl QuestCli {
36    pub fn init_logging(self) -> Self {
37        env_logger::init();
38        self
39    }
40    fn list_quests(quest_file: QuestFile) -> Result<()> {
41        use colored::Colorize;
42        use std::io::Write;
43
44        // Collect quest data for formatting
45        let mut quest_data: Vec<(String, String, String)> = Vec::new();
46
47        for (name, command) in quest_file.iter() {
48            let (method, url) = match command {
49                QuestCommand::Get { url_spec, .. } => ("GET", url_spec.to_url()?),
50                QuestCommand::Post { url_spec, .. } => ("POST", url_spec.to_url()?),
51                QuestCommand::Put { url_spec, .. } => ("PUT", url_spec.to_url()?),
52                QuestCommand::Delete { url_spec, .. } => ("DELETE", url_spec.to_url()?),
53                QuestCommand::Patch { url_spec, .. } => ("PATCH", url_spec.to_url()?),
54            };
55
56            quest_data.push((name.clone(), method.to_string(), url.to_string()));
57        }
58
59        let stdout = std::io::stdout();
60        let mut handle = stdout.lock();
61
62        if quest_data.is_empty() {
63            writeln!(handle, "No quests found in the quest file.")?;
64            return Ok(());
65        }
66
67        // Calculate column widths
68        let max_name_width = quest_data
69            .iter()
70            .map(|(name, _, _)| name.len())
71            .max()
72            .unwrap_or(4)
73            .max(4); // "NAME" header is 4 chars
74
75        let max_method_width = 6; // "DELETE" is longest method
76
77        // Print header
78        writeln!(
79            handle,
80            "{:<name_width$}  {:<method_width$}  {}",
81            "NAME".bold(),
82            "METHOD".bold(),
83            "URL".bold(),
84            name_width = max_name_width,
85            method_width = max_method_width
86        )?;
87
88        // Print separator
89        writeln!(
90            handle,
91            "{}  {}  {}",
92            "─".repeat(max_name_width),
93            "─".repeat(max_method_width),
94            "─".repeat(40)
95        )?;
96
97        // Print each quest
98        for (name, method, url) in quest_data {
99            let colored_method = match method.as_str() {
100                "GET" => method.green().bold(),
101                "POST" => method.blue().bold(),
102                "PUT" => method.yellow().bold(),
103                "DELETE" => method.red().bold(),
104                "PATCH" => method.magenta().bold(),
105                _ => method.white().bold(),
106            };
107
108            writeln!(
109                handle,
110                "{:<name_width$}  {:<method_width$}  {}",
111                name.cyan(),
112                colored_method,
113                url.bright_black(),
114                name_width = max_name_width,
115                method_width = max_method_width
116            )?;
117        }
118
119        handle.flush()?;
120        Ok(())
121    }
122
123    pub fn execute(self) -> Result<()> {
124        // Load environment variables from file if it exists
125        if self.env.exists() {
126            dotenvy::from_path(&self.env).ok();
127            log::debug!("Loaded environment variables from {}", self.env.display());
128        }
129
130        let options = self.options;
131        match self.command {
132            Command::List { file } => {
133                // Load quest file
134                let quest_file = QuestFile::load(&file)
135                    .with_context(|| format!("Failed to load quest file: {}", file.display()))?;
136
137                Self::list_quests(quest_file)?;
138                Ok(())
139            }
140            Command::Go { name, file } => {
141                // 1. Load quest file
142                let quest_file = QuestFile::load(&file)
143                    .with_context(|| format!("Failed to load quest file: {}", file.display()))?;
144
145                // 2. Find quest by name
146                let mut quest_command = quest_file
147                    .get(&name)
148                    .ok_or_else(|| anyhow::anyhow!("Quest '{}' not found.", name))?
149                    .clone();
150
151                // 3. Merge with CLI options (CLI overrides quest)
152                match &mut quest_command {
153                    QuestCommand::Get { options: qopts, .. }
154                    | QuestCommand::Delete { options: qopts, .. } => {
155                        qopts.merge_with(&options);
156                    }
157                    QuestCommand::Post { options: qopts, .. }
158                    | QuestCommand::Put { options: qopts, .. }
159                    | QuestCommand::Patch { options: qopts, .. } => {
160                        qopts.merge_with(&options);
161                    }
162                }
163
164                // 4. Execute the quest command
165                log::info!("Executing quest '{}' from {}", name, file.display());
166                Self::execute_quest_command(options, quest_command)
167            }
168            Command::Get { url } => {
169                let quest = QuestCommand::Get {
170                    url_spec: QuestUrl::Direct { url },
171                    options: RequestOptions::default(),
172                };
173                Self::execute_quest_command(options, quest)
174            }
175            Command::Post { url, body } => {
176                let quest = QuestCommand::Post {
177                    url_spec: QuestUrl::Direct { url },
178                    body,
179                    options: RequestOptions::default(),
180                };
181                Self::execute_quest_command(options, quest)
182            }
183            Command::Put { url, body } => {
184                let quest = QuestCommand::Put {
185                    url_spec: QuestUrl::Direct { url },
186                    body,
187                    options: RequestOptions::default(),
188                };
189                Self::execute_quest_command(options, quest)
190            }
191            Command::Delete { url } => {
192                let quest = QuestCommand::Delete {
193                    url_spec: QuestUrl::Direct { url },
194                    options: RequestOptions::default(),
195                };
196                Self::execute_quest_command(options, quest)
197            }
198            Command::Patch { url, body } => {
199                let quest = QuestCommand::Patch {
200                    url_spec: QuestUrl::Direct { url },
201                    body,
202                    options: RequestOptions::default(),
203                };
204                Self::execute_quest_command(options, quest)
205            }
206        }
207    }
208
209    fn log_request_verbose(
210        url: &Url,
211        method: &str,
212        headers: &HeaderOptions,
213        auth: &AuthOptions,
214    ) -> Result<()> {
215        use colored::Colorize;
216        use std::io::Write;
217
218        let stderr = std::io::stderr();
219        let mut handle = stderr.lock();
220
221        // Request line in cyan/bold
222        writeln!(
223            handle,
224            "{} {} HTTP/1.1",
225            method.cyan().bold(),
226            url.as_str().cyan().bold()
227        )?;
228
229        // Headers in blue
230        writeln!(
231            handle,
232            "{} {}",
233            "Host:".blue(),
234            url.host_str().unwrap_or("unknown")
235        )?;
236
237        // Show headers that will be sent
238        if let Some(user_agent) = &headers.user_agent {
239            writeln!(handle, "{} {}", "User-Agent:".blue(), user_agent)?;
240        } else {
241            writeln!(handle, "{} quest/0.1.0", "User-Agent:".blue())?;
242        }
243
244        for header in &headers.header {
245            if let Some((key, value)) = header.split_once(':') {
246                writeln!(
247                    handle,
248                    "{} {}",
249                    format!("{}:", key.trim()).blue(),
250                    value.trim()
251                )?;
252            }
253        }
254
255        // Log authorization headers with redaction
256        if auth.bearer.is_some() {
257            writeln!(handle, "{} Bearer [REDACTED]", "Authorization:".blue())?;
258        } else if auth.auth.is_some() || auth.basic.is_some() {
259            writeln!(handle, "{} Basic [REDACTED]", "Authorization:".blue())?;
260        }
261
262        // Log Referer header if present
263        if let Some(referer) = &headers.referer {
264            writeln!(handle, "{} {}", "Referer:".blue(), referer)?;
265        }
266
267        if let Some(accept) = &headers.accept {
268            writeln!(handle, "{} {}", "Accept:".blue(), accept)?;
269        }
270
271        if let Some(content_type) = &headers.content_type {
272            writeln!(handle, "{} {}", "Content-Type:".blue(), content_type)?;
273        }
274
275        writeln!(handle)?;
276        handle.flush()?;
277        Ok(())
278    }
279
280    fn log_response_verbose(response: &reqwest::blocking::Response) -> Result<()> {
281        use colored::Colorize;
282        use std::io::Write;
283
284        let stderr = std::io::stderr();
285        let mut handle = stderr.lock();
286
287        // Status line in green/bold for success, red/bold for errors
288        let status = response.status();
289        let status_line = format!(
290            "HTTP/1.1 {} {}",
291            status.as_u16(),
292            status.canonical_reason().unwrap_or("")
293        );
294
295        if status.is_success() {
296            writeln!(handle, "{}", status_line.green().bold())?;
297        } else if status.is_client_error() || status.is_server_error() {
298            writeln!(handle, "{}", status_line.red().bold())?;
299        } else {
300            writeln!(handle, "{}", status_line.cyan().bold())?;
301        }
302
303        // Headers in cyan
304        for (name, value) in response.headers() {
305            if let Ok(val_str) = value.to_str() {
306                writeln!(handle, "{} {}", format!("{}:", name).cyan(), val_str)?;
307            }
308        }
309
310        writeln!(handle)?;
311        handle.flush()?;
312        Ok(())
313    }
314
315    fn execute_quest_command(cli_options: RequestOptions, quest: QuestCommand) -> Result<()> {
316        // 1. Extract quest file options and merge with CLI options
317        let mut quest_options = match &quest {
318            QuestCommand::Get { options, .. } => options,
319            QuestCommand::Post { options, .. } => options,
320            QuestCommand::Put { options, .. } => options,
321            QuestCommand::Delete { options, .. } => options,
322            QuestCommand::Patch { options, .. } => options,
323        }
324        .clone();
325        quest_options.merge_with(&cli_options);
326
327        // 2. Build the client with merged options
328        let client = QuestClientBuilder::new().apply(&quest_options)?.build()?;
329
330        // 3. Build the request based on the quest command and capture details for verbose output
331        let (request_builder, body_options, method, url) = match &quest {
332            QuestCommand::Get { url_spec, .. } => {
333                let url = url_spec.to_url()?;
334                (client.get(url.as_str()), None, "GET", url)
335            }
336            QuestCommand::Post { url_spec, body, .. } => {
337                let url = url_spec.to_url()?;
338                (client.post(url.as_str()), Some(body.clone()), "POST", url)
339            }
340            QuestCommand::Put { url_spec, body, .. } => {
341                let url = url_spec.to_url()?;
342                (client.put(url.as_str()), Some(body.clone()), "PUT", url)
343            }
344            QuestCommand::Delete { url_spec, .. } => {
345                let url = url_spec.to_url()?;
346                (client.delete(url.as_str()), None, "DELETE", url)
347            }
348            QuestCommand::Patch { url_spec, body, .. } => {
349                let url = url_spec.to_url()?;
350                (client.patch(url.as_str()), Some(body.clone()), "PATCH", url)
351            }
352        };
353
354        // Show request details if verbose
355        if quest_options.output.verbose {
356            Self::log_request_verbose(
357                &url,
358                method,
359                &quest_options.headers,
360                &quest_options.authorization,
361            )?;
362        }
363
364        // 4. Apply request options (use merged options)
365        let mut request =
366            QuestRequestBuilder::from_request(request_builder).apply(&quest_options)?;
367
368        // 5. Apply body options if present
369        if let Some(body) = body_options {
370            request = request.apply(&body)?;
371        }
372
373        // 6. Send the request
374        let response = request.send()?;
375
376        // 7. Handle the response
377        Self::handle_response(response, &quest_options.output)?;
378
379        Ok(())
380    }
381
382    fn handle_response(
383        response: reqwest::blocking::Response,
384        output_opts: &OutputOptions,
385    ) -> Result<()> {
386        use std::io::Write;
387
388        // Prepare output content
389        let mut output_parts = Vec::new();
390
391        // Include headers if requested
392        if output_opts.include {
393            let status_line = format!(
394                "HTTP/1.1 {} {}\n",
395                response.status().as_u16(),
396                response.status().canonical_reason().unwrap_or("")
397            );
398            output_parts.push(status_line);
399
400            for (name, value) in response.headers() {
401                if let Ok(val_str) = value.to_str() {
402                    output_parts.push(format!("{}: {}\n", name, val_str));
403                }
404            }
405            output_parts.push("\n".to_string());
406        }
407
408        if output_opts.verbose {
409            Self::log_response_verbose(&response)?;
410        }
411
412        // Get response body
413        let body_text = if response
414            .headers()
415            .get("content-type")
416            .and_then(|v| v.to_str().ok())
417            .map(|ct| ct.contains("application/json") || ct.contains("application/vnd.api+json"))
418            .unwrap_or(false)
419            && !output_opts.simple
420        {
421            // Parse as JSON and pretty-print with colors
422            let json_value = response.json::<serde_json::Value>()?;
423            colored_json::to_colored_json_auto(&json_value)?
424        } else {
425            // Not JSON or unknown content-type, just get as text
426            response.text()?
427        };
428
429        output_parts.push(body_text);
430
431        let full_output = output_parts.join("");
432
433        // Write to file or stdout
434        if let Some(output_file) = &output_opts.output {
435            let mut file = std::fs::File::create(output_file).with_context(|| {
436                format!("Failed to create output file: {}", output_file.display())
437            })?;
438            file.write_all(full_output.as_bytes())?;
439        } else {
440            println!("{full_output}");
441        }
442
443        Ok(())
444    }
445}
446
447#[derive(Debug, Subcommand, Clone)]
448pub enum Command {
449    Get {
450        url: Url,
451    },
452    Post {
453        url: Url,
454        #[clap(flatten)]
455        body: BodyOptions,
456    },
457    Put {
458        url: Url,
459        #[clap(flatten)]
460        body: BodyOptions,
461    },
462    Delete {
463        url: Url,
464    },
465    Patch {
466        url: Url,
467        #[clap(flatten)]
468        body: BodyOptions,
469    },
470    /// Run a named quest from a quest file
471    Go {
472        /// Quest name to execute
473        name: String,
474
475        #[arg(
476            short,
477            long,
478            default_value = ".quests.yaml",
479            help = "Quest file to load from"
480        )]
481        file: PathBuf,
482    },
483    /// List all quests from a quest file
484    List {
485        #[arg(
486            short,
487            long,
488            default_value = ".quests.yaml",
489            help = "Quest file to load from"
490        )]
491        file: PathBuf,
492    },
493}
494
495#[derive(Debug, Args, Clone, Default, Deserialize)]
496#[serde(default)]
497pub struct RequestOptions {
498    #[serde(flatten)]
499    #[clap(flatten)]
500    pub authorization: AuthOptions,
501    #[serde(flatten)]
502    #[clap(flatten)]
503    pub headers: HeaderOptions,
504    #[serde(flatten)]
505    #[clap(flatten)]
506    pub params: ParamOptions,
507    #[serde(flatten)]
508    #[clap(flatten)]
509    pub timeouts: TimeoutOptions,
510    #[serde(flatten)]
511    #[clap(flatten)]
512    pub redirects: RedirectOptions,
513    #[serde(flatten)]
514    #[clap(flatten)]
515    pub tls: TlsOptions,
516    #[serde(flatten)]
517    #[clap(flatten)]
518    pub proxy: ProxyOptions,
519    #[serde(flatten)]
520    #[clap(flatten)]
521    pub output: OutputOptions,
522    #[serde(flatten)]
523    #[clap(flatten)]
524    pub compression: CompressionOptions,
525}
526
527#[derive(Debug, Args, Clone, Default, Deserialize)]
528#[serde(default)]
529pub struct AuthOptions {
530    #[arg(short, long, global = true)]
531    pub auth: Option<SecretString>,
532    #[arg(long, global = true)]
533    pub basic: Option<SecretString>,
534    #[arg(long, global = true)]
535    pub bearer: Option<SecretString>,
536}
537
538#[derive(Debug, Args, Clone, Default, Deserialize)]
539#[serde(default)]
540pub struct HeaderOptions {
541    #[serde(rename = "headers")]
542    #[arg(
543        short = 'H',
544        long = "header",
545        global = true,
546        help = "Custom header (repeatable)"
547    )]
548    pub header: Vec<String>,
549    #[arg(
550        short = 'U',
551        long = "user-agent",
552        global = true,
553        help = "Set User-Agent header"
554    )]
555    pub user_agent: Option<String>,
556    #[arg(
557        short = 'R',
558        long = "referer",
559        global = true,
560        help = "Set Referer header"
561    )]
562    pub referer: Option<String>,
563    #[arg(long = "content-type", global = true, help = "Set Content-Type header")]
564    pub content_type: Option<String>,
565    #[arg(long = "accept", global = true, help = "Set Accept header")]
566    pub accept: Option<String>,
567}
568
569#[derive(Debug, Args, Clone, Default, Deserialize)]
570#[serde(default)]
571pub struct ParamOptions {
572    #[serde(rename = "params")]
573    #[arg(
574        short = 'p',
575        long = "param",
576        global = true,
577        help = "Query parameter (repeatable)"
578    )]
579    pub param: Vec<String>,
580}
581
582#[derive(Debug, Args, Clone, Default, Deserialize)]
583#[serde(default)]
584pub struct TimeoutOptions {
585    #[arg(
586        short = 't',
587        long = "timeout",
588        global = true,
589        help = "Overall request timeout (e.g., '30s', '1m')"
590    )]
591    pub timeout: Option<DurationString>,
592    #[arg(
593        long = "connect-timeout",
594        global = true,
595        help = "Connection timeout (e.g., '10s')"
596    )]
597    pub connect_timeout: Option<DurationString>,
598}
599
600#[derive(Debug, Args, Clone, Default, Deserialize)]
601#[serde(default)]
602pub struct BodyOptions {
603    #[arg(
604        short = 'j',
605        long = "json",
606        group = "body",
607        help = "Send data as JSON (auto sets Content-Type)",
608        value_hint = clap::ValueHint::FilePath
609    )]
610    pub json: Option<StringOrFile>,
611    #[arg(
612        short = 'F',
613        long = "form",
614        group = "body",
615        help = "Form data (repeatable)"
616    )]
617    pub form: Vec<FormField>,
618    #[arg(
619        long = "raw",
620        group = "body",
621        help = "Send raw data without processing",
622        value_hint = clap::ValueHint::FilePath
623    )]
624    pub raw: Option<StringOrFile>,
625    #[arg(
626        long = "binary",
627        group = "body",
628        help = "Send binary data",
629        value_hint = clap::ValueHint::FilePath
630    )]
631    pub binary: Option<StringOrFile>,
632}
633
634#[derive(Debug, Args, Clone, Default, Deserialize)]
635#[serde(default)]
636pub struct RedirectOptions {
637    #[arg(
638        short = 'L',
639        long = "location",
640        global = true,
641        help = "Follow redirects"
642    )]
643    pub location: bool,
644    #[arg(
645        long = "max-redirects",
646        global = true,
647        help = "Maximum number of redirects to follow"
648    )]
649    pub max_redirects: Option<u32>,
650}
651
652#[derive(Debug, Args, Clone, Default, Deserialize)]
653#[serde(default)]
654pub struct TlsOptions {
655    #[arg(
656        short = 'k',
657        long = "insecure",
658        global = true,
659        help = "Skip TLS verification"
660    )]
661    pub insecure: bool,
662    #[arg(
663        long = "cert",
664        global = true,
665        help = "Client certificate file (PEM format)"
666    )]
667    pub cert: Option<PathBuf>,
668    #[arg(
669        long = "key",
670        global = true,
671        help = "Client certificate key file (PEM format)"
672    )]
673    pub key: Option<PathBuf>,
674    #[arg(
675        long = "cacert",
676        global = true,
677        help = "CA certificate to verify peer against"
678    )]
679    pub cacert: Option<PathBuf>,
680}
681
682#[derive(Debug, Args, Clone, Default, Deserialize)]
683#[serde(default)]
684pub struct ProxyOptions {
685    #[arg(short = 'x', long = "proxy", global = true, help = "Proxy server URL")]
686    pub proxy: Option<Url>,
687    #[arg(long = "proxy-auth", global = true, help = "Proxy authentication")]
688    pub proxy_auth: Option<SecretString>,
689}
690
691#[derive(Debug, Args, Clone, Default, Deserialize)]
692#[serde(default)]
693pub struct OutputOptions {
694    #[arg(
695        short = 'o',
696        long = "output",
697        global = true,
698        help = "Write output to file instead of stdout"
699    )]
700    pub output: Option<PathBuf>,
701    #[arg(
702        short = 'i',
703        long = "include",
704        global = true,
705        help = "Include response headers in output"
706    )]
707    pub include: bool,
708    #[arg(
709        short,
710        long = "verbose",
711        global = true,
712        help = "Show detailed request/response info"
713    )]
714    pub verbose: bool,
715    #[arg(
716        short,
717        long = "simple",
718        global = true,
719        help = "Show response without color formatting"
720    )]
721    pub simple: bool,
722}
723
724#[derive(Debug, Args, Clone, Default, Deserialize)]
725#[serde(default)]
726pub struct CompressionOptions {
727    #[arg(
728        long = "compressed",
729        global = true,
730        help = "Request compressed response (gzip, deflate, br)"
731    )]
732    pub compressed: bool,
733}
734
735// Merge implementations for combining quest options with CLI options
736impl RequestOptions {
737    pub fn merge_with(&mut self, cli_options: &RequestOptions) {
738        self.authorization.merge_with(&cli_options.authorization);
739        self.headers.merge_with(&cli_options.headers);
740        self.params.merge_with(&cli_options.params);
741        self.timeouts.merge_with(&cli_options.timeouts);
742        self.redirects.merge_with(&cli_options.redirects);
743        self.tls.merge_with(&cli_options.tls);
744        self.proxy.merge_with(&cli_options.proxy);
745        self.output.merge_with(&cli_options.output);
746        self.compression.merge_with(&cli_options.compression);
747    }
748}
749
750impl AuthOptions {
751    pub fn merge_with(&mut self, cli: &AuthOptions) {
752        if cli.auth.is_some() {
753            self.auth = cli.auth.clone();
754        }
755        if cli.basic.is_some() {
756            self.basic = cli.basic.clone();
757        }
758        if cli.bearer.is_some() {
759            self.bearer = cli.bearer.clone();
760        }
761    }
762}
763
764impl HeaderOptions {
765    pub fn merge_with(&mut self, cli: &HeaderOptions) {
766        // Collections: simple concatenation
767        self.header.extend(cli.header.clone());
768
769        // Scalar overrides
770        if cli.user_agent.is_some() {
771            self.user_agent = cli.user_agent.clone();
772        }
773        if cli.referer.is_some() {
774            self.referer = cli.referer.clone();
775        }
776        if cli.content_type.is_some() {
777            self.content_type = cli.content_type.clone();
778        }
779        if cli.accept.is_some() {
780            self.accept = cli.accept.clone();
781        }
782    }
783}
784
785impl ParamOptions {
786    pub fn merge_with(&mut self, cli: &ParamOptions) {
787        // Concatenate parameters
788        self.param.extend(cli.param.clone());
789    }
790}
791
792impl TimeoutOptions {
793    pub fn merge_with(&mut self, cli: &TimeoutOptions) {
794        if cli.timeout.is_some() {
795            self.timeout = cli.timeout;
796        }
797        if cli.connect_timeout.is_some() {
798            self.connect_timeout = cli.connect_timeout;
799        }
800    }
801}
802
803impl RedirectOptions {
804    pub fn merge_with(&mut self, cli: &RedirectOptions) {
805        if cli.location {
806            self.location = cli.location;
807        }
808        if cli.max_redirects.is_some() {
809            self.max_redirects = cli.max_redirects;
810        }
811    }
812}
813
814impl TlsOptions {
815    pub fn merge_with(&mut self, cli: &TlsOptions) {
816        if cli.insecure {
817            self.insecure = cli.insecure;
818        }
819        if cli.cert.is_some() {
820            self.cert = cli.cert.clone();
821        }
822        if cli.key.is_some() {
823            self.key = cli.key.clone();
824        }
825        if cli.cacert.is_some() {
826            self.cacert = cli.cacert.clone();
827        }
828    }
829}
830
831impl ProxyOptions {
832    pub fn merge_with(&mut self, cli: &ProxyOptions) {
833        if cli.proxy.is_some() {
834            self.proxy = cli.proxy.clone();
835        }
836        if cli.proxy_auth.is_some() {
837            self.proxy_auth = cli.proxy_auth.clone();
838        }
839    }
840}
841
842impl OutputOptions {
843    pub fn merge_with(&mut self, cli: &OutputOptions) {
844        if cli.output.is_some() {
845            self.output = cli.output.clone();
846        }
847        if cli.include {
848            self.include = cli.include;
849        }
850        if cli.verbose {
851            self.verbose = cli.verbose;
852        }
853        if cli.simple {
854            self.simple = cli.simple;
855        }
856    }
857}
858
859impl CompressionOptions {
860    pub fn merge_with(&mut self, cli: &CompressionOptions) {
861        if cli.compressed {
862            self.compressed = cli.compressed;
863        }
864    }
865}