1use crate::{util::*, ConfigurationBuilder, ConfigurationProvider, ConfigurationSource, LoadResult, Value};
2use std::borrow::Cow;
3use std::collections::HashMap;
4
5pub struct CommandLineConfigurationProvider {
8 data: HashMap<String, (String, Value)>,
9 args: Vec<String>,
10 switch_mappings: HashMap<String, String>,
11}
12
13impl CommandLineConfigurationProvider {
14 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 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(¤t.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#[derive(Default)]
114pub struct CommandLineConfigurationSource {
115 pub switch_mappings: HashMap<String, String>,
118
119 pub args: Vec<String>,
121}
122
123impl CommandLineConfigurationSource {
124 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 pub trait CommandLineConfigurationBuilderExtensions {
178 fn add_command_line(&mut self) -> &mut Self;
180
181 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 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 provider.load().unwrap();
255 provider.child_keys(&mut child_keys, None);
256
257 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 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 provider.load().unwrap();
284 provider.child_keys(&mut child_keys, None);
285
286 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 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 provider.load().unwrap();
314
315 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 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 provider.load().unwrap();
349
350 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 let args = ["/Key1=Value1", "--Key1=Value2"].iter();
363 let source = CommandLineConfigurationSource::from(args);
364 let mut provider = source.build(&TestConfigurationBuilder);
365
366 provider.load().unwrap();
368
369 assert_eq!(provider.get("Key1").unwrap().as_str(), "Value2");
371 }
372
373 #[test]
374 fn load_should_ignore_key_when_value_is_missing() {
375 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 provider.load().unwrap();
383 provider.child_keys(&mut child_keys, None);
384
385 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 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 provider.load().unwrap();
400 provider.child_keys(&mut child_keys, None);
401
402 assert!(child_keys.is_empty());
404 }
405
406 #[test]
407 fn load_should_ignore_argument_when_short_switch_is_undefined() {
408 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 provider.load().unwrap();
417 provider.child_keys(&mut child_keys, Some(""));
418
419 assert!(child_keys.is_empty());
421 }
422}