mod primitives;
use std::path::Path;
use hurl_core::reader::{CharPos, Pos, Reader};
use crate::cli::options::config_file::primitives::{
expect_no_value, parse_value, parse_value_separator,
};
use super::{CliOptions, CliOptionsError, Verbosity};
use hurl_core::types::Count;
use primitives::skip_whitespace_and_comments;
#[derive(Debug, PartialEq, Eq)]
struct ConfigFileError {
pos: Pos,
message: String,
}
impl ConfigFileError {
fn new(pos: Pos, message: &str) -> Self {
ConfigFileError {
pos,
message: message.to_string(),
}
}
fn pos(&self) -> Pos {
self.pos
}
}
impl std::fmt::Display for ConfigFileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}: {}", self.pos.line, self.pos.column, self.message)
}
}
pub fn parse_config_file(
config_file_path: Option<&Path>,
default_options: CliOptions,
) -> Result<CliOptions, CliOptionsError> {
if let Some(config_file_path) = config_file_path
&& config_file_path.exists()
{
let content = std::fs::read_to_string(config_file_path).map_err(|e| {
CliOptionsError::Error(format!(
"Failed to read config file {}: {}",
config_file_path.display(),
e
))
})?;
return parse_config(&content, default_options).map_err(|e| {
CliOptionsError::Error(format!(
"{}:{}:{}: {}",
config_file_path.display(),
e.pos().line,
e.pos().column,
e.message
))
});
}
Ok(default_options)
}
fn parse_config(content: &str, default_options: CliOptions) -> Result<CliOptions, ConfigFileError> {
let mut options = default_options;
let mut reader = Reader::new(content);
while !reader.is_eof() {
parse_option(&mut reader, &mut options)?;
}
Ok(options)
}
fn parse_option(reader: &mut Reader, options: &mut CliOptions) -> Result<(), ConfigFileError> {
skip_whitespace_and_comments(reader);
if reader.is_eof() {
return Ok(());
}
let mut save = reader.cursor();
if reader.read_n(CharPos(2)) != "--" {
return Err(ConfigFileError::new(
save.pos,
"Expecting an option starting with --",
));
}
let option_name = reader.read_while(|c: char| c.is_alphanumeric() || c == '-' || c == '_');
match option_name.as_str() {
"verbose" => {
expect_no_value(reader)?;
options.verbosity = Some(Verbosity::Verbose);
Ok(())
}
"header" => {
parse_value_separator(reader)?;
let value = parse_value(reader)?;
if value.is_empty() {
return Err(ConfigFileError::new(
save.pos,
"Option --header requires a value",
));
}
options.headers.push(value);
Ok(())
}
"max-redirs" => {
parse_value_separator(reader)?;
save = reader.cursor();
let value = parse_value(reader)?;
let max_redirs = value.parse::<i32>().map_err(|_| {
ConfigFileError::new(save.pos, "Option --max-redirs requires an integer value")
})?;
options.max_redirect = Count::try_from(max_redirs).map_err(|_| {
ConfigFileError::new(
save.pos,
"Option --max-redirs requires an integer value >= -1",
)
})?;
Ok(())
}
"user-agent" => {
parse_value_separator(reader)?;
let value = parse_value(reader)?;
options.user_agent = Some(value);
Ok(())
}
_ => Err(ConfigFileError::new(
save.pos,
&format!("Unknown option <--{}>", option_name),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::options::Verbosity;
use hurl_core::reader::Pos;
#[test]
fn test_parse_config() {
let content = "# ignore\n\n--verbose\n";
let options = parse_config(content, CliOptions::default()).unwrap();
assert!(!options.color_stdout);
assert!(!options.color_stderr);
assert_eq!(options.verbosity, Some(Verbosity::Verbose));
}
#[test]
fn test_parse_option_flag() {
let mut reader = Reader::new("\n\n--verbose\n");
let mut options = CliOptions::default();
assert_eq!(reader.cursor().pos, Pos::new(1, 1));
assert!(parse_option(&mut reader, &mut options).is_ok());
assert_eq!(options.verbosity, Some(Verbosity::Verbose));
assert_eq!(reader.cursor().pos, Pos::new(4, 1));
}
#[test]
fn test_parse_no_option() {
let mut reader = Reader::new("# ignore\n\n");
let mut options = CliOptions::default();
assert!(parse_option(&mut reader, &mut options).is_ok());
assert_eq!(reader.cursor().pos, Pos::new(3, 1));
}
#[test]
fn test_parse_option_with_value() {
let mut reader = Reader::new("--header=header1:value1\n--verbose\n");
let mut options = CliOptions::default();
assert!(parse_option(&mut reader, &mut options).is_ok());
assert_eq!(options.headers, vec!["header1:value1"]);
assert_eq!(reader.cursor().pos, Pos::new(1, 24));
let mut reader = Reader::new("--header header2:value2\n--verbose\n");
let mut options = CliOptions::default();
assert!(parse_option(&mut reader, &mut options).is_ok());
assert_eq!(options.headers, vec!["header2:value2"]);
assert_eq!(reader.cursor().pos, Pos::new(1, 24));
let mut reader = Reader::new("--header --test:1\n");
let mut options = CliOptions::default();
assert!(parse_option(&mut reader, &mut options).is_ok());
assert_eq!(options.headers, vec!["--test:1"]);
assert_eq!(reader.cursor().pos, Pos::new(1, 18));
}
#[test]
fn test_parse_option_with_value_with_quotes() {
let mut reader = Reader::new("--user-agent=\"Mozilla/5.0 A\"");
let mut options = CliOptions::default();
assert!(parse_option(&mut reader, &mut options).is_ok());
assert_eq!(options.user_agent.unwrap(), "Mozilla/5.0 A".to_string());
assert_eq!(reader.cursor().pos, Pos::new(1, 29));
}
#[test]
fn test_parse_option_error() {
let mut reader = Reader::new("verbose\n");
let mut options = CliOptions::default();
let err = parse_option(&mut reader, &mut options).unwrap_err();
assert_eq!(err.pos, Pos::new(1, 1));
assert_eq!(err.message, "Expecting an option starting with --");
}
#[test]
fn test_parse_option_error_unknown() {
let mut reader = Reader::new("--xxx");
let mut options = CliOptions::default();
let err = parse_option(&mut reader, &mut options).unwrap_err();
assert_eq!(err.pos, Pos::new(1, 1));
assert_eq!(err.message, "Unknown option <--xxx>");
let mut reader = Reader::new("--verbosexxx");
let mut options = CliOptions::default();
let err = parse_option(&mut reader, &mut options).unwrap_err();
assert_eq!(err.pos, Pos::new(1, 1));
assert_eq!(err.message, "Unknown option <--verbosexxx>");
}
#[test]
fn test_parse_option_error_missing_value() {
let mut reader = Reader::new("--header\n");
let mut options = CliOptions::default();
let err = parse_option(&mut reader, &mut options).unwrap_err();
assert_eq!(err.pos, Pos::new(1, 9));
assert_eq!(
err.message,
"Expecting a value using space or '=' separator"
);
}
#[test]
fn test_parse_option_error_flag_with_value() {
let mut reader = Reader::new("--verbose=1\n");
let mut options = CliOptions::default();
let err = parse_option(&mut reader, &mut options).unwrap_err();
assert_eq!(err.pos, Pos::new(1, 10));
assert_eq!(err.message, "Not expecting a value for this option");
let mut reader = Reader::new("--max-redirs=a\n");
let mut options = CliOptions::default();
let err = parse_option(&mut reader, &mut options).unwrap_err();
assert_eq!(err.pos, Pos::new(1, 14));
assert_eq!(err.message, "Option --max-redirs requires an integer value");
}
}