Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support reading DataField and JsonField value from a file #118

Merged
merged 11 commits into from
Mar 29, 2021
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,12 @@ Run `xh help` for more detailed information.

`xh` uses [HTTPie's request-item syntax](https://httpie.io/docs#request-items) to set headers, request body, query string, etc.

* `=`/`:=` for setting the request body's JSON fields (`=` for strings and `:=` for other JSON types).
* `=`/`:=` for setting the request body's JSON or form fields (`=` for strings and `:=` for other JSON types).
* `==` for adding query strings.
* `@` for including files in multipart requests e.g `[email protected]` or `[email protected];type=image/jpeg`.
* `:` for adding or removing headers e.g `connection:keep-alive` or `connection:`.
* `;` for including headers with empty values e.g `header-without-value;`.
* `=@`/`:=@` for setting the request body's JSON or form fields from a file (`=` for strings and `:=` for other JSON types).

## xh and xhs

Expand Down
47 changes: 41 additions & 6 deletions src/request_items.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{fs::File, io, path::Path, str::FromStr};
use std::{fs::File, io, io::Read, path::Path, str::FromStr};

use anyhow::{anyhow, Result};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
Expand All @@ -17,15 +17,17 @@ pub enum RequestItem {
HttpHeaderToUnset(String),
UrlParam(String, String),
DataField(String, String),
DataFieldFromFile(String, String),
JsonField(String, serde_json::Value),
JsonFieldFromFile(String, String),
FormFile(String, String, Option<String>),
}

impl FromStr for RequestItem {
type Err = clap::Error;
fn from_str(request_item: &str) -> clap::Result<RequestItem> {
const SPECIAL_CHARS: &str = "=@:;\\";
const SEPS: &[&str] = &["==", ":=", "=", "@", ":"];
const SEPS: &[&str] = &["=@", ":=@", "==", ":=", "=", "@", ":"];

fn unescape(text: &str) -> String {
let mut out = String::new();
Expand Down Expand Up @@ -107,6 +109,8 @@ impl FromStr for RequestItem {
}
":" if value.is_empty() => Ok(RequestItem::HttpHeaderToUnset(key)),
":" => Ok(RequestItem::HttpHeader(key, value)),
"=@" => Ok(RequestItem::DataFieldFromFile(key, value)),
":=@" => Ok(RequestItem::JsonFieldFromFile(key, value)),
_ => unreachable!(),
}
} else if let Some(header) = request_item.strip_suffix(';') {
Expand Down Expand Up @@ -145,7 +149,7 @@ impl Body {
// This is a slight divergence from HTTPie, which will simply
// discard stdin if it receives --multipart without request items,
// but that behavior is useless so there's no need to match it
Body::Multipart(_) => false,
Body::Multipart(..) => false,
}
}

Expand Down Expand Up @@ -210,10 +214,17 @@ impl RequestItems {
RequestItem::JsonField(key, value) => {
body.insert(key, value);
}
RequestItem::JsonFieldFromFile(key, value)
| RequestItem::DataFieldFromFile(key, value) => {
let mut file = File::open(value)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
ducaale marked this conversation as resolved.
Show resolved Hide resolved
body.insert(key, serde_json::from_str(&contents)?);
ducaale marked this conversation as resolved.
Show resolved Hide resolved
}
RequestItem::DataField(key, value) => {
body.insert(key, serde_json::Value::String(value));
}
RequestItem::FormFile(_, _, _) => {
RequestItem::FormFile(..) => {
return Err(anyhow!(
"Sending Files is not supported when the request body is in JSON format"
));
Expand All @@ -228,10 +239,16 @@ impl RequestItems {
let mut text_fields = Vec::<(String, String)>::new();
for item in self.0 {
match item {
RequestItem::JsonField(_, _) => {
RequestItem::JsonField(..) | RequestItem::JsonFieldFromFile(..) => {
return Err(anyhow!("JSON values are not supported in Form fields"));
}
RequestItem::DataField(key, value) => text_fields.push((key, value)),
RequestItem::DataFieldFromFile(key, value) => {
let mut file = File::open(value)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
text_fields.push((key, contents));
}
RequestItem::FormFile(..) => unreachable!(),
_ => {}
ducaale marked this conversation as resolved.
Show resolved Hide resolved
}
Expand All @@ -243,12 +260,18 @@ impl RequestItems {
let mut form = multipart::Form::new();
for item in self.0 {
match item {
RequestItem::JsonField(_, _) => {
RequestItem::JsonField(..) | RequestItem::JsonFieldFromFile(..) => {
return Err(anyhow!("JSON values are not supported in multipart fields"));
}
RequestItem::DataField(key, value) => {
form = form.text(key, value);
}
RequestItem::DataFieldFromFile(key, value) => {
let mut file = File::open(value)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
form = form.text(key, contents);
}
RequestItem::FormFile(key, value, file_type) => {
let mut part = file_to_part(&value)?;
if let Some(file_type) = file_type {
Expand Down Expand Up @@ -286,7 +309,9 @@ impl RequestItems {
| RequestItem::HttpHeaderToUnset(..)
| RequestItem::UrlParam(..) => continue,
RequestItem::DataField(..)
| RequestItem::DataFieldFromFile(..)
| RequestItem::JsonField(..)
| RequestItem::JsonFieldFromFile(..)
| RequestItem::FormFile(..) => return Method::POST,
}
}
Expand Down Expand Up @@ -326,6 +351,11 @@ mod tests {

// Data field
assert_eq!(parse("foo=bar"), DataField("foo".into(), "bar".into()));
// Data field from file
assert_eq!(
parse("[email protected]"),
DataFieldFromFile("foo".into(), "data.json".into())
);
// URL param
assert_eq!(parse("foo==bar"), UrlParam("foo".into(), "bar".into()));
// Escaped right before separator
Expand All @@ -334,6 +364,11 @@ mod tests {
assert_eq!(parse("foo:bar"), HttpHeader("foo".into(), "bar".into()));
// JSON field
assert_eq!(parse("foo:=[1,2]"), JsonField("foo".into(), json!([1, 2])));
// JSON field from file
assert_eq!(
parse("foo:[email protected]"),
JsonFieldFromFile("foo".into(), "data.json".into())
);
// Bad JSON field
"foo:=bar".parse::<RequestItem>().unwrap_err();
// Can't escape normal chars
Expand Down