From 090dbe8417906c8df541576b05cbbb210aaf85bf Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 2 Mar 2026 13:29:05 -0800 Subject: [PATCH 1/2] [update] the argument lookup to utilize a hashmap and change positional lookup to keep track of the current index --- crates/lambda-rs-args/src/lib.rs | 152 +++++++++++++++++-------------- 1 file changed, 86 insertions(+), 66 deletions(-) diff --git a/crates/lambda-rs-args/src/lib.rs b/crates/lambda-rs-args/src/lib.rs index 15181862..ee04dfc2 100644 --- a/crates/lambda-rs-args/src/lib.rs +++ b/crates/lambda-rs-args/src/lib.rs @@ -432,6 +432,7 @@ impl ArgumentParser { // Errors are returned, not panicked. let mut collecting_values = false; let mut last_key: Option = None; + let mut next_positional_idx: usize = 0; let mut parsed_arguments = vec![]; parsed_arguments.resize( @@ -459,6 +460,7 @@ impl ArgumentParser { let sub_parsed = sub.parse(&argv)?; return Ok(ParsedArgs { values: vec![], + index_by_name: HashMap::new(), subcommand: Some((t.to_string(), Box::new(sub_parsed))), }); } @@ -477,6 +479,7 @@ impl ArgumentParser { let sub_parsed = sub.parse(&argv2)?; return Ok(ParsedArgs { values: vec![], + index_by_name: HashMap::new(), subcommand: Some((arg_token.to_string(), Box::new(sub_parsed))), }); } @@ -487,7 +490,11 @@ impl ArgumentParser { if arg_token == "--" { for value in iter.by_ref() { - self.assign_next_positional(&mut parsed_arguments, value.as_str())?; + self.assign_next_positional( + &mut parsed_arguments, + value.as_str(), + &mut next_positional_idx, + )?; } break; } @@ -753,10 +760,7 @@ impl ArgumentParser { } } - Ok(ParsedArgs { - values: parsed_arguments, - subcommand: None, - }) + Ok(ParsedArgs::new(parsed_arguments, None)) } /// Backwards‑compatible panicking API. Prefer `parse` for non‑panicking use. @@ -884,10 +888,28 @@ impl std::error::Error for ArgsError {} #[derive(Debug, Clone)] pub struct ParsedArgs { values: Vec, + index_by_name: HashMap, subcommand: Option<(String, Box)>, } impl ParsedArgs { + fn new( + values: Vec, + subcommand: Option<(String, Box)>, + ) -> Self { + let mut index_by_name: HashMap = + HashMap::with_capacity(values.len()); + for (idx, arg) in values.iter().enumerate() { + // Keys should be unique; keep the first occurrence if duplicates exist. + index_by_name.entry(arg.name.clone()).or_insert(idx); + } + Self { + values, + index_by_name, + subcommand, + } + } + /// Convert into the raw underlying `(name, value)` vector. pub fn into_vec(self) -> Vec { self.values @@ -895,81 +917,63 @@ impl ParsedArgs { /// True if the named argument is present (and not `None`). pub fn has(&self, name: &str) -> bool { - self - .values - .iter() - .any(|p| p.name == name && !matches!(p.value, ArgumentValue::None)) + let Some(idx) = self.index_by_name.get(name) else { + return false; + }; + return !matches!(self.values[*idx].value, ArgumentValue::None); } /// Get a `String` value by name, if present and typed as string. pub fn get_string(&self, name: &str) -> Option { - self - .values - .iter() - .find(|p| p.name == name) - .and_then(|p| match &p.value { - ArgumentValue::String(s) => Some(s.clone()), - _ => None, - }) + let idx = self.index_by_name.get(name)?; + match &self.values[*idx].value { + ArgumentValue::String(s) => Some(s.clone()), + _ => None, + } } /// Get an `i64` value by name, if present and typed as integer. pub fn get_i64(&self, name: &str) -> Option { - self - .values - .iter() - .find(|p| p.name == name) - .and_then(|p| match &p.value { - ArgumentValue::Integer(v) => Some(*v), - _ => None, - }) + let idx = self.index_by_name.get(name)?; + match &self.values[*idx].value { + ArgumentValue::Integer(v) => Some(*v), + _ => None, + } } /// Get an `f32` value by name, if present and typed as float. pub fn get_f32(&self, name: &str) -> Option { - self - .values - .iter() - .find(|p| p.name == name) - .and_then(|p| match &p.value { - ArgumentValue::Float(v) => Some(*v), - _ => None, - }) + let idx = self.index_by_name.get(name)?; + match &self.values[*idx].value { + ArgumentValue::Float(v) => Some(*v), + _ => None, + } } /// Get an `f64` value by name, if present and typed as double. pub fn get_f64(&self, name: &str) -> Option { - self - .values - .iter() - .find(|p| p.name == name) - .and_then(|p| match &p.value { - ArgumentValue::Double(v) => Some(*v), - _ => None, - }) + let idx = self.index_by_name.get(name)?; + match &self.values[*idx].value { + ArgumentValue::Double(v) => Some(*v), + _ => None, + } } /// Get a `bool` value by name, if present and typed as boolean. pub fn get_bool(&self, name: &str) -> Option { - self - .values - .iter() - .find(|p| p.name == name) - .and_then(|p| match &p.value { - ArgumentValue::Boolean(v) => Some(*v), - _ => None, - }) + let idx = self.index_by_name.get(name)?; + match &self.values[*idx].value { + ArgumentValue::Boolean(v) => Some(*v), + _ => None, + } } pub fn get_count(&self, name: &str) -> Option { - self - .values - .iter() - .find(|p| p.name == name) - .and_then(|p| match &p.value { - ArgumentValue::Integer(v) => Some(*v), - _ => None, - }) + let idx = self.index_by_name.get(name)?; + match &self.values[*idx].value { + ArgumentValue::Integer(v) => Some(*v), + _ => None, + } } pub fn subcommand(&self) -> Option<(&str, &ParsedArgs)> { @@ -1069,6 +1073,17 @@ mod tests { assert_eq!(p.get_string("rest").unwrap(), "b"); } + #[test] + fn has_respects_none_and_unknown() { + let parser = ArgumentParser::new("app") + .with_argument(Argument::new("--opt").with_type(ArgumentType::String)) + .with_argument(Argument::new("--flag").with_type(ArgumentType::Boolean)); + let p = parser.parse(&argv(&["--flag"])).unwrap(); + assert!(p.has("--flag")); + assert!(!p.has("--opt")); + assert!(!p.has("--does-not-exist")); + } + #[test] fn counting_and_cluster() { let parser = ArgumentParser::new("app").with_argument( @@ -1283,17 +1298,22 @@ impl ArgumentParser { &mut self, out: &mut [ParsedArgument], value: &str, + next_positional_idx: &mut usize, ) -> Result<(), ArgsError> { - for pname in self.positionals.clone() { - if let Some(entry) = self.args.get_mut(&pname) { - if !entry.1 { - let parsed = parse_value(&entry.0, value)?; - let idx = entry.2; - out[idx] = ParsedArgument::new(entry.0.name.as_str(), parsed); - entry.1 = true; - return Ok(()); - } + while *next_positional_idx < self.positionals.len() { + let pname = &self.positionals[*next_positional_idx]; + *next_positional_idx += 1; + let Some(entry) = self.args.get_mut(pname) else { + continue; + }; + if entry.1 { + continue; } + let parsed = parse_value(&entry.0, value)?; + let idx = entry.2; + out[idx] = ParsedArgument::new(entry.0.name.as_str(), parsed); + entry.1 = true; + return Ok(()); } Err(ArgsError::InvalidValue { name: "".to_string(), From 31472029a1f92ea5e2db9b1b90a1326d2192bbee Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 2 Mar 2026 15:42:15 -0800 Subject: [PATCH 2/2] [update] documentation --- crates/lambda-rs-args/src/lib.rs | 160 +++++++++++++++++++++++++++---- 1 file changed, 141 insertions(+), 19 deletions(-) diff --git a/crates/lambda-rs-args/src/lib.rs b/crates/lambda-rs-args/src/lib.rs index ee04dfc2..8b299108 100644 --- a/crates/lambda-rs-args/src/lib.rs +++ b/crates/lambda-rs-args/src/lib.rs @@ -427,11 +427,35 @@ impl ArgumentParser { out } - /// New non-panicking parser. Prefer this over `compile`. + /// Parses a slice of tokens into typed arguments. + /// + /// # Arguments + /// - `args`: Token vector (typically `std::env::args().collect()`). The first + /// token is treated as the executable name and is ignored for parsing. + /// + /// # Returns + /// Returns a `ParsedArgs` containing declared options/flags/positionals (with + /// defaults applied) and an optional parsed subcommand. + /// + /// # Errors + /// Returns `ArgsError` when: + /// - `--help` / `-h` is present (`ArgsError::HelpRequested`). + /// - An unknown argument is encountered in strict mode. + /// - A value is missing for a value-taking argument. + /// - A value fails type parsing. + /// - A required argument is not provided. + /// - Mutually exclusive or requires relationships are violated. + /// + /// - Aliases are resolved during parsing; lookups on `ParsedArgs` use the + /// declared argument name (canonical form), not an alias. + /// - Combined short flags like `-vvv` are supported only for `Count` and + /// `Boolean` arguments. pub fn parse(mut self, args: &[String]) -> Result { // Errors are returned, not panicked. let mut collecting_values = false; let mut last_key: Option = None; + // Cursor into `self.positionals` so assigning `--` values is O(p) total, + // rather than O(p^2) across p positionals. let mut next_positional_idx: usize = 0; let mut parsed_arguments = vec![]; @@ -458,11 +482,10 @@ impl ArgumentParser { } let sub = self.subcommands.remove(t).unwrap(); let sub_parsed = sub.parse(&argv)?; - return Ok(ParsedArgs { - values: vec![], - index_by_name: HashMap::new(), - subcommand: Some((t.to_string(), Box::new(sub_parsed))), - }); + return Ok(ParsedArgs::new( + vec![], + Some((t.to_string(), Box::new(sub_parsed))), + )); } } while let Some(token) = iter.next() { @@ -477,11 +500,10 @@ impl ArgumentParser { } let sub = self.subcommands.remove(arg_token).unwrap(); let sub_parsed = sub.parse(&argv2)?; - return Ok(ParsedArgs { - values: vec![], - index_by_name: HashMap::new(), - subcommand: Some((arg_token.to_string(), Box::new(sub_parsed))), - }); + return Ok(ParsedArgs::new( + vec![], + Some((arg_token.to_string(), Box::new(sub_parsed))), + )); } if arg_token == "--help" || arg_token == "-h" { @@ -764,6 +786,16 @@ impl ArgumentParser { } /// Backwards‑compatible panicking API. Prefer `parse` for non‑panicking use. + /// + /// # Arguments + /// - `args`: Token vector (typically `std::env::args().collect()`). The first + /// token is treated as the executable name and is ignored for parsing. + /// + /// # Returns + /// Returns the raw `(name, value)` vector of parsed arguments. + /// + /// # Panics + /// Panics with a formatted `ArgsError` string if parsing fails. pub fn compile(self, args: &[String]) -> Vec { match self.parse(args) { Ok(parsed) => parsed.into_vec(), @@ -884,15 +916,31 @@ impl fmt::Display for ArgsError { impl std::error::Error for ArgsError {} -/// Parsed arguments with typed getters and subcommand support. +/// Parsed arguments with typed getters and optional subcommand support. +/// +/// `ParsedArgs` is produced by `ArgumentParser::parse`. Lookups are optimized +/// for runtime usage (O(1) by name) by building an index when parsing +/// completes. #[derive(Debug, Clone)] pub struct ParsedArgs { values: Vec, + /// Lookup table from argument name to its index in `values`. + /// + /// This allows `has()` and `get_*()` calls to be O(1) instead of scanning + /// the full vector on every lookup. index_by_name: HashMap, subcommand: Option<(String, Box)>, } impl ParsedArgs { + /// Creates a `ParsedArgs` from raw parsed values. + /// + /// # Arguments + /// - `values`: Parsed arguments, indexed by the parser's internal ordering. + /// - `subcommand`: Optional subcommand parse result. + /// + /// # Returns + /// Returns a `ParsedArgs` with an O(1) name index for lookups. fn new( values: Vec, subcommand: Option<(String, Box)>, @@ -910,12 +958,26 @@ impl ParsedArgs { } } - /// Convert into the raw underlying `(name, value)` vector. + /// Converts into the raw underlying `(name, value)` vector. + /// + /// # Returns + /// Returns the internal parsed argument vector in parser order. pub fn into_vec(self) -> Vec { self.values } - /// True if the named argument is present (and not `None`). + /// Returns `true` if the named argument is present (and not `None`). + /// + /// # Arguments + /// - `name`: The declared argument name (for example, `"--port"` or + /// `"input"`). + /// + /// # Returns + /// Returns `true` when the argument is present and was not parsed as + /// `ArgumentValue::None`. + /// + /// Aliases like `"-p"` are not resolved at lookup time. Use the canonical + /// argument name. pub fn has(&self, name: &str) -> bool { let Some(idx) = self.index_by_name.get(name) else { return false; @@ -923,7 +985,14 @@ impl ParsedArgs { return !matches!(self.values[*idx].value, ArgumentValue::None); } - /// Get a `String` value by name, if present and typed as string. + /// Returns a `String` value by name, if present and typed as string. + /// + /// # Arguments + /// - `name`: The declared argument name (for example, `"--title"`). + /// + /// # Returns + /// Returns `Some(String)` when the argument exists and is a string, otherwise + /// returns `None`. pub fn get_string(&self, name: &str) -> Option { let idx = self.index_by_name.get(name)?; match &self.values[*idx].value { @@ -932,7 +1001,14 @@ impl ParsedArgs { } } - /// Get an `i64` value by name, if present and typed as integer. + /// Returns an `i64` value by name, if present and typed as integer. + /// + /// # Arguments + /// - `name`: The declared argument name (for example, `"--count"`). + /// + /// # Returns + /// Returns `Some(i64)` when the argument exists and is an integer, otherwise + /// returns `None`. pub fn get_i64(&self, name: &str) -> Option { let idx = self.index_by_name.get(name)?; match &self.values[*idx].value { @@ -941,7 +1017,14 @@ impl ParsedArgs { } } - /// Get an `f32` value by name, if present and typed as float. + /// Returns an `f32` value by name, if present and typed as float. + /// + /// # Arguments + /// - `name`: The declared argument name. + /// + /// # Returns + /// Returns `Some(f32)` when the argument exists and is a float, otherwise + /// returns `None`. pub fn get_f32(&self, name: &str) -> Option { let idx = self.index_by_name.get(name)?; match &self.values[*idx].value { @@ -950,7 +1033,14 @@ impl ParsedArgs { } } - /// Get an `f64` value by name, if present and typed as double. + /// Returns an `f64` value by name, if present and typed as double. + /// + /// # Arguments + /// - `name`: The declared argument name. + /// + /// # Returns + /// Returns `Some(f64)` when the argument exists and is a double, otherwise + /// returns `None`. pub fn get_f64(&self, name: &str) -> Option { let idx = self.index_by_name.get(name)?; match &self.values[*idx].value { @@ -959,7 +1049,14 @@ impl ParsedArgs { } } - /// Get a `bool` value by name, if present and typed as boolean. + /// Returns a `bool` value by name, if present and typed as boolean. + /// + /// # Arguments + /// - `name`: The declared argument name (for example, `"--verbose"`). + /// + /// # Returns + /// Returns `Some(bool)` when the argument exists and is a boolean, otherwise + /// returns `None`. pub fn get_bool(&self, name: &str) -> Option { let idx = self.index_by_name.get(name)?; match &self.values[*idx].value { @@ -968,6 +1065,14 @@ impl ParsedArgs { } } + /// Returns a count value by name, if present and typed as integer. + /// + /// # Arguments + /// - `name`: The declared argument name (for example, `"-v"`). + /// + /// # Returns + /// Returns `Some(i64)` when the argument exists and is an integer, otherwise + /// returns `None`. pub fn get_count(&self, name: &str) -> Option { let idx = self.index_by_name.get(name)?; match &self.values[*idx].value { @@ -976,6 +1081,10 @@ impl ParsedArgs { } } + /// Returns the parsed subcommand, if present. + /// + /// # Returns + /// Returns `Some((name, args))` when a subcommand was matched during parse. pub fn subcommand(&self) -> Option<(&str, &ParsedArgs)> { self .subcommand @@ -1294,12 +1403,25 @@ impl ArgumentParser { None } + /// Assigns the next positional argument in declaration order. + /// + /// # Arguments + /// - `out`: Output argument storage indexed by the parser's internal order. + /// - `value`: Token to assign to the next positional argument. + /// - `next_positional_idx`: Cursor into `self.positionals` tracking the next + /// candidate positional to assign. + /// + /// # Errors + /// Returns `ArgsError::InvalidValue` when no remaining positionals exist + /// (extra positional arguments). fn assign_next_positional( &mut self, out: &mut [ParsedArgument], value: &str, next_positional_idx: &mut usize, ) -> Result<(), ArgsError> { + // Assign the next positional in declaration order. `next_positional_idx` + // ensures we never rescan previously-considered positionals. while *next_positional_idx < self.positionals.len() { let pname = &self.positionals[*next_positional_idx]; *next_positional_idx += 1;