1use 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}