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