Add module curl to parse curl command-line

This commit is contained in:
Fabrice Reix 2023-04-20 09:44:57 +02:00
parent d88f79c468
commit 31d4783366
No known key found for this signature in database
GPG Key ID: BF5213154B2E7155
5 changed files with 388 additions and 0 deletions

View File

@ -0,0 +1,215 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2023 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/// Split a `str` into a vec of String params
pub fn split(s: &str) -> Result<Vec<String>, String> {
let mut params = vec![];
let mut parser = Parser::new(s);
while let Some(param) = parser.param()? {
params.push(param);
}
Ok(params)
}
struct Parser {
pub buffer: Vec<char>,
pub index: usize,
}
impl Parser {
fn new(s: &str) -> Parser {
let buffer = s.chars().collect();
let index = 0;
Parser { buffer, index }
}
fn skip_spaces(&mut self) {
while self.peek() == Some(' ') {
self.read();
}
}
fn read(&mut self) -> Option<char> {
match self.buffer.get(self.index) {
None => None,
Some(c) => {
self.index += 1;
Some(*c)
}
}
}
fn peek(&mut self) -> Option<char> {
self.buffer.get(self.index).copied()
}
fn end_of_string(&self) -> bool {
self.index == self.buffer.len()
}
fn delimiter(&mut self) -> Option<char> {
if self.peek() == Some('\'') {
self.read();
Some('\'')
} else if self.peek() == Some('$') {
let save = self.index;
self.read();
if self.peek() == Some('\'') {
self.read();
Some('\'')
} else {
self.index = save;
None
}
} else {
None
}
}
fn param(&mut self) -> Result<Option<String>, String> {
self.skip_spaces();
if self.end_of_string() {
return Ok(None);
}
let mut value = "".to_string();
if let Some(delimiter) = self.delimiter() {
while let Some(c) = self.read() {
if c == '\\' {
if let Some(c) = self.read() {
value.push(c);
} else {
return Err(format!("Invalid escape at index {}", self.index));
}
} else if c == delimiter {
return Ok(Some(value));
} else {
value.push(c);
}
}
Err(format!(
"Missing delimiter {delimiter} at index {}",
self.index
))
} else {
loop {
match self.read() {
Some('\\') => {
if let Some(c) = self.read() {
value.push(c);
} else {
return Err(format!("Invalid escape at index {}", self.index));
}
}
Some(' ') => return Ok(Some(value)),
Some(c) => {
value.push(c);
}
_ => return Ok(Some(value)),
}
}
}
}
}
#[cfg(test)]
mod test {
use crate::curl::args;
use crate::curl::args::Parser;
#[test]
fn test_split() {
let expected = vec!["AAA".to_string(), "BBB".to_string()];
assert_eq!(args::split(r#"AAA BBB"#).unwrap(), expected);
assert_eq!(args::split(r#"AAA BBB"#).unwrap(), expected);
assert_eq!(args::split(r#" AAA BBB "#).unwrap(), expected);
assert_eq!(args::split(r#"AAA 'BBB'"#).unwrap(), expected);
assert_eq!(args::split(r#"AAA $'BBB'"#).unwrap(), expected);
let expected = vec!["'".to_string()];
assert_eq!(args::split(r#"$'\''"#).unwrap(), expected);
}
#[test]
fn test_split_error() {
assert_eq!(
args::split(r#"AAA 'BBB"#).err().unwrap(),
"Missing delimiter ' at index 8".to_string()
);
}
#[test]
fn test_param_without_quote() {
let mut parser = Parser::new("value");
assert_eq!(parser.param().unwrap().unwrap(), "value".to_string());
assert_eq!(parser.index, 5);
let mut parser = Parser::new(" value ");
assert_eq!(parser.param().unwrap().unwrap(), "value".to_string());
assert_eq!(parser.index, 7);
}
#[test]
fn test_param_with_quote() {
let mut parser = Parser::new("'value'");
assert_eq!(parser.param().unwrap().unwrap(), "value".to_string());
assert_eq!(parser.index, 7);
let mut parser = Parser::new(" 'value' ");
assert_eq!(parser.param().unwrap().unwrap(), "value".to_string());
assert_eq!(parser.index, 8);
}
#[test]
fn test_dollar_prefix() {
let mut parser = Parser::new("$'Test: \\''");
assert_eq!(parser.param().unwrap().unwrap(), "Test: '".to_string());
assert_eq!(parser.index, 11);
}
#[test]
fn test_param_missing_closing_quote() {
let mut parser = Parser::new("'value");
assert_eq!(
parser.param().err().unwrap(),
"Missing delimiter ' at index 6".to_string()
);
assert_eq!(parser.index, 6);
}
#[test]
fn test_no_more_param() {
assert_eq!(Parser::new("").param().unwrap(), None);
assert_eq!(Parser::new(" ").param().unwrap(), None);
}
#[test]
fn test_delimiter() {
let mut parser = Parser::new("value");
assert_eq!(parser.delimiter(), None);
assert_eq!(parser.index, 0);
let mut parser = Parser::new("'value'");
assert_eq!(parser.delimiter().unwrap(), '\'');
assert_eq!(parser.index, 1);
let mut parser = Parser::new("$'value'");
assert_eq!(parser.delimiter().unwrap(), '\'');
assert_eq!(parser.index, 2);
let mut parser = Parser::new("$value");
assert_eq!(parser.delimiter(), None);
assert_eq!(parser.index, 0);
}
}

View File

@ -0,0 +1,42 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2023 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use clap::ArgAction;
pub fn headers() -> clap::Arg {
clap::Arg::new("headers")
.long("header")
.short('H')
.value_name("NAME:VALUE")
.action(ArgAction::Append)
.num_args(1)
}
pub fn method() -> clap::Arg {
clap::Arg::new("method")
.long("request")
.short('X')
.value_name("METHOD")
.num_args(1)
}
pub fn url() -> clap::Arg {
clap::Arg::new("url")
.help("Sets the url to use")
.required(false)
.num_args(1)
}

View File

@ -0,0 +1,52 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2023 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use clap::ArgMatches;
pub fn method(arg_matches: &ArgMatches) -> String {
match get_string(arg_matches, "method") {
None => "GET".to_string(),
Some(v) => v,
}
}
pub fn url(arg_matches: &ArgMatches) -> String {
let s = get_string(arg_matches, "url").unwrap();
if !s.starts_with("http") {
format!("https://{s}")
} else {
s
}
}
pub fn headers(arg_matches: &ArgMatches) -> Vec<String> {
match get_strings(arg_matches, "headers") {
None => vec![],
Some(v) => v,
}
}
pub fn get_string(matches: &ArgMatches, name: &str) -> Option<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`.
pub fn get_strings(matches: &ArgMatches, name: &str) -> Option<Vec<String>> {
matches
.get_many::<String>(name)
.map(|v| v.map(|x| x.to_string()).collect())
}

View File

@ -0,0 +1,78 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2023 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
mod args;
mod commands;
mod matches;
pub fn parse(s: &str) -> Result<String, String> {
let mut command = clap::Command::new("curl")
.arg(commands::headers())
.arg(commands::method())
.arg(commands::url());
let params = args::split(s)?;
let arg_matches = match command.try_get_matches_from_mut(params) {
Ok(r) => r,
Err(e) => return Err(e.to_string()),
};
let method = matches::method(&arg_matches);
let url = matches::url(&arg_matches);
let headers = matches::headers(&arg_matches);
let s = format(&method, &url, headers);
Ok(s)
}
fn format(method: &str, url: &str, headers: Vec<String>) -> String {
let mut s = format!("{method} {url}");
for header in headers {
s.push_str(format!("\n{header}").as_str());
}
s.push('\n');
s
}
#[cfg(test)]
mod test {
use crate::curl::parse;
#[test]
fn test_hello() {
let hurl_str = r#"GET http://locahost:8000/hello
"#;
assert_eq!(parse("curl http://locahost:8000/hello").unwrap(), hurl_str);
}
#[test]
fn test_headers() {
let hurl_str = r#"GET http://localhost:8000/custom-headers
Fruit:Raspberry
Fruit: Banana
Test: '
"#;
assert_eq!(
parse("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(),
hurl_str
);
}
}

View File

@ -18,5 +18,6 @@
#![cfg_attr(feature = "strict", deny(warnings))]
pub mod cli;
pub mod curl;
pub mod format;
pub mod linter;