config/
cmd.rs

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