From 3e37931a493435422618204b461972a5f705fe7a Mon Sep 17 00:00:00 2001 From: Fabrice Reix Date: Fri, 21 Apr 2023 14:42:40 +0200 Subject: [PATCH] Parse more curl options --- packages/hurlfmt/src/curl/commands.rs | 37 +++++++++- packages/hurlfmt/src/curl/matches.rs | 71 +++++++++++++++++- packages/hurlfmt/src/curl/mod.rs | 101 +++++++++++++++++++++++++- 3 files changed, 202 insertions(+), 7 deletions(-) diff --git a/packages/hurlfmt/src/curl/commands.rs b/packages/hurlfmt/src/curl/commands.rs index fb1f6d75e..2c07ec8b4 100644 --- a/packages/hurlfmt/src/curl/commands.rs +++ b/packages/hurlfmt/src/curl/commands.rs @@ -15,7 +15,19 @@ * limitations under the License. * */ -use clap::ArgAction; +use clap::{value_parser, ArgAction}; + +pub fn compressed() -> clap::Arg { + clap::Arg::new("compressed").long("compressed").num_args(0) +} + +pub fn data() -> clap::Arg { + clap::Arg::new("data") + .long("data") + .short('d') + .value_name("data") + .num_args(1) +} pub fn headers() -> clap::Arg { clap::Arg::new("headers") @@ -26,6 +38,29 @@ pub fn headers() -> clap::Arg { .num_args(1) } +pub fn insecure() -> clap::Arg { + clap::Arg::new("insecure") + .long("insecure") + .short('k') + .num_args(0) +} + +pub fn location() -> clap::Arg { + clap::Arg::new("location") + .long("location") + .short('L') + .num_args(0) +} + +pub fn max_redirects() -> clap::Arg { + clap::Arg::new("max_redirects") + .long("max-redirs") + .value_name("NUM") + .allow_hyphen_values(true) + .value_parser(value_parser!(i32).range(-1..)) + .num_args(1) +} + pub fn method() -> clap::Arg { clap::Arg::new("method") .long("request") diff --git a/packages/hurlfmt/src/curl/matches.rs b/packages/hurlfmt/src/curl/matches.rs index 0a93f27f7..8afdd45d2 100644 --- a/packages/hurlfmt/src/curl/matches.rs +++ b/packages/hurlfmt/src/curl/matches.rs @@ -17,9 +17,28 @@ */ use clap::ArgMatches; +pub fn body(arg_matches: &ArgMatches) -> Option { + match get_string(arg_matches, "data") { + None => None, + Some(v) => { + if let Some(filename) = v.strip_prefix('@') { + Some(format!("file, {filename};")) + } else { + Some(format!("```{v}```")) + } + } + } +} + pub fn method(arg_matches: &ArgMatches) -> String { match get_string(arg_matches, "method") { - None => "GET".to_string(), + None => { + if arg_matches.contains_id("data") { + "POST".to_string() + } else { + "GET".to_string() + } + } Some(v) => v, } } @@ -34,18 +53,62 @@ pub fn url(arg_matches: &ArgMatches) -> String { } pub fn headers(arg_matches: &ArgMatches) -> Vec { - match get_strings(arg_matches, "headers") { + let mut headers = match get_strings(arg_matches, "headers") { None => vec![], Some(v) => v, + }; + if !has_content_type(&headers) { + if let Some(data) = get_string(arg_matches, "data") { + if !data.starts_with('@') { + headers.push("Content-Type: application/x-www-form-urlencoded".to_string()) + } + } } + + headers } -pub fn get_string(matches: &ArgMatches, name: &str) -> Option { +pub fn options(arg_matches: &ArgMatches) -> Vec { + let mut options = vec![]; + if has_flag(arg_matches, "compressed") { + options.push("compressed: true".to_string()); + } + if has_flag(arg_matches, "location") { + options.push("location: true".to_string()); + } + if has_flag(arg_matches, "insecure") { + options.push("insecure: true".to_string()); + } + if let Some(value) = get::(arg_matches, "max_redirects") { + options.push(format!("max-redirs: {value}")); + } + options +} + +fn has_content_type(headers: &Vec) -> bool { + for header in headers { + if header.starts_with("Content-Type") { + return true; + } + } + false +} + +fn has_flag(matches: &ArgMatches, name: &str) -> bool { + matches.get_one::(name) == Some(&true) +} + +/// Returns an optional value of type `T` from the command line `matches` given the option `name`. +fn get(matches: &ArgMatches, name: &str) -> Option { + matches.get_one::(name).cloned() +} + +fn get_string(matches: &ArgMatches, name: &str) -> Option { matches.get_one::(name).map(|x| x.to_string()) } /// Returns an optional list of `String` from the command line `matches` given the option `name`. -pub fn get_strings(matches: &ArgMatches, name: &str) -> Option> { +fn get_strings(matches: &ArgMatches, name: &str) -> Option> { matches .get_many::(name) .map(|v| v.map(|x| x.to_string()).collect()) diff --git a/packages/hurlfmt/src/curl/mod.rs b/packages/hurlfmt/src/curl/mod.rs index fcc69db51..d68898774 100644 --- a/packages/hurlfmt/src/curl/mod.rs +++ b/packages/hurlfmt/src/curl/mod.rs @@ -22,7 +22,12 @@ mod matches; pub fn parse(s: &str) -> Result { let mut command = clap::Command::new("curl") + .arg(commands::compressed()) + .arg(commands::data()) .arg(commands::headers()) + .arg(commands::insecure()) + .arg(commands::location()) + .arg(commands::max_redirects()) .arg(commands::method()) .arg(commands::url()); @@ -35,15 +40,32 @@ pub fn parse(s: &str) -> Result { let method = matches::method(&arg_matches); let url = matches::url(&arg_matches); let headers = matches::headers(&arg_matches); - let s = format(&method, &url, headers); + let options = matches::options(&arg_matches); + let body = matches::body(&arg_matches); + let s = format(&method, &url, headers, options, body); Ok(s) } -fn format(method: &str, url: &str, headers: Vec) -> String { +fn format( + method: &str, + url: &str, + headers: Vec, + options: Vec, + body: Option, +) -> String { let mut s = format!("{method} {url}"); for header in headers { s.push_str(format!("\n{header}").as_str()); } + if !options.is_empty() { + s.push_str("\n[Options]"); + for option in options { + s.push_str(format!("\n{option}").as_str()); + } + } + if let Some(body) = body { + s.push_str(format!("\n{body}").as_str()); + } s.push('\n'); s } @@ -75,4 +97,79 @@ Test: ' hurl_str ); } + + #[test] + fn test_post_format_params() { + let hurl_str = r#"POST http://localhost:3000/data +Content-Type: application/x-www-form-urlencoded +```param1=value1¶m2=value2``` +"#; + assert_eq!( + parse("curl http://localhost:3000/data -d 'param1=value1¶m2=value2'").unwrap(), + hurl_str + ); + assert_eq!( + parse("curl -X POST http://localhost:3000/data -H 'Content-Type: application/x-www-form-urlencoded' --data 'param1=value1¶m2=value2'").unwrap(), + hurl_str + ); + } + + #[test] + fn test_post_json() { + let hurl_str = r#"POST http://localhost:3000/data +Content-Type: application/json +```{"key1":"value1", "key2":"value2"}``` +"#; + assert_eq!( + parse(r#"curl -d '{"key1":"value1", "key2":"value2"}' -H 'Content-Type: application/json' -X POST http://localhost:3000/data"#).unwrap(), + hurl_str + ); + } + + #[test] + fn test_post_file() { + let hurl_str = r#"POST http://example.com/ +file, filename; +"#; + assert_eq!( + parse(r#"curl --data @filename http://example.com/"#).unwrap(), + hurl_str + ); + } + + #[test] + fn test_redirect() { + let hurl_str = r#"GET http://localhost:8000/redirect-absolute +[Options] +location: true +"#; + assert_eq!( + parse(r#"curl -L http://localhost:8000/redirect-absolute"#).unwrap(), + hurl_str + ); + } + + #[test] + fn test_insecure() { + let hurl_str = r#"GET https://localhost:8001/hello +[Options] +insecure: true +"#; + assert_eq!( + parse(r#"curl -k https://localhost:8001/hello"#).unwrap(), + hurl_str + ); + } + + #[test] + fn test_max_redirects() { + let hurl_str = r#"GET https://localhost:8001/hello +[Options] +max-redirs: 10 +"#; + assert_eq!( + parse(r#"curl https://localhost:8001/hello --max-redirs 10"#).unwrap(), + hurl_str + ); + } }