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