Skip to main content

config/
cmd.rs

1use crate::{pascal_case, Result, Settings};
2use std::{borrow::Cow, collections::HashMap};
3
4/// Represents a [configuration provider](crate::Provider) for command line arguments.
5#[derive(Debug)]
6pub struct Provider {
7    args: Vec<String>,
8    switch_mappings: HashMap<String, String>,
9}
10
11impl Provider {
12    /// Initializes a new command line configuration provider.
13    ///
14    /// # Arguments
15    ///
16    /// * `args` - The command line arguments
17    /// * `switch_mappings` - The mapping of switches to configuration values
18    ///
19    /// # Remarks
20    ///
21    /// Only switch mapping keys that start with `--` or `-` are acceptable. Command line arguments may start with
22    /// `--`, `-`, or `/`.
23    pub fn new<I, V, S>(args: I, switch_mappings: &[(S, S)]) -> Self
24    where
25        I: Iterator<Item = V>,
26        V: AsRef<str>,
27        S: AsRef<str>,
28    {
29        Self {
30            args: args.map(|a| a.as_ref().to_owned()).collect(),
31            switch_mappings: switch_mappings
32                .iter()
33                .filter(|m| m.0.as_ref().starts_with("--") || m.0.as_ref().starts_with('-'))
34                .map(|(k, v)| (k.as_ref().to_uppercase(), v.as_ref().to_owned()))
35                .collect(),
36        }
37    }
38}
39
40impl crate::Provider for Provider {
41    #[inline]
42    fn name(&self) -> &str {
43        "Command Line"
44    }
45
46    fn load(&self, settings: &mut Settings) -> Result {
47        let mut args = self.args.iter();
48
49        while let Some(arg) = args.next() {
50            let mut current = Cow::Borrowed(arg);
51            let start: usize = if arg.starts_with("--") {
52                2
53            } else if arg.starts_with('-') {
54                1
55            } else if arg.starts_with('/') {
56                // "/SomeSwitch" is equivalent to "--SomeSwitch" when interpreting switch mappings
57                let mut temp = arg.clone();
58                temp.replace_range(0..1, "--");
59                current = Cow::Owned(temp);
60                2
61            } else {
62                0
63            };
64
65            let key;
66            let value;
67
68            if let Some(separator) = current.find('=') {
69                let segment: String = current
70                    .chars()
71                    .take(separator)
72                    .map(|c| c.to_ascii_uppercase())
73                    .collect();
74
75                key = if let Some(mapping) = self.switch_mappings.get(&segment) {
76                    mapping.clone()
77                } else if start == 1 {
78                    continue;
79                } else {
80                    current.chars().skip(start).take(separator - start).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 == 1 {
92                    continue;
93                } else {
94                    current.chars().skip(start).collect()
95                };
96
97                let Some(next) = args.next() else {
98                    continue;
99                };
100
101                value = next.clone();
102            }
103
104            settings.insert(pascal_case(&key), value);
105        }
106
107        Ok(())
108    }
109}
110
111impl<I, V> From<I> for Provider
112where
113    I: Iterator<Item = V>,
114    V: AsRef<str>,
115{
116    #[inline]
117    fn from(value: I) -> Self {
118        Self::new(value, &Vec::<(&str, &str)>::new())
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::Provider as _;
126
127    #[test]
128    fn load_should_ignore_unknown_arguments() {
129        // arrange
130        let args = ["foo", "/bar=baz"].iter();
131        let provider = Provider::from(args);
132        let mut settings = Settings::default();
133
134        // act
135        provider.load(&mut settings).unwrap();
136
137        println!("{settings:?}");
138
139        // assert
140        assert_eq!(settings.len(), 1);
141        assert_eq!(settings.get("bar"), Some("baz"));
142    }
143
144    #[test]
145    fn load_should_ignore_arguments_in_the_middle() {
146        // arrange
147        let args = [
148            "Key1=Value1",
149            "--Key2=Value2",
150            "/Key3=Value3",
151            "Bogus1",
152            "--Key4",
153            "Value4",
154            "Bogus2",
155            "/Key5",
156            "Value5",
157            "Bogus3",
158        ]
159        .iter();
160        let provider = Provider::from(args);
161        let mut settings = Settings::default();
162
163        // act
164        provider.load(&mut settings).unwrap();
165
166        // assert
167        assert_eq!(settings.get("Key1"), Some("Value1"));
168        assert_eq!(settings.get("Key2"), Some("Value2"));
169        assert_eq!(settings.get("Key3"), Some("Value3"));
170        assert_eq!(settings.get("Key4"), Some("Value4"));
171        assert_eq!(settings.get("Key5"), Some("Value5"));
172    }
173
174    #[test]
175    fn load_should_process_key_value_pairs_without_mappings() {
176        // arrange
177        let args = [
178            "Key1=Value1",
179            "--Key2=Value2",
180            "/Key3=Value3",
181            "--Key4",
182            "Value4",
183            "/Key5",
184            "Value5",
185            "--single=1",
186            "--two-part=2",
187        ]
188        .iter();
189        let provider = Provider::from(args);
190        let mut settings = Settings::default();
191
192        // act
193        provider.load(&mut settings).unwrap();
194
195        // assert
196        assert_eq!(settings.get("Key1"), Some("Value1"));
197        assert_eq!(settings.get("Key2"), Some("Value2"));
198        assert_eq!(settings.get("Key3"), Some("Value3"));
199        assert_eq!(settings.get("Key4"), Some("Value4"));
200        assert_eq!(settings.get("Key5"), Some("Value5"));
201        assert_eq!(settings.get("Single"), Some("1"));
202        assert_eq!(settings.get("TwoPart"), Some("2"));
203    }
204
205    #[test]
206    fn load_should_process_key_value_pairs_with_mappings() {
207        // arrange
208        let args = [
209            "-K1=Value1",
210            "--Key2=Value2",
211            "/Key3=Value3",
212            "--Key4",
213            "Value4",
214            "/Key5",
215            "Value5",
216            "/Key6=Value6",
217        ]
218        .iter();
219        let switch_mappings = [
220            ("-K1", "LongKey1"),
221            ("--Key2", "SuperLongKey2"),
222            ("--Key6", "SuchALongKey6"),
223        ];
224        let provider = Provider::new(args, &switch_mappings);
225        let mut settings = Settings::default();
226
227        // act
228        provider.load(&mut settings).unwrap();
229
230        // assert
231        assert_eq!(settings.get("LongKey1"), Some("Value1"));
232        assert_eq!(settings.get("SuperLongKey2"), Some("Value2"));
233        assert_eq!(settings.get("Key3"), Some("Value3"));
234        assert_eq!(settings.get("Key4"), Some("Value4"));
235        assert_eq!(settings.get("Key5"), Some("Value5"));
236        assert_eq!(settings.get("SuchALongKey6"), Some("Value6"));
237    }
238
239    #[test]
240    fn load_should_override_value_when_key_is_duplicated() {
241        // arrange
242        let args = ["/Key1=Value1", "--Key1=Value2"].iter();
243        let provider = Provider::from(args);
244        let mut settings = Settings::default();
245
246        // act
247        provider.load(&mut settings).unwrap();
248
249        // assert
250        assert_eq!(settings.get("Key1"), Some("Value2"));
251    }
252
253    #[test]
254    fn load_should_ignore_key_when_value_is_missing() {
255        // arrange
256        let args = ["--Key1", "Value1", "/Key2"].iter();
257        let provider = Provider::from(args);
258        let mut settings = Settings::default();
259
260        // act
261        provider.load(&mut settings).unwrap();
262
263        // assert
264        assert_eq!(settings.len(), 1);
265        assert_eq!(settings.get("Key1"), Some("Value1"));
266    }
267
268    #[test]
269    fn load_should_ignore_unrecognizable_argument() {
270        // arrange
271        let args = ["ArgWithoutPrefixAndEqualSign"].iter();
272        let provider = Provider::from(args);
273        let mut settings = Settings::default();
274
275        // act
276        provider.load(&mut settings).unwrap();
277
278        // assert
279        assert!(settings.is_empty());
280    }
281
282    #[test]
283    fn load_should_ignore_argument_when_short_switch_is_undefined() {
284        // arrange
285        let args = ["-Key1", "Value1"].iter();
286        let switch_mappings = [("-Key2", "LongKey2")];
287        let provider = Provider::new(args, &switch_mappings);
288        let mut settings = Settings::default();
289
290        // act
291        provider.load(&mut settings).unwrap();
292
293        // assert
294        assert!(settings.is_empty());
295    }
296}