1use crate::client::configuration::configuration_error::ConfigurationError;
13use ini::{Ini, Properties};
14use std::any::type_name;
15
16use crate::client::configuration::configuration_error::ConfigurationError::{
17 FieldNotFound, MissingMandatoryField, MissingMandatorySection, NoCustomSettings, TypeError,
18};
19
20use std::str::FromStr;
21
22#[cfg(feature = "telemetry")]
23use crate::client::configuration::telemetry_configuration::{
24 TELEMETRY_SECTION, TelemetryConfiguration,
25};
26
27#[cfg(feature = "mobility")]
28use crate::client::configuration::mobility_configuration::{
29 MOBILITY_SECTION, MobilityConfiguration,
30};
31
32#[cfg(feature = "geo_routing")]
33use crate::client::configuration::geo_configuration::{GEO_SECTION, GeoConfiguration};
34use crate::client::configuration::mqtt_configuration::MqttConfiguration;
35
36pub(crate) mod bootstrap_configuration;
37pub mod configuration_error;
38#[cfg(feature = "geo_routing")]
39pub(crate) mod geo_configuration;
40#[cfg(feature = "mobility")]
41pub(crate) mod mobility_configuration;
42pub mod mqtt_configuration;
43#[cfg(feature = "telemetry")]
44pub(crate) mod telemetry_configuration;
45
46const MQTT_SECTION: &str = "mqtt";
47
48#[derive(Clone, Debug, Default)]
49pub struct Configuration {
50 pub mqtt: MqttConfiguration,
51 #[cfg(feature = "geo_routing")]
52 pub geo: GeoConfiguration,
53 #[cfg(feature = "telemetry")]
54 pub telemetry: TelemetryConfiguration,
55 #[cfg(feature = "mobility")]
56 pub mobility: MobilityConfiguration,
57 pub custom_settings: Option<Ini>,
58}
59
60impl Configuration {
61 pub fn set_mqtt_credentials(&mut self, username: &str, password: &str) {
62 self.mqtt.mqtt_options.set_credentials(username, password);
63 }
64
65 pub fn get<T: FromStr>(
66 &self,
67 section: Option<&'static str>,
68 key: &'static str,
69 ) -> Result<T, ConfigurationError> {
70 if self.custom_settings.is_some() {
71 match get_optional(section, key, self.custom_settings.as_ref().unwrap()) {
72 Ok(result) => match result {
73 Some(value) => Ok(value),
74 _ => Err(FieldNotFound(key)),
75 },
76 Err(e) => Err(e),
77 }
78 } else {
79 Err(NoCustomSettings)
80 }
81 }
82
83 pub fn set<T: Into<String>>(&mut self, section: Option<&str>, key: &str, value: T) {
84 if self.custom_settings.is_none() {
85 self.custom_settings = Some(Ini::default())
86 }
87 self.custom_settings
88 .as_mut()
89 .unwrap()
90 .with_section(section)
91 .set(key, value);
92 }
93
94 pub fn get_list<T: FromStr>(
96 &self,
97 section: Option<&'static str>,
98 key: &'static str,
99 ) -> Result<Vec<T>, ConfigurationError> {
100 if self.custom_settings.is_some() {
101 match get_optional_list(section, key, self.custom_settings.as_ref().unwrap()) {
102 Ok(result) => match result {
103 Some(values) => Ok(values),
104 None => Ok(Vec::new()), },
106 Err(e) => Err(e),
107 }
108 } else {
109 Err(NoCustomSettings)
110 }
111 }
112}
113
114pub(crate) fn get_optional<T: FromStr>(
115 section: Option<&'static str>,
116 field: &'static str,
117 ini_config: &Ini,
118) -> Result<Option<T>, ConfigurationError> {
119 let properties = if let Some(properties) = ini_config.section(section) {
120 properties
121 } else {
122 ini_config.general_section()
123 };
124 get_optional_from_properties(field, properties)
125}
126
127pub fn get_optional_from_properties<T: FromStr>(
128 field: &'static str,
129 properties: &Properties,
130) -> Result<Option<T>, ConfigurationError> {
131 if let Some(value) = properties.get(field) {
132 match T::from_str(value) {
133 Ok(value) => Ok(Some(value)),
134 Err(_) => Err(TypeError(field, type_name::<T>())),
135 }
136 } else {
137 Ok(None)
138 }
139}
140
141pub(crate) fn get_optional_list<T: FromStr>(
142 section: Option<&'static str>,
143 field: &'static str,
144 ini_config: &Ini,
145) -> Result<Option<Vec<T>>, ConfigurationError> {
146 let properties = if let Some(properties) = ini_config.section(section) {
147 properties
148 } else {
149 ini_config.general_section()
150 };
151 get_optional_list_from_properties(field, properties)
152}
153
154pub fn get_optional_list_from_properties<T: FromStr>(
155 field: &'static str,
156 properties: &Properties,
157) -> Result<Option<Vec<T>>, ConfigurationError> {
158 if let Some(value) = properties.get(field) {
159 let cleaned_value = value.trim();
160
161 let items_str = if cleaned_value.starts_with('[') && cleaned_value.ends_with(']') {
163 &cleaned_value[1..cleaned_value.len() - 1]
164 } else {
165 cleaned_value
166 };
167
168 let parsed_values: Result<Vec<T>, _> = items_str
169 .split(',')
170 .map(|s| s.trim())
171 .map(|s| s.trim_matches('"')) .filter(|s| !s.is_empty())
173 .map(|item| T::from_str(item))
174 .collect();
175
176 match parsed_values {
177 Ok(values) => Ok(Some(values)),
178 Err(_) => Err(TypeError(field, type_name::<T>())),
179 }
180 } else {
181 Ok(None)
182 }
183}
184
185pub fn get_mandatory<T: FromStr>(
186 section: Option<&'static str>,
187 field: &'static str,
188 ini_config: &Ini,
189) -> Result<T, ConfigurationError> {
190 let properties = if let Some(properties) = ini_config.section(section) {
191 properties
192 } else {
193 ini_config.general_section()
194 };
195 get_mandatory_from_properties(field, properties)
196}
197
198pub(crate) fn get_mandatory_from_properties<T: FromStr>(
199 field: &'static str,
200 properties: &Properties,
201) -> Result<T, ConfigurationError> {
202 match properties.get(field) {
203 Some(value) => match T::from_str(value) {
204 Ok(value) => Ok(value),
205 Err(_e) => Err(TypeError(field, type_name::<T>())),
206 },
207 None => Err(MissingMandatoryField(field)),
208 }
209}
210
211pub(crate) fn pick_mandatory_section(
212 section: &'static str,
213 ini_config: &mut Ini,
214) -> Result<Properties, ConfigurationError> {
215 match ini_config.delete(Some(section)) {
216 Some(properties) => Ok(properties),
217 None => Err(MissingMandatorySection(section)),
218 }
219}
220
221impl TryFrom<Ini> for Configuration {
222 type Error = ConfigurationError;
223
224 fn try_from(ini_config: Ini) -> Result<Self, Self::Error> {
225 let mut ini_config = ini_config;
226
227 Ok(Configuration {
228 mqtt: MqttConfiguration::try_from(&pick_mandatory_section(
229 MQTT_SECTION,
230 &mut ini_config,
231 )?)?,
232 #[cfg(feature = "geo_routing")]
233 geo: GeoConfiguration::try_from(&pick_mandatory_section(
234 GEO_SECTION,
235 &mut ini_config,
236 )?)?,
237 #[cfg(feature = "telemetry")]
238 telemetry: TelemetryConfiguration::try_from(&pick_mandatory_section(
239 TELEMETRY_SECTION,
240 &mut ini_config,
241 )?)?,
242 #[cfg(feature = "mobility")]
243 mobility: MobilityConfiguration::try_from(&pick_mandatory_section(
244 MOBILITY_SECTION,
245 &mut ini_config,
246 )?)?,
247 custom_settings: Some(ini_config),
248 })
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::client::configuration::{
256 Configuration, get_mandatory, get_optional, pick_mandatory_section,
257 };
258 use ini::Ini;
259
260 const EXHAUSTIVE_CUSTOM_INI_CONFIG: &str = r#"
261no_section = noitceson
262
263[mqtt]
264host = localhost
265port = 1883
266use_tls = false
267use_websocket = false
268client_id = com_myapplication
269username = username
270password = password
271
272[geo]
273prefix = sandbox
274suffix = v2x
275
276[mobility]
277source_uuid = com_myapplication-1
278station_id = 1
279use_responsibility = true
280thread_count = 4
281
282[telemetry]
283host = otlp.domain.com
284port = 5418
285use_tls = false
286path = /custom/v1/traces
287max_batch_size = 10
288username = username
289password = password
290
291[custom]
292test = success
293"#;
294
295 const MINIMAL_FEATURELESS_CONFIGURATION: &str = r#"
296[mqtt]
297host = localhost
298port = 1883
299use_tls = false
300use_websocket = false
301client_id = com_myapplication
302"#;
303
304 #[cfg(feature = "mobility")]
305 const MINIMAL_MOBILITY_CONFIGURATION: &str = r#"
306[mqtt]
307host = localhost
308port = 1883
309use_tls = false
310use_websocket = false
311client_id = com_myapplication
312
313[mobility]
314source_uuid = com_myapplication-1
315station_id = 1
316use_responsibility = false
317thread_count = 4
318"#;
319
320 #[cfg(feature = "mobility")]
321 const MINIMAL_GEO_ROUTING_CONFIGURATION: &str = r#"
322[mqtt]
323host = localhost
324port=1883
325use_tls = false
326use_websocket = false
327client_id= com_myapplication
328
329[mobility]
330source_uuid = com_myapplication-1
331station_id = 1
332use_responsibility = false
333thread_count = 4
334
335[geo]
336prefix = sandbox
337suffix = v2x
338"#;
339
340 #[cfg(feature = "telemetry")]
341 const MINIMAL_TELEMETRY_CONFIGURATION: &str = r#"
342[mqtt]
343host = localhost
344port = 1883
345use_tls = false
346use_websocket = false
347client_id = com_myapplication
348
349[telemetry]
350host = otlp.domain.com
351port = 5418
352use_tls = false
353"#;
354
355 #[test]
356 fn custom_settings() {
357 let ini =
358 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
359
360 let configuration = Configuration::try_from(ini).expect("Minimal config should not fail");
361 let no_section = configuration
362 .get::<String>(None, "no_section")
363 .expect("Failed to get field with no section");
364 let custom_test = configuration
365 .get::<String>(Some("custom"), "test")
366 .expect("Failed to get field under custom section");
367
368 assert_eq!(no_section, "noitceson");
369 assert_eq!(custom_test, "success");
370 }
371
372 #[test]
373 fn set_custom_setting() {
374 let ini =
375 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
376 let mut configuration =
377 Configuration::try_from(ini).expect("Minimal config should not fail");
378
379 configuration.set(Some("my_section"), "cool_key", "cool_value");
380 configuration.set(None, "no_section", "updated");
381 let no_section = configuration
382 .get::<String>(None, "no_section")
383 .expect("Failed to get field with no section");
384 let cool_value = configuration
385 .get::<String>(Some("my_section"), "cool_key")
386 .expect("Failed to get field under custom section");
387
388 assert_eq!(no_section, "updated");
389 assert_eq!(cool_value, "cool_value");
390 }
391
392 #[test]
393 fn pick_section() {
394 let mut ini =
395 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
396
397 let s = pick_mandatory_section("mqtt", &mut ini);
398
399 assert!(s.is_ok());
400 assert!(ini.section(Some("mqtt")).is_none());
401 }
402
403 #[test]
404 fn not_set_optional_returns_none() {
405 let ini =
406 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
407
408 let ok_none = get_optional::<String>(None, "pmloikjuyh", &ini);
409
410 let none = ok_none.expect("Not set field should return Ok(None)");
411 assert!(none.is_none());
412 }
413
414 #[test]
415 fn optional_no_section_is_ok_some() {
416 let ini =
417 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
418
419 let ok_some = get_optional::<String>(None, "no_section", &ini);
420
421 let some = ok_some.expect("Optional field must return Ok(Some(T)): found Err(_)");
422 let value = some.expect("Optional field must return Ok(Some(T)): found OK(None)");
423 assert_eq!(value, "noitceson");
424 }
425
426 #[test]
427 fn optional_from_section_is_ok_some() {
428 let ini =
429 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
430
431 let ok_some = get_optional::<String>(Some("custom"), "test", &ini);
432
433 let some = ok_some.expect("Optional field must return Ok(Some(T)): found Err(_)");
434 let value = some.expect("Optional field must return Ok(Some(T)): found OK(None)");
435 assert_eq!(value, "success");
436 }
437
438 #[test]
439 fn optional_wrong_type_is_err() {
440 let ini =
441 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
442
443 let err = get_optional::<u16>(Some("custom"), "test", &ini);
444
445 assert!(err.is_err());
446 }
447
448 #[test]
449 #[cfg_attr(any(feature = "telemetry", feature = "mobility"), should_panic)]
450 fn minimal_featureless_configuration() {
451 let ini = Ini::load_from_str(MINIMAL_FEATURELESS_CONFIGURATION)
452 .expect("Ini creation should not fail");
453
454 let _ = Configuration::try_from(ini)
455 .expect("Failed to create Configuration with minimal mandatory sections and fields");
456 }
457
458 #[test]
459 #[cfg(feature = "telemetry")]
460 #[cfg_attr(feature = "mobility", should_panic)]
461 fn minimal_telemetry_configuration() {
462 let ini = Ini::load_from_str(MINIMAL_TELEMETRY_CONFIGURATION)
463 .expect("Ini creation should not fail");
464
465 Configuration::try_from(ini)
466 .expect("Failed to create Configuration with minimal mandatory sections and fields");
467 }
468
469 #[test]
470 #[cfg(feature = "mobility")]
471 #[cfg_attr(any(feature = "telemetry", feature = "geo_routing"), should_panic)]
472 fn minimal_mobility_configuration() {
473 let ini = Ini::load_from_str(MINIMAL_MOBILITY_CONFIGURATION)
474 .expect("Ini creation should not fail");
475
476 Configuration::try_from(ini)
477 .expect("Failed to create Configuration with minimal mandatory sections and fields");
478 }
479
480 #[test]
481 #[cfg(feature = "geo_routing")]
482 #[cfg_attr(feature = "telemetry", should_panic)]
483 fn minimal_geo_routing_configuration() {
484 let ini = Ini::load_from_str(MINIMAL_GEO_ROUTING_CONFIGURATION)
485 .expect("Ini creation should not fail");
486
487 Configuration::try_from(ini)
488 .expect("Failed to create Configuration with minimal mandatory sections and fields");
489 }
490
491 #[test]
492 fn mandatory_no_section_is_ok() {
493 let ini =
494 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
495
496 let result = get_mandatory::<String>(None, "no_section", &ini);
497
498 assert!(result.is_ok());
499 assert_eq!(result.unwrap(), "noitceson");
500 }
501
502 #[test]
503 fn mandatory_from_section_is_ok() {
504 let ini =
505 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
506
507 let result = get_mandatory::<String>(Some("custom"), "test", &ini);
508
509 assert!(result.is_ok());
510 assert_eq!(result.unwrap(), "success");
511 }
512
513 #[test]
514 fn mandatory_missing_field_is_err() {
515 let ini =
516 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
517
518 let result = get_mandatory::<String>(None, "non_existent", &ini);
519
520 assert!(result.is_err());
521 }
522
523 #[test]
524 fn mandatory_wrong_type_is_err() {
525 let ini =
526 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
527
528 let result = get_mandatory::<u16>(Some("custom"), "test", &ini);
529
530 assert!(result.is_err());
531 }
532
533 #[test]
534 fn optional_missing_section_returns_none() {
535 let ini =
536 Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
537
538 let result = get_optional::<String>(Some("non_existent_section"), "field", &ini);
539
540 assert!(result.is_ok());
541 assert!(result.unwrap().is_none());
542 }
543
544 #[test]
545 fn get_mandatory_ok() {
546 let ini = Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).unwrap();
547 let value = get_mandatory::<String>(Some("custom"), "test", &ini).unwrap();
548 assert_eq!(value, "success");
549 }
550
551 #[test]
552 fn get_mandatory_missing_section() {
553 let ini = Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).unwrap();
554 let result = get_mandatory::<String>(Some("missing"), "test", &ini);
555 assert!(result.is_err());
556 }
557
558 #[test]
559 fn get_mandatory_missing_field() {
560 let ini = Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).unwrap();
561 let result = get_mandatory::<String>(Some("custom"), "missing", &ini);
562 assert!(result.is_err());
563 }
564
565 #[test]
566 fn get_mandatory_type_error() {
567 let ini = Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).unwrap();
568 let result = get_mandatory::<u16>(Some("custom"), "test", &ini);
569 assert!(result.is_err());
570 }
571
572 #[test]
573 fn get_optional_ok() {
574 let ini = Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).unwrap();
575 let value = get_optional::<String>(Some("custom"), "test", &ini)
576 .unwrap()
577 .unwrap();
578 assert_eq!(value, "success");
579 }
580
581 #[test]
582 fn get_optional_missing_section() {
583 let ini = Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).unwrap();
584 let result = get_optional::<String>(Some("missing"), "test", &ini).unwrap();
585 assert!(result.is_none());
586 }
587
588 #[test]
589 fn get_optional_missing_field() {
590 let ini = Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).unwrap();
591 let result = get_optional::<String>(Some("custom"), "missing", &ini).unwrap();
592 assert!(result.is_none());
593 }
594
595 #[test]
596 fn get_optional_type_error() {
597 let ini = Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).unwrap();
598 let result = get_optional::<u16>(Some("custom"), "test", &ini);
599 assert!(result.is_err());
600 }
601
602 #[test]
603 fn get_list_with_comma_separated_values() {
604 let mut configuration = Configuration::default();
605 configuration.set(Some("test"), "list_field", "item1,item2,item3");
606 let result = configuration
607 .get_list::<String>(Some("test"), "list_field")
608 .unwrap();
609 assert_eq!(result, vec!["item1", "item2", "item3"]);
610 }
611
612 #[test]
613 fn get_list_no_custom_settings_error() {
614 let mut configuration = Configuration::default();
615 configuration.custom_settings = None;
616 let result = configuration.get_list::<String>(Some("test"), "list_field");
617 assert!(matches!(result, Err(NoCustomSettings)));
618 }
619
620 #[test]
621 fn get_list_with_spaces_and_trimming() {
622 let mut configuration = Configuration::default();
623 configuration.set(Some("test"), "list_field", " item1 , item2 , item3 ");
624 let result = configuration
625 .get_list::<String>(Some("test"), "list_field")
626 .unwrap();
627 assert_eq!(result, vec!["item1", "item2", "item3"]);
628 }
629
630 #[test]
631 fn get_list_with_empty_items_filtered() {
632 let mut configuration = Configuration::default();
633 configuration.set(Some("test"), "list_field", "item1,,item3,");
634 let result = configuration
635 .get_list::<String>(Some("test"), "list_field")
636 .unwrap();
637 assert_eq!(result, vec!["item1", "item3"]);
638 }
639
640 #[test]
641 fn get_list_type_conversion_error() {
642 let mut configuration = Configuration::default();
643 configuration.set(Some("test"), "list_field", "not_a_number,123");
644 let result = configuration.get_list::<u32>(Some("test"), "list_field");
645 assert!(result.is_err());
646 }
647
648 #[test]
650 fn get_optional_list_ok() {
651 let mut properties = ini::Properties::new();
652 properties.insert("test_list", "a,b,c".to_string());
653 let result = get_optional_list_from_properties::<String>("test_list", &properties).unwrap();
654 assert_eq!(
655 result,
656 Some(vec!["a".to_string(), "b".to_string(), "c".to_string()])
657 );
658 }
659
660 #[test]
661 fn get_optional_list_missing_field() {
662 let properties = ini::Properties::new();
663 let result = get_optional_list_from_properties::<String>("missing", &properties).unwrap();
664 assert!(result.is_none());
665 }
666
667 #[test]
668 fn get_optional_list_type_error() {
669 let mut properties = ini::Properties::new();
670 properties.insert("test_list", "not_a_number,123".to_string());
671 let result = get_optional_list_from_properties::<u32>("test_list", &properties);
672 assert!(result.is_err());
673 }
674
675 #[test]
677 fn get_optional_list_from_ini_ok() {
678 let ini_str = r#"
679 [test_section]
680 list_field = item1,item2,item3
681 "#;
682 let ini = Ini::load_from_str(ini_str).unwrap();
683 let result = get_optional_list::<String>(Some("test_section"), "list_field", &ini).unwrap();
684 assert_eq!(
685 result,
686 Some(vec![
687 "item1".to_string(),
688 "item2".to_string(),
689 "item3".to_string()
690 ])
691 );
692 }
693
694 #[test]
695 fn get_optional_list_from_ini_missing_section() {
696 let ini_str = r#"
697 [other_section]
698 list_field = item1,item2,item3
699 "#;
700 let ini = Ini::load_from_str(ini_str).unwrap();
701 let result =
702 get_optional_list::<String>(Some("missing_section"), "list_field", &ini).unwrap();
703 assert!(result.is_none());
704 }
705
706 #[test]
707 fn get_optional_list_from_general_section() {
708 let ini_str = r#"
709 list_field = item1,item2,item3
710 [other_section]
711 other = value
712 "#;
713 let ini = Ini::load_from_str(ini_str).unwrap();
714 let result = get_optional_list::<String>(None, "list_field", &ini).unwrap();
715 assert_eq!(
716 result,
717 Some(vec![
718 "item1".to_string(),
719 "item2".to_string(),
720 "item3".to_string()
721 ])
722 );
723 }
724
725 #[test]
727 fn set_mqtt_credentials_test() {
728 let mut configuration = Configuration::default();
729 configuration.set_mqtt_credentials("testuser", "testpass");
730 assert_eq!(
731 configuration.mqtt.mqtt_options.credentials(),
732 Some(("testuser".to_string(), "testpass".to_string()))
733 );
734 }
735}