#[allow(unused_imports)]
use crate::config::schema::{ArgumentDefinition, CommandDefinition, OptionDefinition};
use crate::error::{ParseError, Result};
use crate::parser::type_parser;
use std::collections::HashMap;
pub struct CliParser<'a> {
definition: &'a CommandDefinition,
}
impl<'a> CliParser<'a> {
pub fn new(definition: &'a CommandDefinition) -> Self {
Self { definition }
}
pub fn parse(&self, args: &[String]) -> Result<HashMap<String, String>> {
let mut result = HashMap::new();
let mut positional_index = 0;
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg.starts_with("--") {
self.parse_long_option(arg, args, &mut i, &mut result)?;
} else if arg.starts_with('-') && arg.len() > 1 {
if arg
.chars()
.nth(1)
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
self.parse_positional_argument(arg, positional_index, &mut result)?;
positional_index += 1;
} else {
self.parse_short_option(arg, args, &mut i, &mut result)?;
}
} else {
self.parse_positional_argument(arg, positional_index, &mut result)?;
positional_index += 1;
}
i += 1;
}
self.apply_defaults(&mut result)?;
self.validate_required_arguments(&result)?;
self.validate_required_options(&result)?;
Ok(result)
}
fn parse_long_option(
&self,
arg: &str,
args: &[String],
index: &mut usize,
result: &mut HashMap<String, String>,
) -> Result<()> {
let arg_without_dashes = &arg[2..];
if let Some(eq_pos) = arg_without_dashes.find('=') {
let option_name = &arg_without_dashes[..eq_pos];
let value = &arg_without_dashes[eq_pos + 1..];
let option = self.find_option_by_long(option_name)?;
let parsed_value = type_parser::parse_value(value, option.option_type)?;
result.insert(option.name.clone(), parsed_value);
} else {
let option = self.find_option_by_long(arg_without_dashes)?;
if matches!(
option.option_type,
crate::config::schema::ArgumentType::Bool
) {
result.insert(option.name.clone(), "true".to_string());
} else {
*index += 1;
if *index >= args.len() {
return Err(ParseError::InvalidSyntax {
details: format!(
"Option --{} requires a value",
option.long.as_ref().unwrap()
),
hint: Some(format!(
"Usage: --{}=<value> or --{} <value>",
option.long.as_ref().unwrap(),
option.long.as_ref().unwrap()
)),
}
.into());
}
let value = &args[*index];
let parsed_value = type_parser::parse_value(value, option.option_type)?;
result.insert(option.name.clone(), parsed_value);
}
}
Ok(())
}
fn parse_short_option(
&self,
arg: &str,
args: &[String],
index: &mut usize,
result: &mut HashMap<String, String>,
) -> Result<()> {
let short_flag = &arg[1..2];
let option = self.find_option_by_short(short_flag)?;
if matches!(
option.option_type,
crate::config::schema::ArgumentType::Bool
) {
result.insert(option.name.clone(), "true".to_string());
} else {
if arg.len() > 2 {
let value = &arg[2..];
let parsed_value = type_parser::parse_value(value, option.option_type)?;
result.insert(option.name.clone(), parsed_value);
} else {
*index += 1;
if *index >= args.len() {
return Err(ParseError::InvalidSyntax {
details: format!("Option -{} requires a value", short_flag),
hint: Some(format!(
"Usage: -{}<value> or -{} <value>",
short_flag, short_flag
)),
}
.into());
}
let value = &args[*index];
let parsed_value = type_parser::parse_value(value, option.option_type)?;
result.insert(option.name.clone(), parsed_value);
}
}
Ok(())
}
fn parse_positional_argument(
&self,
value: &str,
index: usize,
result: &mut HashMap<String, String>,
) -> Result<()> {
if index >= self.definition.arguments.len() {
return Err(ParseError::too_many_arguments(
&self.definition.name,
self.definition.arguments.len(),
index + 1,
)
.into());
}
let arg_def = &self.definition.arguments[index];
let parsed_value = type_parser::parse_value(value, arg_def.arg_type)?;
result.insert(arg_def.name.clone(), parsed_value);
Ok(())
}
fn apply_defaults(&self, result: &mut HashMap<String, String>) -> Result<()> {
for option in &self.definition.options {
if !result.contains_key(&option.name) {
if let Some(ref default) = option.default {
let parsed_default = type_parser::parse_value(default, option.option_type)?;
result.insert(option.name.clone(), parsed_default);
}
}
}
Ok(())
}
fn validate_required_arguments(&self, result: &HashMap<String, String>) -> Result<()> {
for arg in &self.definition.arguments {
if arg.required && !result.contains_key(&arg.name) {
return Err(ParseError::missing_argument(&arg.name, &self.definition.name).into());
}
}
Ok(())
}
fn validate_required_options(&self, result: &HashMap<String, String>) -> Result<()> {
for option in &self.definition.options {
if option.required && !result.contains_key(&option.name) {
return Err(ParseError::missing_option(
&option
.long
.clone()
.or(option.short.clone())
.unwrap_or_default(),
&self.definition.name,
)
.into());
}
}
Ok(())
}
fn find_option_by_long(&self, long: &str) -> Result<&OptionDefinition> {
self.definition
.options
.iter()
.find(|opt| opt.long.as_deref() == Some(long))
.ok_or_else(|| {
let available: Vec<String> = self
.definition
.options
.iter()
.filter_map(|o| o.long.clone())
.collect();
ParseError::unknown_option_with_suggestions(
&format!("--{}", long),
&self.definition.name,
&available,
)
.into()
})
}
fn find_option_by_short(&self, short: &str) -> Result<&OptionDefinition> {
self.definition
.options
.iter()
.find(|opt| opt.short.as_deref() == Some(short))
.ok_or_else(|| {
let available: Vec<String> = self
.definition
.options
.iter()
.filter_map(|o| o.short.clone())
.collect();
ParseError::unknown_option_with_suggestions(
&format!("-{}", short),
&self.definition.name,
&available,
)
.into()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::schema::{ArgumentType, OptionDefinition};
fn create_test_definition() -> CommandDefinition {
CommandDefinition {
name: "test".to_string(),
aliases: vec![],
description: "Test command".to_string(),
required: false,
arguments: vec![
ArgumentDefinition {
name: "input".to_string(),
arg_type: ArgumentType::Path,
required: true,
description: "Input file".to_string(),
validation: vec![],
},
ArgumentDefinition {
name: "output".to_string(),
arg_type: ArgumentType::Path,
required: false,
description: "Output file".to_string(),
validation: vec![],
},
],
options: vec![
OptionDefinition {
name: "verbose".to_string(),
short: Some("v".to_string()),
long: Some("verbose".to_string()),
option_type: ArgumentType::Bool,
required: false,
default: Some("false".to_string()),
description: "Verbose output".to_string(),
choices: vec![],
},
OptionDefinition {
name: "count".to_string(),
short: Some("c".to_string()),
long: Some("count".to_string()),
option_type: ArgumentType::Integer,
required: false,
default: Some("10".to_string()),
description: "Count".to_string(),
choices: vec![],
},
],
implementation: "handler".to_string(),
}
}
#[test]
fn test_parse_single_positional_argument() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["input.txt".to_string()];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
}
#[test]
fn test_parse_multiple_positional_arguments() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["input.txt".to_string(), "output.txt".to_string()];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
assert_eq!(result.get("output"), Some(&"output.txt".to_string()));
}
#[test]
fn test_parse_missing_required_argument() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args: Vec<String> = vec![];
let result = parser.parse(&args);
assert!(result.is_err());
match result.unwrap_err() {
crate::error::DynamicCliError::Parse(ParseError::MissingArgument {
argument, ..
}) => {
assert_eq!(argument, "input");
}
other => panic!("Expected MissingArgument error, got {:?}", other),
}
}
#[test]
fn test_parse_too_many_positional_arguments() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"input.txt".to_string(),
"output.txt".to_string(),
"extra.txt".to_string(),
];
let result = parser.parse(&args);
assert!(result.is_err());
match result.unwrap_err() {
crate::error::DynamicCliError::Parse(ParseError::TooManyArguments { .. }) => {}
other => panic!("Expected TooManyArguments error, got {:?}", other),
}
}
#[test]
fn test_parse_long_boolean_option() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["input.txt".to_string(), "--verbose".to_string()];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("verbose"), Some(&"true".to_string()));
}
#[test]
fn test_parse_long_option_with_equals() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["input.txt".to_string(), "--count=42".to_string()];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("count"), Some(&"42".to_string()));
}
#[test]
fn test_parse_long_option_with_space() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"input.txt".to_string(),
"--count".to_string(),
"42".to_string(),
];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("count"), Some(&"42".to_string()));
}
#[test]
fn test_parse_unknown_long_option() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["input.txt".to_string(), "--unknown".to_string()];
let result = parser.parse(&args);
assert!(result.is_err());
match result.unwrap_err() {
crate::error::DynamicCliError::Parse(ParseError::UnknownOption { .. }) => {}
other => panic!("Expected UnknownOption error, got {:?}", other),
}
}
#[test]
fn test_parse_short_boolean_option() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["input.txt".to_string(), "-v".to_string()];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("verbose"), Some(&"true".to_string()));
}
#[test]
fn test_parse_short_option_with_space() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["input.txt".to_string(), "-c".to_string(), "42".to_string()];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("count"), Some(&"42".to_string()));
}
#[test]
fn test_parse_short_option_attached_value() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["input.txt".to_string(), "-c42".to_string()];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("count"), Some(&"42".to_string()));
}
#[test]
fn test_parse_negative_number_as_positional() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["-123".to_string()];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("input"), Some(&"-123".to_string()));
}
#[test]
fn test_apply_default_values() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["input.txt".to_string()];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("verbose"), Some(&"false".to_string()));
assert_eq!(result.get("count"), Some(&"10".to_string()));
}
#[test]
fn test_override_default_values() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"input.txt".to_string(),
"-v".to_string(),
"-c".to_string(),
"5".to_string(),
];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("verbose"), Some(&"true".to_string()));
assert_eq!(result.get("count"), Some(&"5".to_string()));
}
#[test]
fn test_type_conversion_error() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"input.txt".to_string(),
"--count".to_string(),
"abc".to_string(),
];
let result = parser.parse(&args);
assert!(result.is_err());
}
#[test]
fn test_parse_complex_command_line() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"input.txt".to_string(),
"output.txt".to_string(),
"--verbose".to_string(),
"--count=100".to_string(),
];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
assert_eq!(result.get("output"), Some(&"output.txt".to_string()));
assert_eq!(result.get("verbose"), Some(&"true".to_string()));
assert_eq!(result.get("count"), Some(&"100".to_string()));
}
#[test]
fn test_parse_mixed_options_and_arguments() {
let definition = create_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"--verbose".to_string(),
"input.txt".to_string(),
"-c".to_string(),
"50".to_string(),
"output.txt".to_string(),
];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
assert_eq!(result.get("output"), Some(&"output.txt".to_string()));
assert_eq!(result.get("verbose"), Some(&"true".to_string()));
assert_eq!(result.get("count"), Some(&"50".to_string()));
}
}