use crate::{ util::*, ConfigurationBuilder, ConfigurationProvider, ConfigurationSource, LoadResult, Value, }; use std::borrow::Cow; use std::collections::HashMap; /// Represents a [`ConfigurationProvider`](crate::ConfigurationProvider) that /// provides command line configuration values. pub struct CommandLineConfigurationProvider { data: HashMap, args: Vec, switch_mappings: HashMap, } impl CommandLineConfigurationProvider { /// Initializes a new command line configuration provider. /// /// # Arguments /// /// * `args` - The command line arguments /// * `switch_mappings` - The mapping of switches to configuration values /// /// # Remarks /// /// Only switch mapping keys that start with `--` or `-` are acceptable. Command /// line arguments may start with `--`, `-`, or `/` pub fn new(args: Vec, switch_mappings: HashMap) -> Self { Self { data: Default::default(), args, switch_mappings, } } } impl ConfigurationProvider for CommandLineConfigurationProvider { fn get(&self, key: &str) -> Option { self.data.get(&key.to_uppercase()).map(|t| t.1.clone()) } fn load(&mut self) -> LoadResult { let mut data = HashMap::new(); let mut args = self.args.iter(); while let Some(arg) = args.next() { let mut current = Cow::Borrowed(arg); let start: usize = if arg.starts_with("--") { 2 } else if arg.starts_with('-') { 1 } else if arg.starts_with('/') { // "/SomeSwitch" is equivalent to "--SomeSwitch" when interpreting switch mappings let mut temp = arg.clone(); temp.replace_range(0..1, "--"); current = Cow::Owned(temp); 2 } else { 0 }; let mut key: String; let value: String; if let Some(separator) = current.find('=') { let segment: String = current .chars() .take(separator) .map(|c| c.to_ascii_uppercase()) .collect(); key = if let Some(mapping) = self.switch_mappings.get(&segment) { mapping.clone() } else if start == 1 { continue; } else { current .chars() .skip(start) .take(separator - start) .collect() }; value = current.chars().skip(separator + 1).collect(); } else { if start == 0 { continue; } key = if let Some(mapping) = self.switch_mappings.get(¤t.to_uppercase()) { mapping.clone() } else if start == 0 { continue; } else { current.chars().skip(start).collect() }; if let Some(next) = args.next() { value = next.clone(); } else { continue; } } key = to_pascal_case_parts(key, '-'); data.insert(key.to_uppercase(), (key, value.into())); } data.shrink_to_fit(); self.data = data; Ok(()) } fn child_keys(&self, earlier_keys: &mut Vec, parent_path: Option<&str>) { accumulate_child_keys(&self.data, earlier_keys, parent_path) } } /// Represents a [`ConfigurationSource`](crate::ConfigurationSource) for command line data. #[derive(Default)] pub struct CommandLineConfigurationSource { /// Gets or sets a collection of key/value pairs representing the mapping between /// switches and configuration keys. pub switch_mappings: HashMap, /// Gets or sets the command line arguments. pub args: Vec, } impl CommandLineConfigurationSource { /// Initializes a new command line configuration source. /// /// # Arguments /// /// * `args` - The command line arguments /// * `switch_mappings` - The mapping of switches to configuration values /// /// # Remarks /// /// Only switch mapping keys that start with `--` or `-` are acceptable. Command /// line arguments may start with `--`, `-`, or `/`. pub fn new(args: I, switch_mappings: &[(S2, S2)]) -> Self where I: Iterator, S1: AsRef, S2: AsRef, { Self { args: args.map(|a| a.as_ref().to_owned()).collect(), switch_mappings: switch_mappings .iter() .filter(|m| m.0.as_ref().starts_with("--") || m.0.as_ref().starts_with('-')) .map(|(k, v)| (k.as_ref().to_uppercase(), v.as_ref().to_owned())) .collect(), } } } impl From for CommandLineConfigurationSource where I: Iterator, S: AsRef, { fn from(value: I) -> Self { let switch_mappings = Vec::<(&str, &str)>::with_capacity(0); Self::new(value, &switch_mappings) } } impl ConfigurationSource for CommandLineConfigurationSource { fn build(&self, _builder: &dyn ConfigurationBuilder) -> Box { Box::new(CommandLineConfigurationProvider::new( self.args.clone(), self.switch_mappings.clone(), )) } } pub mod ext { use super::*; /// Defines extension methods for [`ConfigurationBuilder`](crate::ConfigurationBuilder). pub trait CommandLineConfigurationBuilderExtensions { /// Adds the command line configuration source. fn add_command_line(&mut self) -> &mut Self; /// Adds the command line configuration source. /// /// # Arguments /// /// * `switch_mappings` - The mapping of switches to configuration values fn add_command_line_map>(&mut self, switch_mappings: &[(S, S)]) -> &mut Self; } impl CommandLineConfigurationBuilderExtensions for dyn ConfigurationBuilder + '_ { fn add_command_line(&mut self) -> &mut Self { self.add(Box::new(CommandLineConfigurationSource::from( std::env::args(), ))); self } fn add_command_line_map>(&mut self, switch_mappings: &[(S, S)]) -> &mut Self { self.add(Box::new(CommandLineConfigurationSource::new( std::env::args(), switch_mappings, ))); self } } impl CommandLineConfigurationBuilderExtensions for T { fn add_command_line(&mut self) -> &mut Self { self.add(Box::new(CommandLineConfigurationSource::from( std::env::args(), ))); self } fn add_command_line_map>(&mut self, switch_mappings: &[(S, S)]) -> &mut Self { self.add(Box::new(CommandLineConfigurationSource::new( std::env::args(), switch_mappings, ))); self } } } #[cfg(test)] mod tests { use super::*; struct TestConfigurationBuilder; impl ConfigurationBuilder for TestConfigurationBuilder { fn properties(&self) -> &HashMap> { unimplemented!() } fn sources(&self) -> &[Box] { unimplemented!() } fn add(&mut self, _source: Box) { unimplemented!() } fn build(&self) -> Result, crate::ReloadError> { unimplemented!() } } #[test] fn load_should_ignore_unknown_arguments() { // arrange let args = ["foo", "/bar=baz"].iter(); let source = CommandLineConfigurationSource::from(args); let mut provider = source.build(&TestConfigurationBuilder); let mut child_keys = Vec::with_capacity(2); // act provider.load().unwrap(); provider.child_keys(&mut child_keys, None); // assert assert_eq!(child_keys.len(), 1); assert_eq!(provider.get("bar").unwrap().as_str(), "baz"); } #[test] fn load_should_ignore_arguments_in_the_middle() { // arrange let args = [ "Key1=Value1", "--Key2=Value2", "/Key3=Value3", "Bogus1", "--Key4", "Value4", "Bogus2", "/Key5", "Value5", "Bogus3", ] .iter(); let source = CommandLineConfigurationSource::from(args); let mut provider = source.build(&TestConfigurationBuilder); let mut child_keys = Vec::with_capacity(5); // act provider.load().unwrap(); provider.child_keys(&mut child_keys, None); // assert assert_eq!(provider.get("Key1").unwrap().as_str(), "Value1"); assert_eq!(provider.get("Key2").unwrap().as_str(), "Value2"); assert_eq!(provider.get("Key3").unwrap().as_str(), "Value3"); assert_eq!(provider.get("Key4").unwrap().as_str(), "Value4"); assert_eq!(provider.get("Key5").unwrap().as_str(), "Value5"); } #[test] fn load_should_process_key_value_pairs_without_mappings() { // arrange let args = [ "Key1=Value1", "--Key2=Value2", "/Key3=Value3", "--Key4", "Value4", "/Key5", "Value5", "--single=1", "--two-part=2", ] .iter(); let source = CommandLineConfigurationSource::from(args); let mut provider = source.build(&TestConfigurationBuilder); // act provider.load().unwrap(); // assert assert_eq!(provider.get("Key1").unwrap().as_str(), "Value1"); assert_eq!(provider.get("Key2").unwrap().as_str(), "Value2"); assert_eq!(provider.get("Key3").unwrap().as_str(), "Value3"); assert_eq!(provider.get("Key4").unwrap().as_str(), "Value4"); assert_eq!(provider.get("Key5").unwrap().as_str(), "Value5"); assert_eq!(provider.get("Single").unwrap().as_str(), "1"); assert_eq!(provider.get("TwoPart").unwrap().as_str(), "2"); } #[test] fn load_should_process_key_value_pairs_with_mappings() { // arrange let args = [ "-K1=Value1", "--Key2=Value2", "/Key3=Value3", "--Key4", "Value4", "/Key5", "Value5", "/Key6=Value6", ] .iter(); let switch_mappings = [ ("-K1", "LongKey1"), ("--Key2", "SuperLongKey2"), ("--Key6", "SuchALongKey6"), ]; let source = CommandLineConfigurationSource::new(args, &switch_mappings); let mut provider = source.build(&TestConfigurationBuilder); // act provider.load().unwrap(); // assert assert_eq!(provider.get("LongKey1").unwrap().as_str(), "Value1"); assert_eq!(provider.get("SuperLongKey2").unwrap().as_str(), "Value2"); assert_eq!(provider.get("Key3").unwrap().as_str(), "Value3"); assert_eq!(provider.get("Key4").unwrap().as_str(), "Value4"); assert_eq!(provider.get("Key5").unwrap().as_str(), "Value5"); assert_eq!(provider.get("SuchALongKey6").unwrap().as_str(), "Value6"); } #[test] fn load_should_override_value_when_key_is_duplicated() { // arrange let args = ["/Key1=Value1", "--Key1=Value2"].iter(); let source = CommandLineConfigurationSource::from(args); let mut provider = source.build(&TestConfigurationBuilder); // act provider.load().unwrap(); // assert assert_eq!(provider.get("Key1").unwrap().as_str(), "Value2"); } #[test] fn load_should_ignore_key_when_value_is_missing() { // arrange let args = ["--Key1", "Value1", "/Key2"].iter(); let source = CommandLineConfigurationSource::from(args); let mut provider = source.build(&TestConfigurationBuilder); let mut child_keys = Vec::with_capacity(2); // act provider.load().unwrap(); provider.child_keys(&mut child_keys, None); // assert assert_eq!(child_keys.len(), 1); assert_eq!(provider.get("Key1").unwrap().as_str(), "Value1"); } #[test] fn load_should_ignore_unrecognizable_argument() { // arrange let args = ["ArgWithoutPrefixAndEqualSign"].iter(); let source = CommandLineConfigurationSource::from(args); let mut provider = source.build(&TestConfigurationBuilder); let mut child_keys = Vec::with_capacity(1); // act provider.load().unwrap(); provider.child_keys(&mut child_keys, None); // assert assert!(child_keys.is_empty()); } #[test] fn load_should_ignore_argument_when_short_switch_is_undefined() { // arrange let args = ["-Key1", "Value1"].iter(); let switch_mappings = [("-Key2", "LongKey2")]; let source = CommandLineConfigurationSource::new(args, &switch_mappings); let mut provider = source.build(&TestConfigurationBuilder); let mut child_keys = Vec::with_capacity(1); // act provider.load().unwrap(); provider.child_keys(&mut child_keys, Some("")); // assert assert!(child_keys.is_empty()); } }