#[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,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OptionOccurrence {
pub discriminant: String,
pub params: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ParsedValue {
Scalar(String),
Repeated(Vec<OptionOccurrence>),
}
impl<'a> CliParser<'a> {
pub fn new(definition: &'a CommandDefinition) -> Self {
Self { definition }
}
pub fn parse(&self, args: &[String]) -> Result<HashMap<String, String>> {
let typed = self.parse_typed(args)?;
Ok(typed
.into_iter()
.filter_map(|(name, value)| match value {
ParsedValue::Scalar(s) => Some((name, s)),
ParsedValue::Repeated(_) => None,
})
.collect())
}
pub fn parse_typed(&self, args: &[String]) -> Result<HashMap<String, ParsedValue>> {
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, ParsedValue>,
) -> 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)?;
if option.repeatable {
return Err(ParseError::InvalidSyntax {
details: format!(
"Option --{} is repeatable and does not support --{}=<value>",
option.name, option.name
),
hint: Some(format!(
"Usage: --{} <{}> [key=value ...]",
option.name,
option.choices.join("|")
)),
}
.into());
}
let parsed_value = type_parser::parse_value(value, option.option_type)?;
result.insert(option.name.clone(), ParsedValue::Scalar(parsed_value));
} else {
let option = self.find_option_by_long(arg_without_dashes)?;
if option.repeatable {
self.parse_repeatable_occurrence(option, args, index, result)?;
} else if matches!(
option.option_type,
crate::config::schema::ArgumentType::Bool
) {
result.insert(option.name.clone(), ParsedValue::Scalar("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(), ParsedValue::Scalar(parsed_value));
}
}
Ok(())
}
fn parse_short_option(
&self,
arg: &str,
args: &[String],
index: &mut usize,
result: &mut HashMap<String, ParsedValue>,
) -> Result<()> {
let short_flag = &arg[1..2];
let option = self.find_option_by_short(short_flag)?;
if option.repeatable {
if arg.len() > 2 {
return Err(ParseError::InvalidSyntax {
details: format!(
"Option -{} is repeatable and does not support an attached value",
short_flag
),
hint: Some(format!(
"Usage: -{} <{}> [key=value ...]",
short_flag,
option.choices.join("|")
)),
}
.into());
}
self.parse_repeatable_occurrence(option, args, index, result)?;
} else if matches!(
option.option_type,
crate::config::schema::ArgumentType::Bool
) {
result.insert(option.name.clone(), ParsedValue::Scalar("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(), ParsedValue::Scalar(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(), ParsedValue::Scalar(parsed_value));
}
}
Ok(())
}
fn parse_positional_argument(
&self,
value: &str,
index: usize,
result: &mut HashMap<String, ParsedValue>,
) -> 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(), ParsedValue::Scalar(parsed_value));
Ok(())
}
fn parse_repeatable_occurrence(
&self,
option: &OptionDefinition,
args: &[String],
index: &mut usize,
result: &mut HashMap<String, ParsedValue>,
) -> Result<()> {
*index += 1;
if *index >= args.len() {
return Err(ParseError::InvalidSyntax {
details: format!("Option --{} requires a discriminant", option.name),
hint: Some(format!(
"Usage: --{} <{}> [key=value ...]",
option.name,
option.choices.join("|")
)),
}
.into());
}
let discriminant = args[*index].clone();
if !option.choices.contains(&discriminant) {
return Err(ParseError::UnknownDiscriminant {
option: option.name.clone(),
value: discriminant,
valid_choices: option.choices.clone(),
suggestion: Some(format!(
"Run --help {} to see valid --{} kinds.",
self.definition.name, option.name
)),
}
.into());
}
let empty: Vec<ArgumentDefinition> = Vec::new();
let param_defs = option
.option_parameters
.get(&discriminant)
.unwrap_or(&empty);
let mut params: HashMap<String, String> = HashMap::new();
while *index + 1 < args.len() {
let next = &args[*index + 1];
if next.starts_with('-') {
break;
}
let eq_pos = match next.find('=') {
Some(pos) => pos,
None => {
return Err(ParseError::InvalidSyntax {
details: format!(
"Expected key=value for --{} {}, got: '{}'",
option.name, discriminant, next
),
hint: Some("Sub-parameters must use the key=value form.".to_string()),
}
.into());
}
};
let key = &next[..eq_pos];
let value = &next[eq_pos + 1..];
let arg_def = param_defs.iter().find(|a| a.name == key).ok_or_else(|| {
ParseError::UnknownOptionParameter {
option: option.name.clone(),
discriminant: discriminant.clone(),
key: key.to_string(),
valid_keys: param_defs.iter().map(|a| a.name.clone()).collect(),
suggestion: Some(format!(
"Run --help {} to see valid keys for --{} {}.",
self.definition.name, option.name, discriminant
)),
}
})?;
let typed_value = type_parser::parse_value(value, arg_def.arg_type)?;
params.insert(key.to_string(), typed_value);
*index += 1;
}
for arg_def in param_defs {
if arg_def.required && !params.contains_key(&arg_def.name) {
return Err(ParseError::MissingRequiredOptionParameter {
option: option.name.clone(),
discriminant: discriminant.clone(),
key: arg_def.name.clone(),
suggestion: Some(format!(
"Run --help {} to see required keys for --{} {}.",
self.definition.name, option.name, discriminant
)),
}
.into());
}
}
let occurrence = OptionOccurrence {
discriminant: discriminant.clone(),
params,
};
match result
.entry(option.name.clone())
.or_insert_with(|| ParsedValue::Repeated(Vec::new()))
{
ParsedValue::Repeated(occurrences) => {
if occurrences.contains(&occurrence) {
return Err(ParseError::DuplicateOptionOccurrence {
option: option.name.clone(),
discriminant: occurrence.discriminant.clone(),
params: occurrence.params.clone().into_iter().collect(),
suggestion: Some(format!(
"Remove one of the two identical --{} {} occurrences.",
option.name, discriminant
)),
}
.into());
}
occurrences.push(occurrence);
}
ParsedValue::Scalar(_) => unreachable!(
"option '{}' marked repeatable cannot already hold a Scalar value",
option.name
),
}
Ok(())
}
fn apply_defaults(&self, result: &mut HashMap<String, ParsedValue>) -> 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(), ParsedValue::Scalar(parsed_default));
}
}
}
Ok(())
}
fn validate_required_arguments(&self, result: &HashMap<String, ParsedValue>) -> 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, ParsedValue>) -> 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![],
secure: false,
},
ArgumentDefinition {
name: "output".to_string(),
arg_type: ArgumentType::Path,
required: false,
description: "Output file".to_string(),
validation: vec![],
secure: false,
},
],
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![],
repeatable: false,
option_parameters: HashMap::new(),
},
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![],
repeatable: false,
option_parameters: HashMap::new(),
},
],
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()));
}
fn create_repeatable_test_definition() -> CommandDefinition {
let mut option_parameters = HashMap::new();
option_parameters.insert(
"csv".to_string(),
vec![
ArgumentDefinition {
name: "file".to_string(),
arg_type: ArgumentType::Path,
required: true,
description: "Destination CSV file".to_string(),
validation: vec![],
secure: false,
},
ArgumentDefinition {
name: "resolution".to_string(),
arg_type: ArgumentType::Integer,
required: false,
description: "Time-step resolution".to_string(),
validation: vec![],
secure: false,
},
],
);
option_parameters.insert(
"plot".to_string(),
vec![ArgumentDefinition {
name: "file".to_string(),
arg_type: ArgumentType::Path,
required: true,
description: "Destination image file".to_string(),
validation: vec![],
secure: false,
}],
);
CommandDefinition {
name: "export".to_string(),
aliases: vec![],
description: "Export simulation results".to_string(),
required: false,
arguments: vec![],
options: vec![OptionDefinition {
name: "output".to_string(),
short: None,
long: Some("output".to_string()),
option_type: ArgumentType::String,
required: false,
default: None,
description: "Write results in one or more output kinds".to_string(),
choices: vec!["csv".to_string(), "plot".to_string()],
repeatable: true,
option_parameters,
}],
implementation: "export_handler".to_string(),
}
}
#[test]
fn test_parse_repeatable_option_single_occurrence() {
let definition = create_repeatable_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"--output".to_string(),
"csv".to_string(),
"file=results.csv".to_string(),
];
let result = parser.parse_typed(&args).unwrap();
match result.get("output") {
Some(ParsedValue::Repeated(occurrences)) => {
assert_eq!(occurrences.len(), 1);
assert_eq!(occurrences[0].discriminant, "csv");
assert_eq!(
occurrences[0].params.get("file"),
Some(&"results.csv".to_string())
);
}
other => panic!("Expected Repeated([csv]), got {:?}", other),
}
}
#[test]
fn test_parse_repeatable_option_optional_param_can_be_omitted() {
let definition = create_repeatable_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"--output".to_string(),
"csv".to_string(),
"file=results.csv".to_string(),
];
let result = parser.parse_typed(&args).unwrap();
match result.get("output") {
Some(ParsedValue::Repeated(occurrences)) => {
assert_eq!(occurrences[0].params.get("resolution"), None);
}
other => panic!("Expected Repeated([csv]), got {:?}", other),
}
}
#[test]
fn test_parse_repeatable_option_with_optional_param_provided() {
let definition = create_repeatable_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"--output".to_string(),
"csv".to_string(),
"file=results.csv".to_string(),
"resolution=100".to_string(),
];
let result = parser.parse_typed(&args).unwrap();
match result.get("output") {
Some(ParsedValue::Repeated(occurrences)) => {
assert_eq!(
occurrences[0].params.get("resolution"),
Some(&"100".to_string())
);
}
other => panic!("Expected Repeated([csv]), got {:?}", other),
}
}
#[test]
fn test_parse_repeatable_option_multiple_discriminants_both_parse() {
let definition = create_repeatable_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"--output".to_string(),
"csv".to_string(),
"file=results.csv".to_string(),
"--output".to_string(),
"plot".to_string(),
"file=chart.png".to_string(),
];
let result = parser.parse_typed(&args).unwrap();
match result.get("output") {
Some(ParsedValue::Repeated(occurrences)) => {
assert_eq!(occurrences.len(), 2);
assert_eq!(occurrences[0].discriminant, "csv");
assert_eq!(occurrences[1].discriminant, "plot");
}
other => panic!("Expected Repeated([csv, plot]), got {:?}", other),
}
}
#[test]
fn test_parse_repeatable_option_same_discriminant_different_params_both_kept() {
let definition = create_repeatable_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"--output".to_string(),
"csv".to_string(),
"file=a.csv".to_string(),
"--output".to_string(),
"csv".to_string(),
"file=b.csv".to_string(),
"resolution=50".to_string(),
];
let result = parser.parse_typed(&args).unwrap();
match result.get("output") {
Some(ParsedValue::Repeated(occurrences)) => {
assert_eq!(occurrences.len(), 2);
}
other => panic!("Expected Repeated([csv, csv]), got {:?}", other),
}
}
#[test]
fn test_parse_repeatable_option_duplicate_occurrence_errors() {
let definition = create_repeatable_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"--output".to_string(),
"csv".to_string(),
"file=a.csv".to_string(),
"--output".to_string(),
"csv".to_string(),
"file=a.csv".to_string(),
];
let result = parser.parse_typed(&args);
assert!(result.is_err());
match result.unwrap_err() {
crate::error::DynamicCliError::Parse(ParseError::DuplicateOptionOccurrence {
..
}) => {}
other => panic!("Expected DuplicateOptionOccurrence error, got {:?}", other),
}
}
#[test]
fn test_parse_repeatable_option_missing_required_param_errors() {
let definition = create_repeatable_test_definition();
let parser = CliParser::new(&definition);
let args = vec!["--output".to_string(), "csv".to_string()];
let result = parser.parse_typed(&args);
assert!(result.is_err());
match result.unwrap_err() {
crate::error::DynamicCliError::Parse(ParseError::MissingRequiredOptionParameter {
key,
..
}) => {
assert_eq!(key, "file");
}
other => panic!(
"Expected MissingRequiredOptionParameter error, got {:?}",
other
),
}
}
#[test]
fn test_parse_repeatable_option_unknown_param_key_errors() {
let definition = create_repeatable_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"--output".to_string(),
"csv".to_string(),
"file=a.csv".to_string(),
"compression=gzip".to_string(),
];
let result = parser.parse_typed(&args);
assert!(result.is_err());
match result.unwrap_err() {
crate::error::DynamicCliError::Parse(ParseError::UnknownOptionParameter {
key,
..
}) => {
assert_eq!(key, "compression");
}
other => panic!("Expected UnknownOptionParameter error, got {:?}", other),
}
}
#[test]
fn test_parse_repeatable_option_unknown_discriminant_errors() {
let definition = create_repeatable_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"--output".to_string(),
"xml".to_string(),
"file=a.xml".to_string(),
];
let result = parser.parse_typed(&args);
assert!(result.is_err());
match result.unwrap_err() {
crate::error::DynamicCliError::Parse(ParseError::UnknownDiscriminant {
value, ..
}) => {
assert_eq!(value, "xml");
}
other => panic!("Expected UnknownDiscriminant error, got {:?}", other),
}
}
#[test]
fn test_parse_legacy_drops_repeated_values() {
let definition = create_repeatable_test_definition();
let parser = CliParser::new(&definition);
let args = vec![
"--output".to_string(),
"csv".to_string(),
"file=a.csv".to_string(),
];
let result = parser.parse(&args).unwrap();
assert_eq!(result.get("output"), None);
}
}