1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
// Copyright © 2024 Stephan Kunz

//! The configuration data.
//!
//! An Agents configuration can be defined using json5 formated files.
//! There is a set of read methods for predefined filenames available.
//! You can find some example files [here](https://github.com/dimas-fw/dimas/tree/main/.config)
//!
//! # Examples
//! ```rust,no_run
//! # use dimas_config::Config;
//! # fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
//! // create a configuration from a file named `default.json5`
//! // located in one of the directories listed below.
//! // If that file does not exist, a default config will be created
//! let config = Config::default();
//!
//! // use file named `filename.json5`
//! // returns an error if file does not exist or is no valid configuration file
//! let config = Config::from_file("filename.json5")?;
//!
//! // methods with predefined filenames working like Config::from_file(...)
//! let config = Config::local()?;        // use file named `local.json5`
//! let config = Config::peer()?;         // use file named `peer.json5`
//! let config = Config::client()?;       // use file named `client.json5`
//! let config = Config::router()?;       // use file named `router.json5`
//!
//! # Ok(())
//! # }
//! ```
//!
//! The methods using files will search in following directories for the file (order first to last):
//!  - current working directory
//!  - `.config` directory below current working directory
//!  - `.config` directory below home directory
//!  - local config directory (`Linux`: `$XDG_CONFIG_HOME` or `$HOME/.config` | `Windows`: `{FOLDERID_LocalAppData}` | `MacOS`: `$HOME/Library/Application Support`)
//!  - config directory (`Linux`: `$XDG_CONFIG_HOME` or `$HOME/.config` | `Windows`: `{FOLDERID_RoamingAppData}` | `MacOS`: `$HOME/Library/Application Support`)
//!

// region:		--- exports
//pub use Config;
// endregion:	--- exports

// region:		--- types
/// Type alias for `std::result::Result` to ease up implementation
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;
// endregion:	--- types

// region:		--- modules
use dirs::{config_dir, config_local_dir, home_dir};
use std::env;
use std::io::{Error, ErrorKind};
use tracing::{error, info, warn};
// endregion:	--- modules

// region:		--- utils
/// find and read a config file given by name
fn _read_file(filename: &str) -> Result<String> {
	// handle environment path current working directory `CWD`
	let path = find_config_file(filename)?;
	info!("using file {:?}", &path);
	Ok(std::fs::read_to_string(path)?)
}
// endregion:	--- utils

// region:		--- Config
/// Manages the configuration
#[repr(transparent)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Config {
	#[serde(deserialize_with = "zenoh::config::Config::deserialize")]
	zenoh: zenoh::config::Config,
}

impl Default for Config {
	/// Create a default configuration<br>
	/// Will search for a configuration file with name "default.json5" in the directories mentioned in [`Examples`](index.html#examples).<br>
	/// This file should contain the wanted default configuration.<br>
	/// If no file is found, it will create a defined minimal default configuration.<br>
	/// Currently this is just a default zenoh peer configuration which connects to peers in same subnet.
	#[allow(clippy::cognitive_complexity)]
	fn default() -> Self {
		match find_config_file("default.json5") {
			Ok(path) => {
				info!("trying file {:?}", &path);
				match std::fs::read_to_string(path) {
					Ok(content) => match json5::from_str(&content) {
						Ok(result) => result,
						Err(error) => {
							error!("{}", error);
							warn!("using default zenoh peer configuration instead");
							Self {
								zenoh: zenoh::config::peer(),
							}
						}
					},
					Err(error) => {
						error!("{}", error);
						warn!("using default zenoh peer configuration instead");
						Self {
							zenoh: zenoh::config::peer(),
						}
					}
				}
			}
			Err(error) => {
				error!("{}", error);
				warn!("using default zenoh peer configuration instead");
				Self {
					zenoh: zenoh::config::peer(),
				}
			}
		}
	}
}

impl Config {
	/// Create a configuration based on file named `local.json5`.<br>
	/// Will search in the directories mentioned in [`Examples`](index.html#examples).<br>
	/// This file should contain a configuration that only connects to entities on same host.
	///
	/// # Errors
	/// Returns a [`std::io::Error`], if file does not exist in any of the places or is not accessible.
	pub fn local() -> Result<Self> {
		let path = find_config_file("local.json5")?;
		info!("using file {:?}", &path);
		let content = std::fs::read_to_string(path)?;
		let cfg = json5::from_str(&content)?;
		Ok(cfg)
	}

	/// Create a configuration based on file named `client.json5`.<br>
	/// Will search in the directories mentioned in [`Examples`](index.html#examples).<br>
	/// This file should contain a configuration that creates an entity in client mode.
	///
	/// # Errors
	/// Returns a [`std::io::Error`], if file does not exist in any of the places or is not accessible.
	pub fn client() -> Result<Self> {
		let path = find_config_file("client.json5")?;
		info!("using file {:?}", &path);
		let content = std::fs::read_to_string(path)?;
		let cfg = json5::from_str(&content)?;
		Ok(cfg)
	}

