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();
}
}