Skip to main content

config/
cmd.rs

1use crate::{util::*, ConfigurationBuilder, ConfigurationProvider, ConfigurationSource, LoadResult, Value};
2use std::borrow::Cow;
3use std::collections::HashMap;
4
5/// Represents a [`ConfigurationProvider`](crate::ConfigurationProvider) that
6/// provides command line configuration values.
7pub struct CommandLineConfigurationProvider {
8    data: HashMap<String, (String, Value)>,
9    args: Vec<String>,
10    switch_mappings: HashMap<String, String>,
11}
12
13impl CommandLineConfigurationProvider {
14    /// Initializes a new command line configuration provider.
15    ///
16    /// # Arguments
17    ///
18    /// * `args` - The command line arguments
19    /// * `switch_mappings` - The mapping of switches to configuration values
20    ///
21    /// # Remarks
22    ///
23    /// Only switch mapping keys that start with `--` or `-` are acceptable. Command
24    /// line arguments may start with `--`, `-`, or `/`
25    pub fn new(args: Vec<String>, switch_mappings: HashMap<String, String>) -> Self {
26        Self {
27            data: Default::default(),
28            args,
29            switch_mappings,
30        }
31    }
32}
33
34impl ConfigurationProvider for CommandLineConfigurationProvider {
35    fn get(&self, key: &str) -> Option<Value> {
36        self.data.get(&key.to_uppercase()).map(|t| t.1.clone())
37    }
38
39    fn load(&mut self) -> LoadResult {
40        let mut data = HashMap::new();
41        let mut args = self.args.iter();
42
43        while let Some(arg) = args.next() {
44            let mut current = Cow::Borrowed(arg);
45            let start: usize = if arg.starts_with("--") {
46                2
47            } else if arg.starts_with('-') {
48                1
49            } else if arg.starts_with('/') {
50                // "/SomeSwitch" is equivalent to "--SomeSwitch" when interpreting switch mappings
51                let mut temp = arg.clone();
52                temp.replace_range(0..1, "--");
53                current = Cow::Owned(temp);
54                2
55            } else {
56                0
57            };
58
59            let mut key: String;
60            let value: String;
61
62            if let Some(separator) = current.find('=') {
63                let segment: String = current
64                    .chars()
65                    .take(separator)
66                    .map(|c| c.to_ascii_uppercase())
67                    .collect();
68
69                key = if let Some(mapping) = self.switch_mappings.get(&segment) {
70                    mapping.clone()
71                } else if start == 1 {
72                    continue;
73                } else {
74                    current.chars().skip(start).take(separator - start).collect()
75                };
76
77                value = current.chars().skip(separator + 1).collect();
78            } else {
79                if start == 0 {
80                    continue;
81                }
82
83                key = if let Some(mapping) = self.switch_mappings.get(&current.to_uppercase()) {
84                    mapping.clone()
85                } else if start == 0 {
86                    continue;
87                } else {
88                    current.chars().skip(start).collect()
89                };
90
91                if let Some(next) = args.next() {
92                    value = next.clone();
93                } else {
94                    continue;
95                }
96            }
97
98            key = to_pascal_case_parts(key, '-');
99            data.insert(key.to_uppercase(), (key, value.into()));
100        }
101
102        data.shrink_to_fit();
103        self.data = data;
104        Ok(())
105    }
106
107    fn child_keys(&self, earlier_keys: &mut Vec<String>, parent_path: Option<&str>) {
108        accumulate_child_keys(&self.data, earlier_keys, parent_path)
109    }
110}
111
112/// Represents a [`ConfigurationSource`](crate::ConfigurationSource) for command line data.
113#[derive(Default)]
114pub struct CommandLineConfigurationSource {
115    /// Gets or sets a collection of key/value pairs representing the mapping between
116    /// switches and configuration keys.
117    pub switch_mappings: HashMap<String, String>,
118
119    /// Gets or sets the command line arguments.
120    pub args: Vec<String>,
121}
122
123impl CommandLineConfigurationSource {
124    /// Initializes a new command line configuration source.
125    ///
126    /// # Arguments
127    ///
128    /// * `args` - The command line arguments
129    /// * `switch_mappings` - The mapping of switches to configuration values
130    ///
131    /// # Remarks
132    ///
133    /// Only switch mapping keys that start with `--` or `-` are acceptable. Command
134    /// line arguments may start with `--`, `-`, or `/`.
135    pub fn new<I, S1, S2>(args: I, switch_mappings: &[(S2, S2)]) -> Self
136    where
137        I: Iterator<Item = S1>,
138        S1: AsRef<str>,
139        S2: AsRef<str>,
140    {
141        Self {
142            args: args.map(|a| a.as_ref().to_owned()).collect(),
143            switch_mappings: switch_mappings
144                .iter()
145                .filter(|m| m.0.as_ref().starts_with("--") || m.0.as_ref().starts_with('-'))
146                .map(|(k, v)| (k.as_ref().to_uppercase(), v.as_ref().to_owned()))
147                .collect(),
148        }
149    }
150}
151
152impl<I, S> From<I> for CommandLineConfigurationSource
153where
154    I: Iterator<Item = S>,
155    S: AsRef<str>,
156{
157    fn from(value: I) -> Self {
158        let switch_mappings = Vec::<(&str, &str)>::with_capacity(0);
159        Self::new(value, &switch_mappings)
160    }
161}
162
163impl ConfigurationSource for CommandLineConfigurationSource {
164    fn build(&self, _builder: &dyn ConfigurationBuilder) -> Box<dyn ConfigurationProvider> {
165        Box::new(CommandLineConfigurationProvider::new(
166            self.args.clone(),
167            self.switch_mappings.clone(),
168        ))
169    }
170}
171
172pub mod ext {
173
174    use super::*;
175
176    /// Defines extension methods for [`ConfigurationBuilder`](crate::ConfigurationBuilder).
177    pub trait CommandLineConfigurationBuilderExtensions {
178        /// Adds the command line configuration source.
179        fn add_command_line(&mut self) -> &mut Self;
180
181        /// Adds the command line configuration source.
182        ///
183        /// # Arguments
184        ///
185        /// * `switch_mappings` - The mapping of switches to configuration values
186        fn add_command_line_map<S: AsRef<str>>(&mut self, switch_mappings: &[(S, S)]) -> &mut Self;
187    }
188
189    impl CommandLineConfigurationBuilderExtensions for dyn ConfigurationBuilder + '_ {
190        fn add_command_line(&mut self) -> &mut Self {
191            self.add(Box::new(CommandLineConfigurationSource::from(std::env::args())));
192            self
193        }
194
195        fn add_command_line_map<S: AsRef<str>>(&mut self, switch_mappings: &[(S, S)]) -> &mut Self {
196            self.add(Box::new(CommandLineConfigurationSource::new(
197                std::env::args(),
198                switch_mappings,
199            )));
200            self
201        }
202    }
203
204    impl<T: ConfigurationBuilder> CommandLineConfigurationBuilderExtensions for T {
205        fn add_command_line(&mut self) -> &mut Self {
206            self.add(Box::new(CommandLineConfigurationSource::from(std::env::args())));
207            self
208        }
209
210        fn add_command_line_map<S: AsRef<str>>(&mut self, switch_mappings: &[(S, S)]) -> &mut Self {
211            self.add(Box::new(CommandLineConfigurationSource::new(
212                std::env::args(),
213                switch_mappings,
214            )));
215            self
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222
223    use super::*;
224
225    struct TestConfigurationBuilder;
226
227    impl ConfigurationBuilder for TestConfigurationBuilder {
228        fn properties(&self) -> &HashMap<String, Box<dyn std::any::Any>> {
229            unimplemented!()
230        }
231
232        fn sources(&self) -> &[Box<dyn ConfigurationSource>] {
233            unimplemented!()
234        }
235
236        fn add(&mut self, _source: Box<dyn ConfigurationSource>) {
237            unimplemented!()
238        }
239
240        fn build(&self) -> Result<Box<dyn crate::ConfigurationRoot>, crate::ReloadError> {
241            unimplemented!()
242        }
243    }
244
245    #[test]
246    fn load_should_ignore_unknown_arguments() {
247        // arrange
248        let args = ["foo", "/bar=baz"].iter();
249        let source = CommandLineConfigurationSource::from(args);
250        let mut provider = source.build(&TestConfigurationBuilder);
251        let mut child_keys = Vec::with_capacity(2);
252
253        // act
254        provider.load().unwrap();
255        provider.child_keys(&mut child_keys, None);
256
257        // assert
258        assert_eq!(child_keys.len(), 1);
259        assert_eq!(provider.get("bar").unwrap().as_str(), "baz");
260    }
261
262    #[test]
263    fn load_should_ignore_arguments_in_the_middle() {
264        // arrange
265        let args = [
266            "Key1=Value1",
267            "--Key2=Value2",
268            "/Key3=Value3",
269            "Bogus1",
270            "--Key4",
271            "Value4",
272            "Bogus2",
273            "/Key5",
274            "Value5",
275            "Bogus3",
276        ]
277        .iter();
278        let source = CommandLineConfigurationSource::from(args);
279        let mut provider = source.build(&TestConfigurationBuilder);
280        let mut child_keys = Vec::with_capacity(5);
281
282        // act
283        provider.load().unwrap();
284        provider.child_keys(&mut child_keys, None);
285
286        // assert
287        assert_eq!(provider.get("Key1").unwrap().as_str(), "Value1");
288        assert_eq!(provider.get("Key2").unwrap().as_str(), "Value2");
289        assert_eq!(provider.get("Key3").unwrap().as_str(), "Value3");
290        assert_eq!(provider.get("Key4").unwrap().as_str(), "Value4");
291        assert_eq!(provider.get("Key5").unwrap().as_str(), "Value5");
292    }
293
294    #[test]
295    fn load_should_process_key_value_pairs_without_mappings() {
296        // arrange
297        let args = [
298            "Key1=Value1",
299            "--Key2=Value2",
300            "/Key3=Value3",
301            "--Key4",
302            "Value4",
303            "/Key5",
304            "Value5",
305            "--single=1",
306            "--two-part=2",
307        ]
308        .iter();
309        let source = CommandLineConfigurationSource::from(args);
310        let mut provider = source.build(&TestConfigurationBuilder);
311
312        // act
313        provider.load().unwrap();
314
315        // assert
316        assert_eq!(provider.get("Key1").unwrap().as_str(), "Value1");
317        assert_eq!(provider.get("Key2").unwrap().as_str(), "Value2");
318        assert_eq!(provider.get("Key3").unwrap().as_str(), "Value3");
319        assert_eq!(provider.get("Key4").unwrap().as_str(), "Value4");
320        assert_eq!(provider.get("Key5").unwrap().as_str(), "Value5");
321        assert_eq!(provider.get("Single").unwrap().as_str(), "1");
322        assert_eq!(provider.get("TwoPart").unwrap().as_str(), "2");
323    }
324
325    #[test]
326    fn load_should_process_key_value_pairs_with_mappings() {
327        // arrange
328        let args = [
329            "-K1=Value1",
330            "--Key2=Value2",
331            "/Key3=Value3",
332            "--Key4",
333            "Value4",
334            "/Key5",
335            "Value5",
336            "/Key6=Value6",
337        ]
338        .iter();
339        let switch_mappings = [
340            ("-K1", "LongKey1"),
341            ("--Key2", "SuperLongKey2"),
342            ("--Key6", "SuchALongKey6"),
343        ];
344        let source = CommandLineConfigurationSource::new(args, &switch_mappings);
345        let mut provider = source.build(&TestConfigurationBuilder);
346
347        // act
348        provider.load().unwrap();
349
350        // assert
351        assert_eq!(provider.get("LongKey1").unwrap().as_str(), "Value1");
352        assert_eq!(provider.get("SuperLongKey2").unwrap().as_str(), "Value2");
353        assert_eq!(provider.get("Key3").unwrap().as_str(), "Value3");
354        assert_eq!(provider.get("Key4").unwrap().as_str(), "Value4");
355        assert_eq!(provider.get("Key5").unwrap().as_str(), "Value5");
356        assert_eq!(provider.get("SuchALongKey6").unwrap().as_str(), "Value6");
357    }
358
359    #[test]
360    fn load_should_override_value_when_key_is_duplicated() {
361        // arrange
362        let args = ["/Key1=Value1", "--Key1=Value2"].iter();
363        let source = CommandLineConfigurationSource::from(args);
364        let mut provider = source.build(&TestConfigurationBuilder);
365
366        // act
367        provider.load().unwrap();
368
369        // assert
370        assert_eq!(provider.get("Key1").unwrap().as_str(), "Value2");
371    }
372
373    #[test]
374    fn load_should_ignore_key_when_value_is_missing() {
375        // arrange
376        let args = ["--Key1", "Value1", "/Key2"].iter();
377        let source = CommandLineConfigurationSource::from(args);
378        let mut provider = source.build(&TestConfigurationBuilder);
379        let mut child_keys = Vec::with_capacity(2);
380
381        // act
382        provider.load().unwrap();
383        provider.child_keys(&mut child_keys, None);
384
385        // assert
386        assert_eq!(child_keys.len(), 1);
387        assert_eq!(provider.get("Key1").unwrap().as_str(), "Value1");
388    }
389
390    #[test]
391    fn load_should_ignore_unrecognizable_argument() {
392        // arrange
393        let args = ["ArgWithoutPrefixAndEqualSign"].iter();
394        let source = CommandLineConfigurationSource::from(args);
395        let mut provider = source.build(&TestConfigurationBuilder);
396        let mut child_keys = Vec::with_capacity(1);
397
398        // act
399        provider.load().unwrap();
400        provider.child_keys(&mut child_keys, None);
401
402        // assert
403        assert!(child_keys.is_empty());
404    }
405
406    #[test]
407    fn load_should_ignore_argument_when_short_switch_is_undefined() {
408        // arrange
409        let args = ["-Key1", "Value1"].iter();
410        let switch_mappings = [("-Key2", "LongKey2")];
411        let source = CommandLineConfigurationSource::new(args, &switch_mappings);
412        let mut provider = source.build(&TestConfigurationBuilder);
413        let mut child_keys = Vec::with_capacity(1);
414
415        // act
416        provider.load().unwrap();
417        provider.child_keys(&mut child_keys, Some(""));
418
419        // assert
420        assert!(child_keys.is_empty());
421    }
422}