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 rumqttc::v5::MqttOptions;
15use std::any::type_name;
16
17use std::ops::Deref;
18use std::str::FromStr;
19#[cfg(feature = "mobility")]
20use std::sync::RwLock;
21
22use crate::client::configuration::configuration_error::ConfigurationError::{
23    FieldNotFound, MissingMandatoryField, MissingMandatorySection, NoCustomSettings, NoPassword,
24    TypeError,
25};
26use crate::transport::mqtt::configure_transport;
27
28#[cfg(feature = "telemetry")]
29use crate::client::configuration::telemetry_configuration::{
30    TelemetryConfiguration, TELEMETRY_SECTION,
31};
32
33#[cfg(feature = "mobility")]
34use crate::client::configuration::{
35    mobility_configuration::{MobilityConfiguration, STATION_SECTION},
36    node_configuration::{NodeConfiguration, NODE_SECTION},
37};
38
39#[cfg(feature = "geo_routing")]
40use crate::client::configuration::geo_configuration::{GeoConfiguration, GEO_SECTION};
41
42pub(crate) mod bootstrap_configuration;
43pub mod configuration_error;
44#[cfg(feature = "geo_routing")]
45pub mod geo_configuration;
46#[cfg(feature = "mobility")]
47pub mod mobility_configuration;
48#[cfg(feature = "mobility")]
49pub mod node_configuration;
50#[cfg(feature = "telemetry")]
51pub mod telemetry_configuration;
52
53const MQTT_SECTION: &str = "mqtt";
54
55pub struct Configuration {
56    pub mqtt_options: MqttOptions,
57    #[cfg(feature = "geo_routing")]
58    pub geo: GeoConfiguration,
59    #[cfg(feature = "telemetry")]
60    pub telemetry: TelemetryConfiguration,
61    #[cfg(feature = "mobility")]
62    pub mobility: MobilityConfiguration,
63    #[cfg(feature = "mobility")]
64    pub node: Option<RwLock<NodeConfiguration>>,
65    pub(crate) custom_settings: Option<Ini>,
66}
67
68impl Configuration {
69    #[cfg(feature = "mobility")]
70    pub fn component_name(&self, modifier: Option<u32>) -> String {
71        let station_id: String = match &self.node {
72            Some(node_configuration) => node_configuration
73                .read()
74                .unwrap()
75                .station_id(modifier)
76                .to_string(),
77            None => self.mobility.station_id.clone(),
78        };
79        format!("{}_{}", self.mqtt_options.client_id(), station_id)
80    }
81
82    #[cfg(feature = "mobility")]
83    pub fn set_node_configuration(&mut self, node_configuration: NodeConfiguration) {
84        self.node = Some(RwLock::new(node_configuration));
85    }
86
87    pub fn set_mqtt_credentials(&mut self, username: &str, password: &str) {
88        self.mqtt_options.set_credentials(username, password);
89    }
90
91    pub fn get<T: FromStr>(
92        &self,
93        section: Option<&'static str>,
94        key: &'static str,
95    ) -> Result<T, ConfigurationError> {
96        if self.custom_settings.is_some() {
97            match get_optional_field(section, key, self.custom_settings.as_ref().unwrap()) {
98                Ok(result) => {
99                    if let Some(value) = result {
100                        Ok(value)
101                    } else {
102                        Err(FieldNotFound(key))
103                    }
104                }
105                Err(e) => Err(e),
106            }
107        } else {
108            Err(NoCustomSettings)
109        }
110    }
111
112    pub fn set<T: Into<String>>(&mut self, section: Option<&str>, key: &str, value: T) {
113        if self.custom_settings.is_none() {
114            self.custom_settings = Some(Ini::default())
115        }
116        self.custom_settings
117            .as_mut()
118            .unwrap()
119            .with_section(section)
120            .set(key, value);
121    }
122}
123
124// FIXME maybe move this into a dedicated .rs file
125pub(crate) struct MqttOptionWrapper(MqttOptions);
126impl TryFrom<&Properties> for MqttOptionWrapper {
127    type Error = ConfigurationError;
128
129    fn try_from(properties: &Properties) -> Result<Self, Self::Error> {
130        let section = (MQTT_SECTION, properties);
131        let mut mqtt_options = MqttOptions::new(
132            get_mandatory_from_section::<String>("client_id", section)?,
133            get_mandatory_from_section::<String>("host", section)?,
134            get_mandatory_from_section::<u16>("port", section)?,
135        );
136
137        if let Ok(Some(username)) = get_optional_from_section::<String>("username", section.1) {
138            if let Ok(Some(password)) = get_optional_from_section::<String>("password", section.1) {
139                mqtt_options.set_credentials(username, password);
140            } else {
141                return Err(NoPassword);
142            }
143        }
144
145        // TODO manage other optional
146
147        let use_tls = get_optional_from_section::<bool>("use_tls", properties)
148            .unwrap_or_default()
149            .unwrap_or_default();
150        let use_websocket = get_optional_from_section::<bool>("use_websocket", properties)
151            .unwrap_or_default()
152            .unwrap_or_default();
153
154        configure_transport(use_tls, use_websocket, &mut mqtt_options);
155
156        Ok(MqttOptionWrapper(mqtt_options))
157    }
158}
159impl Deref for MqttOptionWrapper {
160    type Target = MqttOptions;
161    fn deref(&self) -> &Self::Target {
162        &self.0
163    }
164}
165
166pub(crate) fn get_optional_field<T: FromStr>(
167    section: Option<&'static str>,
168    field: &'static str,
169    ini_config: &Ini,
170) -> Result<Option<T>, ConfigurationError> {
171    let section = if let Some(section) = ini_config.section(section) {
172        section
173    } else {
174        ini_config.general_section()
175    };
176    get_optional_from_section(field, section)
177}
178
179pub(crate) fn get_optional_from_section<T: FromStr>(
180    field: &'static str,
181    properties: &Properties,
182) -> Result<Option<T>, ConfigurationError> {
183    if let Some(value) = properties.get(field) {
184        match T::from_str(value) {
185            Ok(value) => Ok(Some(value)),
186            Err(_) => Err(TypeError(field, type_name::<T>())),
187        }
188    } else {
189        Ok(None)
190    }
191}
192
193pub(crate) fn get_mandatory_field<T: FromStr>(
194    section: Option<&'static str>,
195    field: &'static str,
196    ini_config: &Ini,
197) -> Result<T, ConfigurationError> {
198    if let Some(properties) = ini_config.section(section) {
199        if let Some(value) = properties.get(field) {
200            match T::from_str(value) {
201                Ok(value) => Ok(value),
202                Err(_e) => Err(TypeError(field, type_name::<T>())),
203            }
204        } else {
205            Err(MissingMandatoryField(field, section.unwrap_or_default()))
206        }
207    } else {
208        Err(MissingMandatorySection(section.unwrap_or_default()))
209    }
210}
211
212pub(crate) fn get_mandatory_from_section<T: FromStr>(
213    field: &'static str,
214    section: (&'static str, &Properties),
215) -> Result<T, ConfigurationError> {
216    match section.1.get(field) {
217        Some(value) => match T::from_str(value) {
218            Ok(value) => Ok(value),
219            Err(_e) => Err(TypeError(field, type_name::<T>())),
220        },
221        None => Err(MissingMandatoryField(field, section.0)),
222    }
223}
224
225pub(crate) fn pick_mandatory_section(
226    section: &'static str,
227    ini_config: &mut Ini,
228) -> Result<Properties, ConfigurationError> {
229    match ini_config.delete(Some(section)) {
230        Some(properties) => Ok(properties),
231        None => Err(MissingMandatorySection(section)),
232    }
233}
234
235impl TryFrom<Ini> for Configuration {
236    type Error = ConfigurationError;
237
238    fn try_from(ini_config: Ini) -> Result<Self, Self::Error> {
239        let mut ini_config = ini_config;
240
241        Ok(Configuration {
242            mqtt_options: MqttOptionWrapper::try_from(&pick_mandatory_section(
243                MQTT_SECTION,
244                &mut ini_config,
245            )?)?
246            .deref()
247            .clone(),
248            #[cfg(feature = "geo_routing")]
249            geo: GeoConfiguration::try_from(&pick_mandatory_section(
250                GEO_SECTION,
251                &mut ini_config,
252            )?)?,
253            #[cfg(feature = "telemetry")]
254            telemetry: TelemetryConfiguration::try_from(&pick_mandatory_section(
255                TELEMETRY_SECTION,
256                &mut ini_config,
257            )?)?,
258            #[cfg(feature = "mobility")]
259            mobility: MobilityConfiguration::try_from(&pick_mandatory_section(
260                STATION_SECTION,
261                &mut ini_config,
262            )?)?,
263            #[cfg(feature = "mobility")]
264            node: match ini_config.section(Some(NODE_SECTION)) {
265                Some(properties) => Some(RwLock::new(NodeConfiguration::try_from(properties)?)),
266                None => None,
267            },
268            custom_settings: Some(ini_config),
269        })
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use crate::client::configuration::{get_optional_field, pick_mandatory_section, Configuration};
276    use ini::Ini;
277
278    #[cfg(feature = "telemetry")]
279    use crate::client::configuration::telemetry_configuration;
280
281    const EXHAUSTIVE_CUSTOM_INI_CONFIG: &str = r#"
282no_section="noitceson"
283
284[station]
285id="com_myapplication"
286type="mec_application"
287
288[mqtt]
289host="localhost"
290port=1883
291client_id="com_myapplication"
292
293[geo]
294prefix=sandbox
295suffix=v2x
296
297[node]
298responsibility_enabled=true
299
300[telemetry]
301host="otlp.domain.com"
302port=5418
303path="/custom/v1/traces"
304
305[custom]
306test="success"
307"#;
308
309    const MINIMAL_FEATURELESS_CONFIGURATION: &str = r#"
310[mqtt]
311host="localhost"
312port=1883
313client_id="com_myapplication"
314"#;
315
316    #[cfg(feature = "mobility")]
317    const MINIMAL_MOBILITY_CONFIGURATION: &str = r#"
318[station]
319id="com_myapplication"
320type="mec_application"
321
322[mqtt]
323host="localhost"
324port=1883
325client_id="com_myapplication"
326"#;
327
328    #[cfg(feature = "mobility")]
329    const MINIMAL_GEO_ROUTING_CONFIGURATION: &str = r#"
330[station]
331id="com_myapplication"
332type="mec_application"
333
334[mqtt]
335host="localhost"
336port=1883
337client_id="com_myapplication"
338
339[geo]
340prefix=sandbox
341suffix=v2x
342"#;
343
344    #[cfg(feature = "telemetry")]
345    const MINIMAL_TELEMETRY_CONFIGURATION: &str = r#"
346[mqtt]
347host="localhost"
348port=1883
349client_id="com_myapplication"
350
351[telemetry]
352host="otlp.domain.com"
353port=5418
354"#;
355
356    #[test]
357    fn custom_settings() {
358        let ini =
359            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
360
361        let configuration = Configuration::try_from(ini).expect("Minimal config should not fail");
362        let no_section = configuration
363            .get::<String>(None, "no_section")
364            .expect("Failed to get field with no section");
365        let custom_test = configuration
366            .get::<String>(Some("custom"), "test")
367            .expect("Failed to get field under custom section");
368
369        assert_eq!(no_section, "noitceson");
370        assert_eq!(custom_test, "success");
371    }
372
373    #[test]
374    fn set_custom_setting() {
375        let ini =
376            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
377        let mut configuration =
378            Configuration::try_from(ini).expect("Minimal config should not fail");
379
380        configuration.set(Some("my_section"), "cool_key", "cool_value");
381        configuration.set(None, "no_section", "updated");
382        let no_section = configuration
383            .get::<String>(None, "no_section")
384            .expect("Failed to get field with no section");
385        let cool_value = configuration
386            .get::<String>(Some("my_section"), "cool_key")
387            .expect("Failed to get field under custom section");
388
389        assert_eq!(no_section, "updated");
390        assert_eq!(cool_value, "cool_value");
391    }
392
393    #[test]
394    fn pick_section() {
395        let mut ini =
396            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
397
398        let s = pick_mandatory_section("mqtt", &mut ini);
399
400        assert!(s.is_ok());
401        assert!(ini.section(Some("mqtt")).is_none());
402    }
403
404    #[test]
405    fn not_set_optional_returns_none() {
406        let ini =
407            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
408
409        let ok_none = get_optional_field::<String>(None, "pmloikjuyh", &ini);
410
411        let none = ok_none.expect("Not set field should return Ok(None)");
412        assert!(none.is_none());
413    }
414
415    #[test]
416    fn optional_no_section_is_ok_some() {
417        let ini =
418            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
419
420        let ok_some = get_optional_field::<String>(None, "no_section", &ini);
421
422        let some = ok_some.expect("Optional field must return Ok(Some(T)): found Err(_)");
423        let value = some.expect("Optional field must return Ok(Some(T)): found OK(None)");
424        assert_eq!(value, "noitceson");
425    }
426
427    #[test]
428    fn optional_from_section_is_ok_some() {
429        let ini =
430            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
431
432        let ok_some = get_optional_field::<String>(Some("custom"), "test", &ini);
433
434        let some = ok_some.expect("Optional field must return Ok(Some(T)): found Err(_)");
435        let value = some.expect("Optional field must return Ok(Some(T)): found OK(None)");
436        assert_eq!(value, "success");
437    }
438
439    #[test]
440    fn optional_wrong_type_is_err() {
441        let ini =
442            Ini::load_from_str(EXHAUSTIVE_CUSTOM_INI_CONFIG).expect("Ini creation should not fail");
443
444        let err = get_optional_field::<u16>(Some("custom"), "test", &ini);
445
446        assert!(err.is_err());
447    }
448
449    #[test]
450    #[cfg_attr(any(feature = "telemetry", feature = "mobility"), should_panic)]
451    fn minimal_featureless_configuration() {
452        let ini = Ini::load_from_str(MINIMAL_FEATURELESS_CONFIGURATION)
453            .expect("Ini creation should not fail");
454
455        let _ = Configuration::try_from(ini)
456            .expect("Failed to create Configuration with minimal mandatory sections and fields");
457    }
458
459    #[test]
460    #[cfg(feature = "telemetry")]
461    #[cfg_attr(feature = "mobility", should_panic)]
462    fn minimal_telemetry_configuration() {
463        let ini = Ini::load_from_str(MINIMAL_TELEMETRY_CONFIGURATION)
464            .expect("Ini creation should not fail");
465
466        let configuration = Configuration::try_from(ini)
467            .expect("Failed to create Configuration with minimal mandatory sections and fields");
468
469        assert_eq!(
470            telemetry_configuration::DEFAULT_PATH.to_string(),
471            configuration.telemetry.path,
472            "Telemetry path must default to {}",
473            telemetry_configuration::DEFAULT_PATH
474        );
475    }
476
477    #[test]
478    #[cfg(feature = "mobility")]
479    #[cfg_attr(any(feature = "telemetry", feature = "geo_routing"), should_panic)]
480    fn minimal_mobility_configuration() {
481        let ini = Ini::load_from_str(MINIMAL_MOBILITY_CONFIGURATION)
482            .expect("Ini creation should not fail");
483
484        let _ = Configuration::try_from(ini)
485            .expect("Failed to create Configuration with minimal mandatory sections and fields");
486    }
487
488    #[test]
489    #[cfg(feature = "geo_routing")]
490    #[cfg_attr(feature = "telemetry", should_panic)]
491    fn minimal_geo_routing_configuration() {
492        let ini = Ini::load_from_str(MINIMAL_GEO_ROUTING_CONFIGURATION)
493            .expect("Ini creation should not fail");
494
495        let _ = Configuration::try_from(ini)
496            .expect("Failed to create Configuration with minimal mandatory sections and fields");
497    }
498}