1use crate::{
2 util::*, ConfigurationBuilder, ConfigurationProvider, ConfigurationSource, LoadResult, Value,
3};
4use std::borrow::Cow;
5use std::collections::HashMap;
6
7pub struct CommandLineConfigurationProvider {
10 data: HashMap<String, (String, Value)>,
11 args: Vec<String>,
12 switch_mappings: HashMap<String, String>,
13}
14
15impl CommandLineConfigurationProvider {
16 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 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(¤t.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#[derive(Default)]
120pub struct CommandLineConfigurationSource {
121 pub switch_mappings: HashMap<String, String>,
124
125 pub args: Vec<String>,
127}
128
129impl CommandLineConfigurationSource {
130 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 pub trait CommandLineConfigurationBuilderExtensions {
184 fn add_command_line(&mut self) -> &mut Self;
186
187 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 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 provider.load().unwrap();
265 provider.child_keys(&mut child_keys, None);
266
267 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 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 provider.load().unwrap();
294 provider.child_keys(&mut child_keys, None);
295
296 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 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 provider.load().unwrap();
324
325 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 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 provider.load().unwrap();
359
360 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 let args = ["/Key1=Value1", "--Key1=Value2"].iter();
373 let source = CommandLineConfigurationSource::from(args);
374 let mut provider = source.build(&TestConfigurationBuilder);
375
376 provider.load().unwrap();
378
379 assert_eq!(provider.get("Key1").unwrap().as_str(), "Value2");
381 }
382
383 #[test]
384 fn load_should_ignore_key_when_value_is_missing() {
385 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 provider.load().unwrap();
393 provider.child_keys(&mut child_keys, None);
394
395 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 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 provider.load().unwrap();
410 provider.child_keys(&mut child_keys, None);
411
412 assert!(child_keys.is_empty());
414 }
415
416 #[test]
417 fn load_should_ignore_argument_when_short_switch_is_undefined() {
418 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 provider.load().unwrap();
427 provider.child_keys(&mut child_keys, Some(""));
428
429 assert!(child_keys.is_empty());
431 }
432}