Repository: ducaale/xh (rust)

Issue:
`--raw` is ignored when translating xh commands using `--curl`
#### Expected
```sh
$ xh httpbin.org/post --raw=hello --curl
# Not sure if content-type should be set if body is invalid JSON
curl http://httpbin.org/post -H 'content-type: application/json' -H 'accept: application/json, */*;q=0.5' -d 'hello'
```

#### Actual
```sh
$ xh httpbin.org/post --raw=hello --curl
curl http://httpbin.org/post
```

Pull request:
to_curl: Support --raw flag
Resolves #252

Patch:
diff --git a/src/cli.rs b/src/cli.rs
index 399b2c2..8b6d771 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -59,7 +59,7 @@ pub struct Cli {
     /// Like --form, but force a multipart/form-data request even without files.
     ///
     /// Overrides both --json and --form.
-    #[clap(long, overrides_with_all = &["json", "form"])]
+    #[clap(long, overrides_with_all = &["json", "form"], conflicts_with = "raw")]
     pub multipart: bool,
 
     /// Pass raw request data without extra processing.
@@ -572,6 +572,12 @@ impl Cli {
         } else if self.multipart {
             self.request_items.body_type = BodyType::Multipart;
         }
+        if self.raw.is_some() && !self.request_items.is_body_empty() {
+            return Err(Self::into_app().error(
+                ErrorKind::ValueValidation,
+                "Request body (from --raw) and request data (key=value) cannot be mixed.",
+            ));
+        }
         if self.session_read_only.is_some() {
             self.is_session_read_only = true;
             self.session = mem::take(&mut self.session_read_only);
diff --git a/src/main.rs b/src/main.rs
index 5b6ba13..f996fd4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -37,7 +37,7 @@ use reqwest::tls;
 
 use crate::auth::{Auth, DigestAuthMiddleware};
 use crate::buffer::Buffer;
-use crate::cli::{BodyType, Cli, HttpVersion, Print, Proxy, Verify};
+use crate::cli::{Cli, HttpVersion, Print, Proxy, Verify};
 use crate::download::{download_file, get_file_size};
 use crate::middleware::ClientWithMiddleware;
 use crate::printer::Printer;
@@ -119,26 +119,9 @@ fn run(args: Cli) -> Result<i32> {
     let url = url_with_query(args.url, &args.request_items.query()?);
 
     let use_stdin = !(args.ignore_stdin || atty::is(Stream::Stdin) || test_pretend_term());
-    let body_type = args.request_items.body_type;
-    let body_from_request_items = args.request_items.body()?;
-    let has_request_items = !body_from_request_items.is_empty();
 
-    let body = match (use_stdin, args.raw, has_request_items) {
-        (true, None, false) => {
-            let mut buffer = Vec::new();
-            stdin().read_to_end(&mut buffer)?;
-            Body::Raw(buffer)
-        }
-        (false, Some(raw), false) => Body::Raw(raw.into_bytes()),
-        (false, None, _) => body_from_request_items,
-
-        (true, Some(_), _) => {
-            return Err(anyhow!(
-                "Request body from stdin and --raw cannot be mixed. \
-                Pass --ignore-stdin to ignore standard input."
-            ))
-        }
-        (true, _, true) => {
+    let body = if use_stdin {
+        if !args.request_items.is_body_empty() {
             if args.multipart {
                 // Multipart bodies are never "empty", so we can get here without request items
                 return Err(anyhow!("Cannot build a multipart request body from stdin"));
@@ -149,26 +132,28 @@ fn run(args: Cli) -> Result<i32> {
                 ));
             }
         }
-        (_, Some(_), true) => {
-            if args.multipart {
-                // Multipart bodies are never "empty", so we can get here without request items
-                return Err(anyhow!("Cannot build a multipart request body from --raw"));
-            } else {
-                return Err(anyhow!(
-                    "Request body (from --raw) and request data (key=value) cannot be mixed."
-                ));
-            }
+        if args.raw.is_some() {
+            return Err(anyhow!(
+                "Request body from stdin and --raw cannot be mixed. \
+                Pass --ignore-stdin to ignore standard input."
+            ));
         }
+        let mut buffer = Vec::new();
+        stdin().read_to_end(&mut buffer)?;
+        Body::Raw(buffer)
+    } else if let Some(raw) = args.raw {
+        Body::Raw(raw.into_bytes())
+    } else {
+        args.request_items.body()?
     };
 
     let method = args.method.unwrap_or_else(|| body.pick_method());
-    let timeout = args.timeout.and_then(|t| t.as_duration());
 
     let mut client = Client::builder()
         .http1_title_case_headers()
         .http2_adaptive_window(true)
         .redirect(reqwest::redirect::Policy::none())
-        .timeout(timeout)
+        .timeout(args.timeout.and_then(|t| t.as_duration()))
         .no_gzip()
         .no_deflate()
         .no_brotli();
@@ -420,13 +405,15 @@ fn run(args: Cli) -> Result<i32> {
                     request_builder
                 }
             }
-            Body::Raw(body) => match body_type {
-                BodyType::Json => request_builder
-                    .header(ACCEPT, HeaderValue::from_static(JSON_ACCEPT))
-                    .header(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE)),
-                BodyType::Form => request_builder
-                    .header(CONTENT_TYPE, HeaderValue::from_static(FORM_CONTENT_TYPE)),
-                BodyType::Multipart => unreachable!(),
+            Body::Raw(body) => {
+                if args.form {
+                    request_builder
+                        .header(CONTENT_TYPE, HeaderValue::from_static(FORM_CONTENT_TYPE))
+                } else {
+                    request_builder
+                        .header(ACCEPT, HeaderValue::from_static(JSON_ACCEPT))
+                        .header(CONTENT_TYPE, HeaderValue::from_static(JSON_CONTENT_TYPE))
+                }
             }
             .body(body),
             Body::File {
diff --git a/src/request_items.rs b/src/request_items.rs
index 262ba87..739ca33 100644
--- a/src/request_items.rs
+++ b/src/request_items.rs
@@ -477,8 +477,16 @@ impl RequestItems {
     /// for the benefit of `to_curl`, which sometimes has to process the
     /// request items itself.
     pub fn pick_method(&self) -> Method {
+        if self.is_body_empty() {
+            Method::GET
+        } else {
+            Method::POST
+        }
+    }
+
+    pub fn is_body_empty(&self) -> bool {
         if self.body_type == BodyType::Multipart {
-            return Method::POST;
+            return false;
         }
         for item in &self.items {
             match item {
@@ -491,10 +499,10 @@ impl RequestItems {
                 | RequestItem::DataFieldFromFile { .. }
                 | RequestItem::JsonField(..)
                 | RequestItem::JsonFieldFromFile(..)
-                | RequestItem::FormFile { .. } => return Method::POST,
+                | RequestItem::FormFile { .. } => return false,
             }
         }
-        Method::GET
+        true
     }
 }
 
diff --git a/src/to_curl.rs b/src/to_curl.rs
index 26a5a2e..a58a197 100644
--- a/src/to_curl.rs
+++ b/src/to_curl.rs
@@ -322,7 +322,17 @@ pub fn translate(args: Cli) -> Result<Command> {
         }
     }
 
-    if args.request_items.is_multipart() {
+    if let Some(raw) = args.raw {
+        if args.form {
+            cmd.header("content-type", FORM_CONTENT_TYPE);
+        } else {
+            cmd.header("content-type", JSON_CONTENT_TYPE);
+            cmd.header("accept", JSON_ACCEPT);
+        }
+
+        cmd.opt("-d", "--data");
+        cmd.arg(raw);
+    } else if args.request_items.is_multipart() {
         // We can't use .body() here because we can't look inside the multipart
         // form after construction and we don't want to actually read the files
         for item in args.request_items.items {


Test patch:
diff --git a/tests/cli.rs b/tests/cli.rs
index 0a73b7e..77df571 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -1668,7 +1668,7 @@ fn multipart_raw() {
         .args(["--offline", "--raw=hello", "--multipart", ":"])
         .assert()
         .failure()
-        .stderr(contains("Cannot build a multipart request body from --raw"));
+        .stderr(contains("'--raw <RAW>' cannot be used with '--multipart'"));
 }
 
 #[test]