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