1use 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
124pub(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 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}