dimas_config/
config.rs

1// Copyright © 2024 Stephan Kunz
2
3//! The configuration data.
4//!
5//! An Agents configuration can be defined using json5 formated files.
6//! There is a set of read methods for predefined filenames available.
7//! You can find some example files [here](https://github.com/dimas-fw/dimas/tree/main/.config)
8//!
9//! # Examples
10//! ```rust,no_run
11//! # use dimas_config::Config;
12//! # extern crate std;
13//! # fn main() -> Result<(), Box<dyn core::error::Error + Send + Sync + 'static>> {
14//! // in no-std environment:
15//! // creates a default configuration
16//! // in std environment:
17//! // creates a configuration from a file named `default.json5`
18//! // located in one of the directories listed below.
19//! // If that file does not exist, a default config will be created
20//! let config = Config::default();
21//!
22//! // use file named `filename.json5` (needs std environment)
23//! // returns an error if file does not exist or is no valid configuration file
24//! let config = Config::from_file("filename.json5")?;
25//!
26//! // methods with predefined filenames working like Config::from_file(...) (needs std environment)
27//! let config = Config::local()?;        // use file named `local.json5`
28//! let config = Config::peer()?;         // use file named `peer.json5`
29//! let config = Config::client()?;       // use file named `client.json5`
30//! let config = Config::router()?;       // use file named `router.json5`
31//!
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! The methods using files will search in following directories for the file (order first to last):
37//!  - current working directory
38//!  - `.config` directory below current working directory
39//!  - `.config` directory below home directory
40//!  - local config directory (`Linux`: `$XDG_CONFIG_HOME/dimas` or `$HOME/.config/dimas` | `Windows`: `{FOLDERID_LocalAppData}/dimas` | `MacOS`: `$HOME/Library/Application Support/dimas`)
41//!  - config directory (`Linux`: `$XDG_CONFIG_HOME/dimas` or `$HOME/.config/dimas` | `Windows`: `{FOLDERID_RoamingAppData}/dimas` | `MacOS`: `$HOME/Library/Application Support/dimas`)
42//!
43
44#[doc(hidden)]
45extern crate alloc;
46
47#[cfg(feature = "std")]
48extern crate std;
49
50// region:		--- modules
51use crate::utils::{find_config_file, read_config_file};
52use crate::Result;
53use alloc::vec::Vec;
54#[cfg(feature = "std")]
55use tracing::{debug, warn};
56// endregion:	--- modules
57
58// region:		--- Session
59#[derive(Debug, Default, Clone, serde::Deserialize)]
60pub struct Session {
61	pub protocol: alloc::string::String,
62	pub name: alloc::string::String,
63	#[serde(deserialize_with = "zenoh::Config::deserialize")]
64	pub config: zenoh::Config,
65}
66// endregion:	--- Session
67
68// region:		--- Config
69/// Manages the configuration
70//#[repr(transparent)]
71#[derive(Debug, Clone, serde::Deserialize)]
72pub struct Config {
73	#[serde(deserialize_with = "zenoh::Config::deserialize")]
74	zenoh: zenoh::Config,
75	sessions: Option<Vec<Session>>,
76}
77
78#[cfg(not(feature = "std"))]
79impl Default for Config {
80	/// Create a default configuration
81	fn default() -> Self {
82		Self {
83			zenoh: zenoh::Config::default(),
84		}
85	}
86}
87
88#[cfg(feature = "std")]
89impl Default for Config {
90	/// Create a default configuration
91	///
92	/// Will search for a configuration file with name "default.json5" in the directories mentioned in [`Examples`](index.html#examples).
93	/// This file should contain the wanted default configuration.
94	/// If no file is found, it will create a defined minimal default configuration.
95	/// Currently this is just a default zenoh peer configuration which connects to peers in same subnet.
96	#[allow(clippy::cognitive_complexity)]
97	fn default() -> Self {
98		match find_config_file("default.json5") {
99			Ok(path) => {
100				debug!("trying file {:?}", &path);
101				match read_config_file(&path) {
102					Ok(content) => match json5::from_str(&content) {
103						Ok(result) => result,
104						Err(error) => {
105							warn!("{}, using default dimas configuration instead", error);
106							Self {
107								zenoh: zenoh::Config::default(),
108								sessions: None,
109							}
110						}
111					},
112					Err(error) => {
113						warn!("{}, using default dimas configuration instead", error);
114						Self {
115							zenoh: zenoh::Config::default(),
116							sessions: None,
117						}
118					}
119				}
120			}
121			Err(error) => {
122				warn!("{}, using default dimas configuration instead", error);
123				Self {
124					zenoh: zenoh::Config::default(),
125					sessions: None,
126				}
127			}
128		}
129	}
130}
131
132impl Config {
133	/// Create a configuration based on file named `local.json5`.
134	///
135	/// Will search in the directories mentioned in [`Examples`](index.html#examples).
136	/// This file should contain a configuration that only connects to entities on same host.
137	///
138	/// # Errors
139	/// Returns a [`std::io::Error`], if file does not exist in any of the places or is not accessible.
140	#[cfg(feature = "std")]
141	pub fn local() -> Result<Self> {
142		let path = find_config_file("local.json5")?;
143		#[cfg(feature = "std")]
144		debug!("using file {:?}", &path);
145		let content = read_config_file(&path)?;
146		let cfg = json5::from_str(&content)?;
147		Ok(cfg)
148	}
149
150	/// Create a configuration based on file named `client.json5`.
151	///
152	/// Will search in the directories mentioned in [`Examples`](index.html#examples).
153	/// This file should contain a configuration that creates an entity in client mode.
154	///
155	/// # Errors
156	/// Returns a [`std::io::Error`], if file does not exist in any of the places or is not accessible.
157	#[cfg(feature = "std")]
158	pub fn client() -> Result<Self> {
159		let path = find_config_file("client.json5")?;
160		debug!("using file {:?}", &path);
161		let content = read_config_file(&path)?;
162		let cfg = json5::from_str(&content)?;
163		Ok(cfg)
164	}
165
166	/// Create a configuration based on file named `peer.json5`.
167	///
168	/// Will search in the directories mentioned in [`Examples`](index.html#examples).
169	/// This file should contain a configuration that creates an entity in peer mode.
170	///
171	/// # Errors
172	/// Returns a [`std::io::Error`], if file does not exist in any of the places or is not accessible.
173	#[cfg(feature = "std")]
174	pub fn peer() -> Result<Self> {
175		let path = find_config_file("peer.json5")?;
176		debug!("using file {:?}", &path);
177		let content = read_config_file(&path)?;
178		let cfg = json5::from_str(&content)?;
179		Ok(cfg)
180	}
181
182	/// Create a configuration based on file named `router.json5`.
183	///
184	/// Will search in the directories mentioned in [`Examples`](index.html#examples).
185	/// This file should contain a configuration that creates an entity in router mode.
186	///
187	/// # Errors
188	/// Returns a [`std::io::Error`], if file does not exist in any of the places or is not accessible.
189	#[cfg(feature = "std")]
190	pub fn router() -> Result<Self> {
191		let path = find_config_file("router.json5")?;
192		debug!("using file {:?}", &path);
193		let content = read_config_file(&path)?;
194
195		let cfg = json5::from_str(&content)?;
196		Ok(cfg)
197	}
198
199	/// Create a configuration based on file with given filename.
200	///
201	/// Will search in the directories mentioned in [`Examples`](index.html#examples).
202	///
203	/// # Errors
204	/// Returns a [`std::io::Error`], if file does not exist in any of the places or is not accessible.
205	#[cfg(feature = "std")]
206	pub fn from_file(filename: &str) -> Result<Self> {
207		let path = find_config_file(filename)?;
208		debug!("using file {:?}", &path);
209		let content = read_config_file(&path)?;
210		let cfg = json5::from_str(&content)?;
211		Ok(cfg)
212	}
213
214	/// Method to extract the zenoh configuration from [`Config`].
215	///
216	/// Can be passed to `zenoh::open()`.
217	#[must_use]
218	pub const fn zenoh_config(&self) -> &zenoh::Config {
219		&self.zenoh
220	}
221
222	/// Method to get access to the the sessions in [`Config`].
223	///
224	#[must_use]
225	pub const fn sessions(&self) -> &Option<Vec<Session>> {
226		&self.sessions
227	}
228}
229// endregion:	--- Config
230
231#[cfg(test)]
232mod tests {
233	use super::*;
234
235	// check, that the auto traits are available
236	const fn is_normal<T: Sized + Send + Sync>() {}
237
238	#[test]
239	const fn normal_types() {
240		is_normal::<Config>();
241	}
242
243	#[test]
244	fn config_default() {
245		Config::default();
246	}
247
248	#[cfg(feature = "std")]
249	#[test]
250	fn config_local() -> Result<()> {
251		Config::local()?;
252		Ok(())
253	}
254
255	#[cfg(feature = "std")]
256	#[test]
257	fn config_router() -> Result<()> {
258		Config::router()?;
259		Ok(())
260	}
261
262	#[cfg(feature = "std")]
263	#[test]
264	fn config_peer() -> Result<()> {
265		Config::peer()?;
266		Ok(())
267	}
268
269	#[cfg(feature = "std")]
270	#[test]
271	fn config_client() -> Result<()> {
272		Config::client()?;
273		Ok(())
274	}
275
276	#[cfg(feature = "std")]
277	#[test]
278	fn config_from_file() -> Result<()> {
279		Config::from_file("default.json5")?;
280		Ok(())
281	}
282
283	#[cfg(feature = "std")]
284	#[test]
285	fn config_from_file_fails() {
286		let _ = Config::from_file("non_existent.json5").is_err();
287	}
288}