libits/client/
configuration.rs

1/*
2 * Software Name : libits-client
3 * SPDX-FileCopyrightText: Copyright (c) Orange SA
4 * SPDX-License-Identifier: MIT
5 *
6 * This software is distributed under the MIT license,
7 * see the "LICENSE.txt" file for more details or https://opensource.org/license/MIT/
8 *
9 * Authors: see CONTRIBUTORS.md
10 */
11
12use 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    /// Get a list of values from configuration, separated by commas
95    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()), // Return empty vec if not found
105                },
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        // Handle JSON array format [item1, item2, ...]
162        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('"')) // Remove quotes if present
172            .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    // Tests for get_optional_list functionality
649    #[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    // Tests for get_optional_list functionality with Ini
676    #[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 for set_mqtt_credentials
726    #[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}