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 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 let max_name_width = quest_data
69 .iter()
70 .map(|(name, _, _)| name.len())
71 .max()
72 .unwrap_or(4)
73 .max(4); let max_method_width = 6; 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 writeln!(
90 handle,
91 "{} {} {}",
92 "─".repeat(max_name_width),
93 "─".repeat(max_method_width),
94 "─".repeat(40)
95 )?;
96
97 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 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 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 let quest_file = QuestFile::load(&file)
143 .with_context(|| format!("Failed to load quest file: {}", file.display()))?;
144
145 let quest_command = quest_file
147 .get(&name)
148 .ok_or_else(|| anyhow::anyhow!("Quest '{}' not found.", name))?
149 .clone();
150
151 log::info!("Executing quest '{}' from {}", name, file.display());
153 Self::execute_quest_command(options, quest_command)
154 }
155 Command::Get { url } => {
156 let quest = QuestCommand::Get {
157 url_spec: QuestUrl::Direct { url },
158 options: RequestOptions::default(),
159 };
160 Self::execute_quest_command(options, quest)
161 }
162 Command::Post { url } => {
163 let quest = QuestCommand::Post {
164 url_spec: QuestUrl::Direct { url },
165 body: BodyOptions::default(),
166 options: RequestOptions::default(),
167 };
168 Self::execute_quest_command(options, quest)
169 }
170 Command::Put { url } => {
171 let quest = QuestCommand::Put {
172 url_spec: QuestUrl::Direct { url },
173 body: BodyOptions::default(),
174 options: RequestOptions::default(),
175 };
176 Self::execute_quest_command(options, quest)
177 }
178 Command::Delete { url } => {
179 let quest = QuestCommand::Delete {
180 url_spec: QuestUrl::Direct { url },
181 options: RequestOptions::default(),
182 };
183 Self::execute_quest_command(options, quest)
184 }
185 Command::Patch { url } => {
186 let quest = QuestCommand::Patch {
187 url_spec: QuestUrl::Direct { url },
188 body: BodyOptions::default(),
189 options: RequestOptions::default(),
190 };
191 Self::execute_quest_command(options, quest)
192 }
193 }
194 }
195
196 fn log_request_verbose(
197 url: &Url,
198 method: &str,
199 headers: &HeaderOptions,
200 auth: &AuthOptions,
201 ) -> Result<()> {
202 use colored::Colorize;
203 use std::io::Write;
204
205 let stderr = std::io::stderr();
206 let mut handle = stderr.lock();
207
208 writeln!(
210 handle,
211 "{} {} HTTP/1.1",
212 method.cyan().bold(),
213 url.as_str().cyan().bold()
214 )?;
215
216 writeln!(
218 handle,
219 "{} {}",
220 "Host:".blue(),
221 url.host_str().unwrap_or("unknown")
222 )?;
223
224 if let Some(user_agent) = &headers.user_agent {
226 writeln!(handle, "{} {}", "User-Agent:".blue(), user_agent)?;
227 } else {
228 writeln!(handle, "{} quest/0.1.0", "User-Agent:".blue())?;
229 }
230
231 for header in &headers.header {
232 if let Some((key, value)) = header.split_once(':') {
233 writeln!(
234 handle,
235 "{} {}",
236 format!("{}:", key.trim()).blue(),
237 value.trim()
238 )?;
239 }
240 }
241
242 if auth.bearer.is_some() {
244 writeln!(handle, "{} Bearer [REDACTED]", "Authorization:".blue())?;
245 } else if auth.auth.is_some() || auth.basic.is_some() {
246 writeln!(handle, "{} Basic [REDACTED]", "Authorization:".blue())?;
247 }
248
249 if let Some(referer) = &headers.referer {
251 writeln!(handle, "{} {}", "Referer:".blue(), referer)?;
252 }
253
254 if let Some(accept) = &headers.accept {
255 writeln!(handle, "{} {}", "Accept:".blue(), accept)?;
256 }
257
258 if let Some(content_type) = &headers.content_type {
259 writeln!(handle, "{} {}", "Content-Type:".blue(), content_type)?;
260 }
261
262 writeln!(handle)?;
263 handle.flush()?;
264 Ok(())
265 }
266
267 fn log_response_verbose(response: &reqwest::blocking::Response) -> Result<()> {
268 use colored::Colorize;
269 use std::io::Write;
270
271 let stderr = std::io::stderr();
272 let mut handle = stderr.lock();
273
274 let status = response.status();
276 let status_line = format!(
277 "HTTP/1.1 {} {}",
278 status.as_u16(),
279 status.canonical_reason().unwrap_or("")
280 );
281
282 if status.is_success() {
283 writeln!(handle, "{}", status_line.green().bold())?;
284 } else if status.is_client_error() || status.is_server_error() {
285 writeln!(handle, "{}", status_line.red().bold())?;
286 } else {
287 writeln!(handle, "{}", status_line.cyan().bold())?;
288 }
289
290 for (name, value) in response.headers() {
292 if let Ok(val_str) = value.to_str() {
293 writeln!(handle, "{} {}", format!("{}:", name).cyan(), val_str)?;
294 }
295 }
296
297 writeln!(handle)?;
298 handle.flush()?;
299 Ok(())
300 }
301
302 fn execute_quest_command(cli_options: RequestOptions, quest: QuestCommand) -> Result<()> {
303 let (mut quest_options, quest_body) = match &quest {
305 QuestCommand::Get { options, .. } => (options.clone(), None),
306 QuestCommand::Post { options, body, .. } => (options.clone(), Some(body)),
307 QuestCommand::Put { options, body, .. } => (options.clone(), Some(body)),
308 QuestCommand::Delete { options, .. } => (options.clone(), None),
309 QuestCommand::Patch { options, body, .. } => (options.clone(), Some(body)),
310 };
311
312 if let Some(body) = quest_body {
314 quest_options.body = body.clone();
315 }
316
317 quest_options.merge_with(&cli_options)?;
319
320 let client = QuestClientBuilder::new().apply(&quest_options)?.build()?;
322
323 let (request_builder, method, url) = match &quest {
325 QuestCommand::Get { url_spec, .. } => {
326 let url = url_spec.to_url()?;
327 (client.get(url.as_str()), "GET", url)
328 }
329 QuestCommand::Post { url_spec, .. } => {
330 let url = url_spec.to_url()?;
331 (client.post(url.as_str()), "POST", url)
332 }
333 QuestCommand::Put { url_spec, .. } => {
334 let url = url_spec.to_url()?;
335 (client.put(url.as_str()), "PUT", url)
336 }
337 QuestCommand::Delete { url_spec, .. } => {
338 let url = url_spec.to_url()?;
339 (client.delete(url.as_str()), "DELETE", url)
340 }
341 QuestCommand::Patch { url_spec, .. } => {
342 let url = url_spec.to_url()?;
343 (client.patch(url.as_str()), "PATCH", url)
344 }
345 };
346
347 if quest_options.output.verbose {
349 Self::log_request_verbose(
350 &url,
351 method,
352 &quest_options.headers,
353 &quest_options.authorization,
354 )?;
355 }
356
357 let request = QuestRequestBuilder::from_request(request_builder).apply(&quest_options)?;
359
360 let response = request.send()?;
362
363 Self::handle_response(response, &quest_options.output)?;
365
366 Ok(())
367 }
368
369 fn handle_response(
370 response: reqwest::blocking::Response,
371 output_opts: &OutputOptions,
372 ) -> Result<()> {
373 use std::io::Write;
374
375 if output_opts.verbose {
376 Self::log_response_verbose(&response)?;
377 }
378
379 let content = response.bytes()?;
380
381 let output = if !output_opts.simple
382 && output_opts.output.is_none()
383 && let Ok(json) = serde_json::from_slice::<serde_json::Value>(&content)
384 && let Ok(formatted) = colored_json::to_colored_json_auto(&json)
385 {
386 Output::Text(formatted)
387 } else if let Ok(text) = String::from_utf8(content.to_vec()) {
388 Output::Text(text)
389 } else {
390 Output::Bytes(content.to_vec())
391 };
392
393 match (output, &output_opts.output) {
394 (output, Some(path)) => {
395 let bytes = output.into_bytes();
396 let mut file = std::fs::File::create(path)
397 .with_context(|| format!("Failed to create output file: {}", path.display()))?;
398 file.write_all(&bytes)?;
399 }
400 (Output::Text(s), None) => {
401 let mut stdout = std::io::stdout().lock();
402 writeln!(stdout, "{s}").or_else(|e| {
403 if e.kind() == std::io::ErrorKind::BrokenPipe {
404 Ok(())
405 } else {
406 Err(e)
407 }
408 })?
409 }
410 (Output::Bytes(v), None) => {
411 anyhow::bail!("[Binary data: {} bytes - use --output to save]", v.len());
412 }
413 }
414
415 Ok(())
416 }
417}
418
419enum Output {
420 Text(String),
421 Bytes(Vec<u8>),
422}
423
424impl Output {
425 pub fn into_bytes(self) -> Vec<u8> {
426 match self {
427 Self::Bytes(v) => v,
428 Self::Text(s) => s.into_bytes(),
429 }
430 }
431}
432
433#[derive(Debug, Subcommand, Clone)]
434pub enum Command {
435 Get {
436 url: Url,
437 },
438 Post {
439 url: Url,
440 },
441 Put {
442 url: Url,
443 },
444 Delete {
445 url: Url,
446 },
447 Patch {
448 url: Url,
449 },
450 Go {
452 name: String,
454
455 #[arg(
456 short,
457 long,
458 default_value = ".quests.yaml",
459 help = "Quest file to load from"
460 )]
461 file: PathBuf,
462 },
463 List {
465 #[arg(
466 short,
467 long,
468 default_value = ".quests.yaml",
469 help = "Quest file to load from"
470 )]
471 file: PathBuf,
472 },
473}
474
475#[derive(Debug, Args, Clone, Default, Deserialize)]
476#[serde(default)]
477pub struct RequestOptions {
478 #[serde(flatten)]
479 #[clap(flatten)]
480 pub authorization: AuthOptions,
481 #[serde(flatten)]
482 #[clap(flatten)]
483 pub headers: HeaderOptions,
484 #[serde(flatten)]
485 #[clap(flatten)]
486 pub params: ParamOptions,
487 #[serde(flatten)]
488 #[clap(flatten)]
489 pub body: BodyOptions,
490 #[serde(flatten)]
491 #[clap(flatten)]
492 pub timeouts: TimeoutOptions,
493 #[serde(flatten)]
494 #[clap(flatten)]
495 pub redirects: RedirectOptions,
496 #[serde(flatten)]
497 #[clap(flatten)]
498 pub tls: TlsOptions,
499 #[serde(flatten)]
500 #[clap(flatten)]
501 pub proxy: ProxyOptions,
502 #[serde(flatten)]
503 #[clap(flatten)]
504 pub output: OutputOptions,
505 #[serde(flatten)]
506 #[clap(flatten)]
507 pub compression: CompressionOptions,
508}
509
510#[derive(Debug, Args, Clone, Default, Deserialize)]
511#[serde(default)]
512pub struct AuthOptions {
513 #[arg(short, long, global = true)]
514 pub auth: Option<SecretString>,
515 #[arg(long, global = true)]
516 pub basic: Option<SecretString>,
517 #[arg(long, global = true)]
518 pub bearer: Option<SecretString>,
519}
520
521#[derive(Debug, Args, Clone, Default, Deserialize)]
522#[serde(default)]
523pub struct HeaderOptions {
524 #[serde(rename = "headers")]
525 #[arg(
526 short = 'H',
527 long = "header",
528 global = true,
529 help = "Custom header (repeatable)"
530 )]
531 pub header: Vec<String>,
532 #[arg(
533 short = 'U',
534 long = "user-agent",
535 global = true,
536 help = "Set User-Agent header"
537 )]
538 pub user_agent: Option<String>,
539 #[arg(
540 short = 'R',
541 long = "referer",
542 global = true,
543 help = "Set Referer header"
544 )]
545 pub referer: Option<String>,
546 #[arg(long = "content-type", global = true, help = "Set Content-Type header")]
547 pub content_type: Option<String>,
548 #[arg(long = "accept", global = true, help = "Set Accept header")]
549 pub accept: Option<String>,
550}
551
552#[derive(Debug, Args, Clone, Default, Deserialize)]
553#[serde(default)]
554pub struct ParamOptions {
555 #[serde(rename = "params")]
556 #[arg(
557 short = 'p',
558 long = "param",
559 global = true,
560 help = "Query parameter (repeatable)"
561 )]
562 pub param: Vec<String>,
563}
564
565#[derive(Debug, Args, Clone, Default, Deserialize)]
566#[serde(default)]
567pub struct TimeoutOptions {
568 #[arg(
569 short = 't',
570 long = "timeout",
571 global = true,
572 help = "Overall request timeout (e.g., '30s', '1m')"
573 )]
574 pub timeout: Option<DurationString>,
575 #[arg(
576 long = "connect-timeout",
577 global = true,
578 help = "Connection timeout (e.g., '10s')"
579 )]
580 pub connect_timeout: Option<DurationString>,
581}
582
583#[derive(Debug, Args, Clone, Default, Deserialize)]
584#[serde(default)]
585pub struct BodyOptions {
586 #[arg(
587 short = 'j',
588 long = "json",
589 group = "body",
590 global = true,
591 help = "Send data as JSON (auto sets Content-Type)",
592 value_hint = clap::ValueHint::FilePath
593 )]
594 pub json: Option<StringOrFile>,
595 #[arg(
596 short = 'F',
597 long = "form",
598 group = "body",
599 global = true,
600 help = "Form data (repeatable)"
601 )]
602 pub form: Vec<FormField>,
603 #[arg(
604 long = "raw",
605 group = "body",
606 global = true,
607 help = "Send raw data without processing",
608 value_hint = clap::ValueHint::FilePath
609 )]
610 pub raw: Option<StringOrFile>,
611 #[arg(
612 long = "binary",
613 group = "body",
614 global = true,
615 help = "Send binary data",
616 value_hint = clap::ValueHint::FilePath
617 )]
618 pub binary: Option<StringOrFile>,
619}
620
621#[derive(Debug, Args, Clone, Default, Deserialize)]
622#[serde(default)]
623pub struct RedirectOptions {
624 #[arg(
625 short = 'L',
626 long = "location",
627 global = true,
628 help = "Follow redirects"
629 )]
630 pub location: bool,
631 #[arg(
632 long = "max-redirects",
633 global = true,
634 help = "Maximum number of redirects to follow"
635 )]
636 pub max_redirects: Option<u32>,
637}
638
639#[derive(Debug, Args, Clone, Default, Deserialize)]
640#[serde(default)]
641pub struct TlsOptions {
642 #[arg(
643 short = 'k',
644 long = "insecure",
645 global = true,
646 help = "Skip TLS verification"
647 )]
648 pub insecure: bool,
649 #[arg(
650 long = "cert",
651 global = true,
652 help = "Client certificate file (PEM format)"
653 )]
654 pub cert: Option<PathBuf>,
655 #[arg(
656 long = "key",
657 global = true,
658 help = "Client certificate key file (PEM format)"
659 )]
660 pub key: Option<PathBuf>,
661 #[arg(
662 long = "cacert",
663 global = true,
664 help = "CA certificate to verify peer against"
665 )]
666 pub cacert: Option<PathBuf>,
667}
668
669#[derive(Debug, Args, Clone, Default, Deserialize)]
670#[serde(default)]
671pub struct ProxyOptions {
672 #[arg(short = 'x', long = "proxy", global = true, help = "Proxy server URL")]
673 pub proxy: Option<Url>,
674 #[arg(long = "proxy-auth", global = true, help = "Proxy authentication")]
675 pub proxy_auth: Option<SecretString>,
676}
677
678#[derive(Debug, Args, Clone, Default, Deserialize)]
679#[serde(default)]
680pub struct OutputOptions {
681 #[arg(
682 short = 'o',
683 long = "output",
684 global = true,
685 help = "Write output to file instead of stdout"
686 )]
687 pub output: Option<PathBuf>,
688
689 #[arg(
690 short,
691 long = "verbose",
692 global = true,
693 help = "Show detailed request/response info"
694 )]
695 pub verbose: bool,
696 #[arg(
697 short,
698 long = "simple",
699 global = true,
700 help = "Show response without color formatting"
701 )]
702 pub simple: bool,
703}
704
705#[derive(Debug, Args, Clone, Default, Deserialize)]
706#[serde(default)]
707pub struct CompressionOptions {
708 #[arg(
709 long = "compressed",
710 global = true,
711 help = "Request compressed response (gzip, deflate, br)"
712 )]
713 pub compressed: bool,
714}
715
716impl RequestOptions {
718 pub fn merge_with(&mut self, cli_options: &RequestOptions) -> Result<&Self> {
719 self.authorization.merge_with(&cli_options.authorization)?;
720 self.headers.merge_with(&cli_options.headers)?;
721 self.params.merge_with(&cli_options.params)?;
722 self.body.merge_with(&cli_options.body)?;
723 self.timeouts.merge_with(&cli_options.timeouts)?;
724 self.redirects.merge_with(&cli_options.redirects)?;
725 self.tls.merge_with(&cli_options.tls)?;
726 self.proxy.merge_with(&cli_options.proxy)?;
727 self.output.merge_with(&cli_options.output)?;
728 self.compression.merge_with(&cli_options.compression)?;
729
730 Ok(self)
731 }
732}
733
734impl AuthOptions {
735 pub fn merge_with(&mut self, cli: &AuthOptions) -> Result<&Self> {
736 if cli.auth.is_some() {
737 self.auth = cli.auth.clone();
738 }
739 if cli.basic.is_some() {
740 self.basic = cli.basic.clone();
741 }
742 if cli.bearer.is_some() {
743 self.bearer = cli.bearer.clone();
744 }
745
746 Ok(self)
747 }
748}
749
750impl HeaderOptions {
751 pub fn merge_with(&mut self, cli: &HeaderOptions) -> Result<&Self> {
752 self.header.extend(cli.header.clone());
754
755 if cli.user_agent.is_some() {
757 self.user_agent = cli.user_agent.clone();
758 }
759 if cli.referer.is_some() {
760 self.referer = cli.referer.clone();
761 }
762 if cli.content_type.is_some() {
763 self.content_type = cli.content_type.clone();
764 }
765 if cli.accept.is_some() {
766 self.accept = cli.accept.clone();
767 }
768
769 Ok(self)
770 }
771}
772
773impl ParamOptions {
774 pub fn merge_with(&mut self, cli: &ParamOptions) -> Result<&Self> {
775 use std::collections::{BTreeMap, BTreeSet};
778
779 let mut params: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
780
781 for param in &self.param {
782 let (key, value) = param.split_once("=").ok_or_else(|| anyhow::anyhow!(
783 "Invalid parameter format: '{}'. Expected format: 'key=value' (must contain an equals sign)",
784 param
785 ))?;
786
787 params
788 .entry(key.trim().to_string())
789 .or_default()
790 .insert(value.trim().to_string());
791 }
792
793 let mut cli_params: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
794
795 for param in &cli.param {
796 let (key, value) = param.split_once("=").ok_or_else(|| anyhow::anyhow!(
797 "Invalid parameter format: '{}'. Expected format: 'key=value' (must contain an equals sign)",
798 param
799 ))?;
800
801 cli_params
802 .entry(key.trim().to_string())
803 .or_default()
804 .insert(value.trim().to_string());
805 }
806
807 params.extend(cli_params);
808
809 self.param = params
811 .into_iter()
812 .flat_map(|(key, values)| {
813 values
814 .into_iter()
815 .map(|value| format!("{key}={value}"))
816 .collect::<Vec<_>>()
817 })
818 .collect();
819
820 Ok(self)
821 }
822}
823
824impl TimeoutOptions {
825 pub fn merge_with(&mut self, cli: &TimeoutOptions) -> Result<&Self> {
826 if cli.timeout.is_some() {
827 self.timeout = cli.timeout;
828 }
829 if cli.connect_timeout.is_some() {
830 self.connect_timeout = cli.connect_timeout;
831 }
832
833 Ok(self)
834 }
835}
836
837impl RedirectOptions {
838 pub fn merge_with(&mut self, cli: &RedirectOptions) -> Result<&Self> {
839 if cli.location {
840 self.location = cli.location;
841 }
842 if cli.max_redirects.is_some() {
843 self.max_redirects = cli.max_redirects;
844 }
845
846 Ok(self)
847 }
848}
849
850impl TlsOptions {
851 pub fn merge_with(&mut self, cli: &TlsOptions) -> Result<&Self> {
852 if cli.insecure {
853 self.insecure = cli.insecure;
854 }
855 if cli.cert.is_some() {
856 self.cert = cli.cert.clone();
857 }
858 if cli.key.is_some() {
859 self.key = cli.key.clone();
860 }
861 if cli.cacert.is_some() {
862 self.cacert = cli.cacert.clone();
863 }
864
865 Ok(self)
866 }
867}
868
869impl ProxyOptions {
870 pub fn merge_with(&mut self, cli: &ProxyOptions) -> Result<&Self> {
871 if cli.proxy.is_some() {
872 self.proxy = cli.proxy.clone();
873 }
874 if cli.proxy_auth.is_some() {
875 self.proxy_auth = cli.proxy_auth.clone();
876 }
877
878 Ok(self)
879 }
880}
881
882impl OutputOptions {
883 pub fn merge_with(&mut self, cli: &OutputOptions) -> Result<&Self> {
884 if cli.output.is_some() {
885 self.output = cli.output.clone();
886 }
887 if cli.verbose {
888 self.verbose = cli.verbose;
889 }
890 if cli.simple {
891 self.simple = cli.simple;
892 }
893
894 Ok(self)
895 }
896}
897
898impl CompressionOptions {
899 pub fn merge_with(&mut self, cli: &CompressionOptions) -> Result<&Self> {
900 if cli.compressed {
901 self.compressed = cli.compressed;
902 }
903
904 Ok(self)
905 }
906}
907
908impl BodyOptions {
909 pub fn merge_with(&mut self, cli: &BodyOptions) -> Result<&Self> {
910 if cli.json.is_some() | !cli.form.is_empty() | cli.raw.is_some() | cli.binary.is_some() {
913 *self = cli.clone();
914 }
915
916 Ok(self)
918 }
919}