	/// Create a configuration based on file named `peer.json5`.<br>
	/// Will search in the directories mentioned in [`Examples`](index.html#examples).<br>
	/// This file should contain a configuration that creates an entity in peer mode.
	///
	/// # Errors
	/// Returns a [`std::io::Error`], if file does not exist in any of the places or is not accessible.
	pub fn peer() -> Result<Self> {
		let path = find_config_file("peer.json5")?;
		info!("using file {:?}", &path);
		let content = std::fs::read_to_string(path)?;
		let cfg = json5::from_str(&content)?;
		Ok(cfg)
	}

	/// Create a configuration based on file named `router.json5`.<br>
	/// Will search in the directories mentioned in [`Examples`](index.html#examples).<br>
	/// This file should contain a configuration that creates an entity in router mode.
	///
	/// # Errors
	/// Returns a [`std::io::Error`], if file does not exist in any of the places or is not accessible.
	pub fn router() -> Result<Self> {
		let path = find_config_file("router.json5")?;
		info!("using file {:?}", &path);
		let content = std::fs::read_to_string(path)?;

		let cfg = json5::from_str(&content)?;
		Ok(cfg)
	}

	/// Create a configuration based on file with given filename.<br>
	/// Will search in the directories mentioned in [`Examples`](index.html#examples).<br>
	///
	/// # Errors
	/// Returns a [`std::io::Error`], if file does not exist in any of the places or is not accessible.
	pub fn from_file(filename: &str) -> Result<Self> {
		let path = find_config_file(filename)?;
		info!("using file {:?}", &path);
		let content = std::fs::read_to_string(path)?;
		let cfg = json5::from_str(&content)?;
		Ok(cfg)
	}

	/// Method to extract the zenoh configuration from [`Config`].<br>
	/// Can be passed to `zenoh::open()`.
	#[must_use]
	pub fn zenoh_config(&self) -> zenoh::config::Config {
		self.zenoh.clone()
	}
}
// endregion:	--- Config

// region:		--- functions
/// find a config file given by name
/// function will search in following directories for the file (order first to last):
///  - current working directory
///  - `.config` directory below current working directory
///  - `.config` directory below home directory
///  - local config directory (`Linux`: `$XDG_CONFIG_HOME` or `$HOME/.config` | `Windows`: `{FOLDERID_LocalAppData}` | `MacOS`: `$HOME/Library/Application Support`)
///  - config directory (`Linux`: `$XDG_CONFIG_HOME` or `$HOME/.config` | `Windows`: `{FOLDERID_RoamingAppData}` | `MacOS`: `$HOME/Library/Application Support`)
/// # Errors
pub fn find_config_file(filename: &str) -> Result<std::path::PathBuf> {
	// handle environment path current working directory `CWD`
	if let Ok(cwd) = env::current_dir() {
		#[cfg(not(test))]
		let path = cwd.join(filename);
		#[cfg(test)]
		let path = cwd.join("..").join(filename);
		if path.is_file() {
			return Ok(path);
		}
		#[cfg(test)]
		let path = cwd.join("../..").join(filename);
		if path.is_file() {
			return Ok(path);
		}

		let path = cwd.join(".config").join(filename);
		if path.is_file() {
			return Ok(path);
		}
		#[cfg(test)]
		let path = cwd.join("../.config").join(filename);
		if path.is_file() {
			return Ok(path);
		}
		#[cfg(test)]
		let path = cwd.join("../../.config").join(filename);
		if path.is_file() {
			return Ok(path);
		}
	};

	// handle typical config directories
	for path in [home_dir(), config_local_dir(), config_dir()]
		.into_iter()
		.flatten()
	{
		let file = path.join(filename);
		if file.is_file() {
			return Ok(path);
		}
	}

	Err(Box::new(Error::new(
		ErrorKind::NotFound,
		format!("file {filename} not found"),
	)))
}
// endregion:	--- functions

#[cfg(test)]
mod tests {
	use super::*;

	// check, that the auto traits are available
	const fn is_normal<T: Sized + Send + Sync + Unpin>() {}

	#[test]
	const fn normal_types() {
		is_normal::<Config>();
	}

	#[test]
	fn config_default() {
		Config::default();
	}

	#[test]
	fn config_local() -> Result<()> {
		Config::local()?;
		Ok(())
	}

	#[test]
	fn config_router() -> Result<()> {
		Config::router()?;
		Ok(())
	}

	#[test]
	fn config_peer() -> Result<()> {
		Config::peer()?;
		Ok(())
	}

	#[test]
	fn config_client() -> Result<()> {
		Config::client()?;
		Ok(())
	}

	#[test]
	fn config_from_file() -> Result<()> {
		Config::from_file("default.json5")?;
		Ok(())
	}

	#[test]
	fn config_from_file_fails() {
		let _ = Config::from_file("non_existent.json5").is_err();
	}
}