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(crate) 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(crate) 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_field(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
95pub(crate) fn get_optional_field<T: FromStr>(
96    section: Option<&'static str>,
97    field: &'static str,
98    ini_config: &Ini,
99) -> Result<Option<T>, ConfigurationError> {
100    let section = if let Some(section) = ini_config.section(section) {
101        section
102    } else {
103        ini_config.general_section()
104    };
105    get_optional_from_section(field, section)
106}
107
108pub(crate) fn get_optional_from_section<T: FromStr>(
109    field: &'static str,
110    properties: &Properties,
111) -> Result<Option<T>, ConfigurationError> {
112    if let Some(value) = properties.get(field) {
113        match T::from_str(value) {
114            Ok(value) => Ok(Some(value)),
115            Err(_) => Err(TypeError(field, type_name::<T>())),
116        }
117    } else {
118        Ok(None)
119    }
120}
121
122pub(crate) fn get_mandatory_from_section<T: FromStr>(
123    field: &'static str,
124    section: (&'static str, &Properties),
125) -> Result<T, ConfigurationError> {
126    match section.1.get(field) {
127        Some(value) => match T::from_str(value) {
128            Ok(value) => Ok(value),
129            Err(_e) => Err(TypeError(field, type_name::<T>())),
130        },
131        None => Err(MissingMandatoryField(field, section.0)),
132    }
133}
134
135pub(crate) fn pick_mandatory_section(
136    section: &'static str,
137    ini_config: &mut Ini,
138) -> Result<Properties, ConfigurationError> {
139    match ini_config.delete(Some(section)) {
140        Some(properties) => Ok(properties),
141        None => Err(MissingMandatorySection(section)),
142    }
143}
144
145impl TryFrom<Ini> for Configuration {
146    type Error = ConfigurationError;
147
148    fn try_from(ini_config: Ini) -> Result<Self, Self::Error> {
149        let mut ini_config = ini_config;
150
151        Ok(Configuration {
152            mqtt: MqttConfiguration::try_from(&pick_mandatory_section(
153                MQTT_SECTION,
154                &mut ini_config,
155            )?)?,
156            #[cfg(feature = "geo_routing")]
157            geo: GeoConfiguration::try_from(&pick_mandatory_section(
158                GEO_SECTION,
159                &mut ini_config,
160            )?)?,
161            #[cfg(feature = "telemetry")]
162            telemetry: TelemetryConfiguration::try_from(&pick_mandatory_section(
163                TELEMETRY_SECTION,
164                &mut ini_config,
165            )?)?,
166            #[cfg(feature = "mobility")]
167            mobility: MobilityConfiguration::try_from(&pick_mandatory_section(
168                MOBILITY_SECTION,
169                &mut ini_config,
170            )?)?,
171            custom_settings: Some(ini_config),
172        })
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use crate::client::configuration::{Configuration, get_optional_field, pick_mandatory_section};
179    use ini::Ini;
180
181    const EXHAUSTIVE_CUSTOM_INI_CONFIG: &str = r#"
182no_section = noitceson
183
184[mqtt]
185host = localhost
186port = 1883
187use_tls = false
188use_websocket = false
189client_id = com_myapplication
190username = username
191password = password
192
193[geo]
194prefix = sandbox
195suffix = v2x
196
197[mobility]
198source_uuid = com_myapplication-1
199station_id = 1
200use_responsibility = true
201thread_count = 4
202
203[telemetry]
204host = otlp.domain.com
205port = 5418
206use_tls = false
207path = /custom/v1/traces
208max_batch_size = 10
209username = username
210password = password
211
212[custom]
213test = success
214"#;
215
216    const MINIMAL_FEATURELESS_CONFIGURATION: &str = r#"
217[mqtt]
218host = localhost
219port = 1883
220use_tls = false
221use_websocket = false
222client_id = com_myapplication
223"#;
224
225    #[cfg(feature = "mobility")]
226    const MINIMAL_MOBILITY_CONFIGURATION: &str = r#"
227[mqtt]
228host = localhost
229port = 1883
230use_tls = false
231use_websocket = false
232client_id = com_myapplication
233
234[mobility]
235source_uuid = com_myapplication-1
236station_id = 1
237use_responsibility = false
238thread_count = 4
239"#;
240
241    #[cfg(feature = "mobility")]
242    const MINIMAL_GEO_ROUTING_CONFIGURATION: &str = r#"
243[mqtt]
244host = localhost
245port=1883
246use_tls = false
247use_websocket = false
248client_id= com_myapplication
249
250[mobility]
251source_uuid = com_myapplication-1
252station_id = 1
253use_responsibility = false
254thread_count = 4
255
256[geo]
257prefix = sandbox
258suffix = v2x
259"#;
260
261    #[cfg(feature = "telemetry")]
262    const MINIMAL_TELEMETRY_CONFIGURATION: &str = r#"
263[mqtt]
264host = localhost
265port = 1883
266use_tls = false
267use_websocket = false
268client_id = com_myapplication
269
270[telemetry]
271host = otlp.domain.com
272port = 5418
273use_tls = false
274"#;
275
276    #[test]
277    fn custom_settings() {
278        let ini =
279            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
280
281        let configuration = Configuration::try_from(ini).expect("Minimal config should not fail");
282        let no_section = configuration
283            .get::<String>(None, "no_section")
284            .expect("Failed to get field with no section");
285        let custom_test = configuration
286            .get::<String>(Some("custom"), "test")
287            .expect("Failed to get field under custom section");
288
289        assert_eq!(no_section, "noitceson");
290        assert_eq!(custom_test, "success");
291    }
292
293    #[test]
294    fn set_custom_setting() {
295        let ini =
296            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
297        let mut configuration =
298            Configuration::try_from(ini).expect("Minimal config should not fail");
299
300        configuration.set(Some("my_section"), "cool_key", "cool_value");
301        configuration.set(None, "no_section", "updated");
302        let no_section = configuration
303            .get::<String>(None, "no_section")
304            .expect("Failed to get field with no section");
305        let cool_value = configuration
306            .get::<String>(Some("my_section"), "cool_key")
307            .expect("Failed to get field under custom section");
308
309        assert_eq!(no_section, "updated");
310        assert_eq!(cool_value, "cool_value");
311    }
312
313    #[test]
314    fn pick_section() {
315        let mut ini =
316            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
317
318        let s = pick_mandatory_section("mqtt", &mut ini);
319
320        assert!(s.is_ok());
321        assert!(ini.section(Some("mqtt")).is_none());
322    }
323
324    #[test]
325    fn not_set_optional_returns_none() {
326        let ini =
327            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
328
329        let ok_none = get_optional_field::<String>(None, "pmloikjuyh", &ini);
330
331        let none = ok_none.expect("Not set field should return Ok(None)");
332        assert!(none.is_none());
333    }
334
335    #[test]
336    fn optional_no_section_is_ok_some() {
337        let ini =
338            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
339
340        let ok_some = get_optional_field::<String>(None, "no_section", &ini);
341
342        let some = ok_some.expect("Optional field must return Ok(Some(T)): found Err(_)");
343        let value = some.expect("Optional field must return Ok(Some(T)): found OK(None)");
344        assert_eq!(value, "noitceson");
345    }
346
347    #[test]
348    fn optional_from_section_is_ok_some() {
349        let ini =
350            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
351
352        let ok_some = get_optional_field::<String>(Some("custom"), "test", &ini);
353
354        let some = ok_some.expect("Optional field must return Ok(Some(T)): found Err(_)");
355        let value = some.expect("Optional field must return Ok(Some(T)): found OK(None)");
356        assert_eq!(value, "success");
357    }
358
359    #[test]
360    fn optional_wrong_type_is_err() {
361        let ini =
362            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
363
364        let err = get_optional_field::<u16>(Some("custom"), "test", &ini);
365
366        assert!(err.is_err());
367    }
368
369    #[test]
370    #[cfg_attr(any(feature = "telemetry", feature = "mobility"), should_panic)]
371    fn minimal_featureless_configuration() {
372        let ini = Ini::load_from_str(MINIMAL_FEATURELESS_CONFIGURATION)
373            .expect("Ini creation should not fail");
374
375        let _ = Configuration::try_from(ini)
376            .expect("Failed to create Configuration with minimal mandatory sections and fields");
377    }
378
379    #[test]
380    #[cfg(feature = "telemetry")]
381    #[cfg_attr(feature = "mobility", should_panic)]
382    fn minimal_telemetry_configuration() {
383        let ini = Ini::load_from_str(MINIMAL_TELEMETRY_CONFIGURATION)
384            .expect("Ini creation should not fail");
385
386        Configuration::try_from(ini)
387            .expect("Failed to create Configuration with minimal mandatory sections and fields");
388    }
389
390    #[test]
391    #[cfg(feature = "mobility")]
392    #[cfg_attr(any(feature = "telemetry", feature = "geo_routing"), should_panic)]
393    fn minimal_mobility_configuration() {
394        let ini = Ini::load_from_str(MINIMAL_MOBILITY_CONFIGURATION)
395            .expect("Ini creation should not fail");
396
397        Configuration::try_from(ini)
398            .expect("Failed to create Configuration with minimal mandatory sections and fields");
399    }
400
401    #[test]
402    #[cfg(feature = "geo_routing")]
403    #[cfg_attr(feature = "telemetry", should_panic)]
404    fn minimal_geo_routing_configuration() {
405        let ini = Ini::load_from_str(MINIMAL_GEO_ROUTING_CONFIGURATION)
406            .expect("Ini creation should not fail");
407
408        Configuration::try_from(ini)
409            .expect("Failed to create Configuration with minimal mandatory sections and fields");
410    }
411}