Parse more curl options

This commit is contained in:
Fabrice Reix 2023-04-21 14:42:40 +02:00 committed by hurl-bot
parent d9fd713ab5
commit 3e37931a49
No known key found for this signature in database
GPG Key ID: 1283A2B4A0DCAF8D
3 changed files with 202 additions and 7 deletions

View File

@ -15,7 +15,19 @@
* limitations under the License. * 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 { pub fn headers() -> clap::Arg {
clap::Arg::new("headers") clap::Arg::new("headers")
@ -26,6 +38,29 @@ pub fn headers() -> clap::Arg {
.num_args(1) .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 { pub fn method() -> clap::Arg {
clap::Arg::new("method") clap::Arg::new("method")
.long("request") .long("request")

View File

@ -17,9 +17,28 @@
*/ */
use clap::ArgMatches; use clap::ArgMatches;
pub fn body(arg_matches: &ArgMatches) -> Option<String> {
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 { pub fn method(arg_matches: &ArgMatches) -> String {
match get_string(arg_matches, "method") { 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, Some(v) => v,
} }
} }
@ -34,18 +53,62 @@ pub fn url(arg_matches: &ArgMatches) -> String {
} }
pub fn headers(arg_matches: &ArgMatches) -> Vec<String> { pub fn headers(arg_matches: &ArgMatches) -> Vec<String> {
match get_strings(arg_matches, "headers") { let mut headers = match get_strings(arg_matches, "headers") {
None => vec![], None => vec![],
Some(v) => v, 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<String> { pub fn options(arg_matches: &ArgMatches) -> Vec<String> {
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::<i32>(arg_matches, "max_redirects") {
options.push(format!("max-redirs: {value}"));
}
options
}
fn has_content_type(headers: &Vec<String>) -> 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::<bool>(name) == Some(&true)
}
/// Returns an optional value of type `T` from the command line `matches` given the option `name`.
fn get<T: Clone + Send + Sync + 'static>(matches: &ArgMatches, name: &str) -> Option<T> {
matches.get_one::<T>(name).cloned()
}
fn get_string(matches: &ArgMatches, name: &str) -> Option<String> {
matches.get_one::<String>(name).map(|x| x.to_string()) matches.get_one::<String>(name).map(|x| x.to_string())
} }
/// Returns an optional list of `String` from the command line `matches` given the option `name`. /// Returns an optional list of `String` from the command line `matches` given the option `name`.
pub fn get_strings(matches: &ArgMatches, name: &str) -> Option<Vec<String>> { fn get_strings(matches: &ArgMatches, name: &str) -> Option<Vec<String>> {
matches matches
.get_many::<String>(name) .get_many::<String>(name)
.map(|v| v.map(|x| x.to_string()).collect()) .map(|v| v.map(|x| x.to_string()).collect())

View File

@ -22,7 +22,12 @@ mod matches;
pub fn parse(s: &str) -> Result<String, String> { pub fn parse(s: &str) -> Result<String, String> {
let mut command = clap::Command::new("curl") let mut command = clap::Command::new("curl")
.arg(commands::compressed())
.arg(commands::data())
.arg(commands::headers()) .arg(commands::headers())
.arg(commands::insecure())
.arg(commands::location())
.arg(commands::max_redirects())
.arg(commands::method()) .arg(commands::method())
.arg(commands::url()); .arg(commands::url());
@ -35,15 +40,32 @@ pub fn parse(s: &str) -> Result<String, String> {
let method = matches::method(&arg_matches); let method = matches::method(&arg_matches);
let url = matches::url(&arg_matches); let url = matches::url(&arg_matches);
let headers = matches::headers(&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) Ok(s)
} }
fn format(method: &str, url: &str, headers: Vec<String>) -> String { fn format(
method: &str,
url: &str,
headers: Vec<String>,
options: Vec<String>,
body: Option<String>,
) -> String {
let mut s = format!("{method} {url}"); let mut s = format!("{method} {url}");
for header in headers { for header in headers {
s.push_str(format!("\n{header}").as_str()); 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.push('\n');
s s
} }
@ -75,4 +97,79 @@ Test: '
hurl_str 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&param2=value2```
"#;
assert_eq!(
parse("curl http://localhost:3000/data -d 'param1=value1&param2=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&param2=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
);
}
} }