diff --git a/integration/tests_ok/import_curl.in b/integration/tests_ok/import_curl.in new file mode 100644 index 00000000000..5e0b6848cb1 --- /dev/null +++ b/integration/tests_ok/import_curl.in @@ -0,0 +1,3 @@ +curl http://localhost:8000/hello +curl http://localhost:8000/custom-headers -H 'Fruit:Raspberry' -H 'Fruit:Apple' -H 'Fruit:Banana' -H 'Fruit: Grape' -H 'Color:Green' +curl --header 'Content-Type: application/json' --data $'{\n "name": "Bob",\n "password": "&secret\\\\\'<>",\n "age": 30,\n "strict": true,\n "spacing": "\\n",\n "g_clef": "\\uD834\\uDD1E",\n "items": [true, "true", 1],\n "variable": "\\\\"\n}' 'http://localhost:8000/post-json' diff --git a/integration/tests_ok/import_curl.out b/integration/tests_ok/import_curl.out new file mode 100644 index 00000000000..de3f7743dd0 --- /dev/null +++ b/integration/tests_ok/import_curl.out @@ -0,0 +1,24 @@ +GET http://localhost:8000/hello + +GET http://localhost:8000/custom-headers +Fruit: Raspberry +Fruit: Apple +Fruit: Banana +Fruit: Grape +Color: Green + +POST http://localhost:8000/post-json +Content-Type: application/json +``` +{ + "name": "Bob", + "password": "&secret\\'<>", + "age": 30, + "strict": true, + "spacing": "\n", + "g_clef": "\uD834\uDD1E", + "items": [true, "true", 1], + "variable": "\\" +} +``` + diff --git a/integration/tests_ok/import_curl.ps1 b/integration/tests_ok/import_curl.ps1 new file mode 100755 index 00000000000..4095ff186bb --- /dev/null +++ b/integration/tests_ok/import_curl.ps1 @@ -0,0 +1,3 @@ +Set-StrictMode -Version latest +$ErrorActionPreference = 'Stop' +hurlfmt --in curl tests_ok/import_curl.in diff --git a/integration/tests_ok/import_curl.sh b/integration/tests_ok/import_curl.sh new file mode 100755 index 00000000000..977f697b2d3 --- /dev/null +++ b/integration/tests_ok/import_curl.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -Eeuo pipefail + +hurl tests_ok/import_curl.out >/dev/null +hurlfmt --in curl tests_ok/import_curl.in diff --git a/packages/hurlfmt/src/cli/options/matches.rs b/packages/hurlfmt/src/cli/options/matches.rs index 01cb133a80a..e49b94d9e2e 100644 --- a/packages/hurlfmt/src/cli/options/matches.rs +++ b/packages/hurlfmt/src/cli/options/matches.rs @@ -39,6 +39,7 @@ pub fn color(arg_matches: &ArgMatches) -> bool { pub fn input_format(arg_matches: &ArgMatches) -> Result { match get_string(arg_matches, "input_format").unwrap().as_str() { "hurl" => Ok(InputFormat::Hurl), + "curl" => Ok(InputFormat::Curl), v => Err(OptionsError::Error(format!("Invalid input format {v}"))), } } diff --git a/packages/hurlfmt/src/cli/options/mod.rs b/packages/hurlfmt/src/cli/options/mod.rs index 453b03f2f25..5e05f05b6cc 100644 --- a/packages/hurlfmt/src/cli/options/mod.rs +++ b/packages/hurlfmt/src/cli/options/mod.rs @@ -38,6 +38,7 @@ pub struct Options { #[derive(Clone, Debug, PartialEq, Eq)] pub enum InputFormat { + Curl, Hurl, } diff --git a/packages/hurlfmt/src/curl/args.rs b/packages/hurlfmt/src/curl/args.rs index 70f2b507631..c7406ea64e7 100644 --- a/packages/hurlfmt/src/curl/args.rs +++ b/packages/hurlfmt/src/curl/args.rs @@ -94,7 +94,7 @@ impl Parser { Some('t') => '\t', Some('r') => '\r', Some(c) => c, - _ => return Err(format!("Invalid escape at index {}", self.index)), + _ => return Err(format!("Invalid escape at column {}", self.index + 1)), }; value.push(c2); } else if c1 == delimiter { @@ -104,8 +104,8 @@ impl Parser { } } Err(format!( - "Missing delimiter {delimiter} at index {}", - self.index + "Missing delimiter {delimiter} at column {}", + self.index + 1 )) } else { loop { @@ -114,7 +114,7 @@ impl Parser { if let Some(c) = self.read() { value.push(c); } else { - return Err(format!("Invalid escape at index {}", self.index)); + return Err(format!("Invalid escape at column {}", self.index + 1)); } } Some(' ') => return Ok(Some(value)), @@ -150,7 +150,7 @@ mod test { fn test_split_error() { assert_eq!( args::split(r#"AAA 'BBB"#).err().unwrap(), - "Missing delimiter ' at index 8".to_string() + "Missing delimiter ' at column 9".to_string() ); } @@ -196,7 +196,7 @@ mod test { let mut parser = Parser::new("'value"); assert_eq!( parser.param().err().unwrap(), - "Missing delimiter ' at index 6".to_string() + "Missing delimiter ' at column 7".to_string() ); assert_eq!(parser.index, 6); } diff --git a/packages/hurlfmt/src/curl/matches.rs b/packages/hurlfmt/src/curl/matches.rs index 8afdd45d2cf..88937267fae 100644 --- a/packages/hurlfmt/src/curl/matches.rs +++ b/packages/hurlfmt/src/curl/matches.rs @@ -24,7 +24,7 @@ pub fn body(arg_matches: &ArgMatches) -> Option { if let Some(filename) = v.strip_prefix('@') { Some(format!("file, {filename};")) } else { - Some(format!("```{v}```")) + Some(format!("```\n{v}\n```")) } } } diff --git a/packages/hurlfmt/src/curl/mod.rs b/packages/hurlfmt/src/curl/mod.rs index fb8d7b1a88b..b224f32ba23 100644 --- a/packages/hurlfmt/src/curl/mod.rs +++ b/packages/hurlfmt/src/curl/mod.rs @@ -21,6 +21,22 @@ mod commands; mod matches; pub fn parse(s: &str) -> Result { + let lines: Vec<&str> = regex::Regex::new(r"\n|\r\n") + .unwrap() + .split(s) + .filter(|s| !s.is_empty()) + .collect(); + let mut s = "".to_string(); + for (i, line) in lines.iter().enumerate() { + let hurl_str = parse_line(line).map_err(|message| { + format!("Can not parse curl command at line {}: {message}", i + 1) + })?; + s.push_str(format!("{hurl_str}\n").as_str()) + } + Ok(s) +} + +fn parse_line(s: &str) -> Result { let mut command = clap::Command::new("curl") .arg(commands::compressed()) .arg(commands::data()) @@ -64,7 +80,8 @@ fn format( } } if let Some(body) = body { - s.push_str(format!("\n{body}").as_str()); + s.push('\n'); + s.push_str(body.as_str()); } s.push('\n'); s @@ -72,13 +89,35 @@ fn format( #[cfg(test)] mod test { - use crate::curl::parse; + use crate::curl::*; + + #[test] + fn test_parse() { + let hurl_str = r#"GET http://localhost:8000/hello + +GET http://localhost:8000/custom-headers +Fruit:Raspberry + +"#; + assert_eq!( + parse( + r#"curl http://localhost:8000/hello +curl http://localhost:8000/custom-headers -H 'Fruit:Raspberry' +"# + ) + .unwrap(), + hurl_str + ); + } #[test] fn test_hello() { - let hurl_str = r#"GET http://locahost:8000/hello + let hurl_str = r#"GET http://localhost:8000/hello "#; - assert_eq!(parse("curl http://locahost:8000/hello").unwrap(), hurl_str); + assert_eq!( + parse_line("curl http://localhost:8000/hello").unwrap(), + hurl_str + ); } #[test] @@ -89,11 +128,25 @@ Fruit: Banana Test: ' "#; assert_eq!( - parse("curl http://localhost:8000/custom-headers -H 'Fruit:Raspberry' -H 'Fruit: Banana' -H $'Test: \\''").unwrap(), + parse_line("curl http://localhost:8000/custom-headers -H 'Fruit:Raspberry' -H 'Fruit: Banana' -H $'Test: \\''").unwrap(), hurl_str ); assert_eq!( - parse("curl http://localhost:8000/custom-headers --header Fruit:Raspberry -H 'Fruit: Banana' -H $'Test: \\'' ").unwrap(), + parse_line("curl http://localhost:8000/custom-headers --header Fruit:Raspberry -H 'Fruit: Banana' -H $'Test: \\'' ").unwrap(), + hurl_str + ); + } + + #[test] + fn test_post_hello() { + let hurl_str = r#"POST http://localhost:8000/hello +Content-Type: text/plain +``` +hello +``` +"#; + assert_eq!( + parse_line(r#"curl -d $'hello' -H 'Content-Type: text/plain' -X POST http://localhost:8000/hello"#).unwrap(), hurl_str ); } @@ -102,14 +155,16 @@ 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``` +``` +param1=value1¶m2=value2 +``` "#; assert_eq!( - parse("curl http://localhost:3000/data -d 'param1=value1¶m2=value2'").unwrap(), + parse_line("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(), + parse_line("curl -X POST http://localhost:3000/data -H 'Content-Type: application/x-www-form-urlencoded' --data 'param1=value1¶m2=value2'").unwrap(), hurl_str ); } @@ -118,23 +173,26 @@ Content-Type: application/x-www-form-urlencoded fn test_post_json() { let hurl_str = r#"POST http://localhost:3000/data Content-Type: application/json -```{"key1":"value1", "key2":"value2"}``` +``` +{"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 - ); + hurl_str, + parse_line(r#"curl -d '{"key1":"value1", "key2":"value2"}' -H 'Content-Type: application/json' -X POST http://localhost:3000/data"#).unwrap() + ); let hurl_str = r#"POST http://localhost:3000/data Content-Type: application/json -```{ +``` +{ "key1": "value1", "key2": "value2" } ``` "#; assert_eq!( - parse(r#"curl -d $'{\n "key1": "value1",\n "key2": "value2"\n}\n' -H 'Content-Type: application/json' -X POST http://localhost:3000/data"#).unwrap(), + parse_line(r#"curl -d $'{\n "key1": "value1",\n "key2": "value2"\n}' -H 'Content-Type: application/json' -X POST http://localhost:3000/data"#).unwrap(), hurl_str ); } @@ -145,7 +203,7 @@ Content-Type: application/json file, filename; "#; assert_eq!( - parse(r#"curl --data @filename http://example.com/"#).unwrap(), + parse_line(r#"curl --data @filename http://example.com/"#).unwrap(), hurl_str ); } @@ -157,7 +215,7 @@ file, filename; location: true "#; assert_eq!( - parse(r#"curl -L http://localhost:8000/redirect-absolute"#).unwrap(), + parse_line(r#"curl -L http://localhost:8000/redirect-absolute"#).unwrap(), hurl_str ); } @@ -169,7 +227,7 @@ location: true insecure: true "#; assert_eq!( - parse(r#"curl -k https://localhost:8001/hello"#).unwrap(), + parse_line(r#"curl -k https://localhost:8001/hello"#).unwrap(), hurl_str ); } @@ -181,7 +239,7 @@ insecure: true max-redirs: 10 "#; assert_eq!( - parse(r#"curl https://localhost:8001/hello --max-redirs 10"#).unwrap(), + parse_line(r#"curl https://localhost:8001/hello --max-redirs 10"#).unwrap(), hurl_str ); } diff --git a/packages/hurlfmt/src/main.rs b/packages/hurlfmt/src/main.rs index d3a973e742c..3b1c6116845 100644 --- a/packages/hurlfmt/src/main.rs +++ b/packages/hurlfmt/src/main.rs @@ -20,8 +20,8 @@ use std::path::PathBuf; use std::process; use hurl_core::parser; -use hurlfmt::cli::options::{OptionsError, OutputFormat}; -use hurlfmt::{cli, format, linter}; +use hurlfmt::cli::options::{InputFormat, OptionsError, OutputFormat}; +use hurlfmt::{cli, curl, format, linter}; #[cfg(target_family = "unix")] pub fn init_colored() { @@ -80,17 +80,28 @@ fn main() { contents }; + let input = match opts.input_format { + InputFormat::Hurl => contents, + InputFormat::Curl => match curl::parse(&contents) { + Ok(s) => s, + Err(e) => { + eprintln!("{}", e); + process::exit(2); + } + }, + }; + let lines: Vec<&str> = regex::Regex::new(r"\n|\r\n") .unwrap() - .split(&contents) + .split(&input) .collect(); - let lines: Vec = lines.iter().map(|s| (*s).to_string()).collect(); let log_parser_error = cli::make_logger_parser_error(lines.clone(), opts.color, opts.input_file.clone()); let log_linter_error = cli::make_logger_linter_error(lines, opts.color, opts.input_file.clone()); - match parser::parse_hurl_file(&contents) { + + match parser::parse_hurl_file(&input) { Err(e) => { log_parser_error(&e, false); process::exit(2);