Skip to main content

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 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    /// Get a list of values from configuration, separated by commas
89    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()), // Return empty vec if not found
99                },
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        // Handle JSON array format [item1, item2, ...]
156        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('"')) // Remove quotes if present
166            .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    // Tests for get_optional_list functionality
645    #[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    // Tests for get_optional_list functionality with Ini
672    #[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 for set_mqtt_credentials
722    #[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}