1use crate::LogLevel;
17use config::{
18 Config as ConfigSource, ConfigError as SourceConfigError,
19 File as ConfigFile,
20};
21use envy;
22use parking_lot::RwLock;
23use serde::{Deserialize, Serialize};
24use std::{
25 collections::HashMap,
26 env,
27 fs::{self, OpenOptions},
28 num::NonZeroU64,
29 path::{Path, PathBuf},
30 sync::Arc,
31};
32use thiserror::Error;
33
34const CURRENT_CONFIG_VERSION: &str = "1.0";
35
36#[cfg(feature = "tokio")]
37mod hot_reload;
38mod log_rotation;
39mod set;
40
41#[cfg(feature = "tokio")]
42pub use hot_reload::HOT_RELOAD_POLL_INTERVAL;
43pub use log_rotation::LogRotation;
44use set::SETTERS;
45
46#[derive(Debug, Error)]
48pub enum ConfigError {
49 #[error("Environment variable parse error: {0}")]
51 EnvVarParseError(#[from] envy::Error),
52
53 #[error("Configuration parsing error: {0}")]
55 ConfigParseError(#[from] SourceConfigError),
56
57 #[error("Invalid file path: {0}")]
59 InvalidFilePath(String),
60
61 #[error("File read error: {0}")]
63 FileReadError(String),
64
65 #[error("File write error: {0}")]
67 FileWriteError(String),
68
69 #[error("Configuration validation error: {0}")]
71 ValidationError(String),
72
73 #[error("Configuration version error: {0}")]
75 VersionError(String),
76
77 #[error("Missing required field: {0}")]
79 MissingFieldError(String),
80
81 #[cfg(feature = "tokio")]
83 #[error("Watcher error: {0}")]
84 WatcherError(std::io::Error),
85}
86
87impl From<crate::commons::config::ConfigError> for ConfigError {
88 fn from(err: crate::commons::config::ConfigError) -> Self {
89 Self::ValidationError(err.to_string())
90 }
91}
92
93#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
95#[serde(tag = "type", content = "value")]
96pub enum LoggingDestination {
97 File(PathBuf),
99 Stdout,
101 Network(String),
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107#[allow(clippy::unsafe_derive_deserialize)]
109pub struct Config {
110 #[serde(default = "default_version")]
112 pub version: String,
113 #[serde(default = "default_profile")]
115 pub profile: String,
116 #[serde(default = "default_log_file_path")]
118 pub log_file_path: PathBuf,
119 #[serde(default)]
121 pub log_level: LogLevel,
122 pub log_rotation: Option<LogRotation>,
124 #[serde(default = "default_log_format")]
126 pub log_format: String,
127 #[serde(default = "default_logging_destinations")]
129 pub logging_destinations: Vec<LoggingDestination>,
130 #[serde(default)]
132 pub env_vars: HashMap<String, String>,
133}
134
135fn default_version() -> String {
136 CURRENT_CONFIG_VERSION.to_string()
137}
138fn default_profile() -> String {
139 "default".to_string()
140}
141fn default_log_file_path() -> PathBuf {
142 PathBuf::from("RLG.log")
143}
144fn default_log_format() -> String {
145 "%level - %message".to_string()
146}
147fn default_logging_destinations() -> Vec<LoggingDestination> {
148 vec![LoggingDestination::File(PathBuf::from("RLG.log"))]
149}
150
151impl Default for Config {
152 fn default() -> Self {
153 Self {
154 version: default_version(),
155 profile: default_profile(),
156 log_file_path: default_log_file_path(),
157 log_level: LogLevel::INFO,
158 log_rotation: NonZeroU64::new(10 * 1024 * 1024)
159 .map(LogRotation::Size),
160 log_format: default_log_format(),
161 logging_destinations: default_logging_destinations(),
162 env_vars: HashMap::new(),
163 }
164 }
165}
166
167impl Config {
168 pub fn load<P: AsRef<Path>>(
178 config_path: Option<P>,
179 ) -> Result<Arc<RwLock<Self>>, ConfigError> {
180 let config = match config_path {
181 Some(path) => Self::from_toml(
182 &fs::read_to_string(path.as_ref())
183 .map_err(|e| read_error(&e))?,
184 )?,
185 None => Self::default(),
186 };
187 config.activate()
188 }
189
190 #[cfg(feature = "tokio")]
199 pub async fn load_async<P: AsRef<Path>>(
200 config_path: Option<P>,
201 ) -> Result<Arc<RwLock<Self>>, ConfigError> {
202 let path = config_path.map(|p| p.as_ref().to_path_buf());
205 let config = match path {
206 Some(path) => Self::from_toml(
207 &tokio::fs::read_to_string(&path)
208 .await
209 .map_err(|e| read_error(&e))?,
210 )?,
211 None => Self::default(),
212 };
213 config.activate()
214 }
215
216 fn from_toml(contents: &str) -> Result<Self, ConfigError> {
218 let source = ConfigSource::builder()
219 .add_source(ConfigFile::from_str(
220 contents,
221 config::FileFormat::Toml,
222 ))
223 .build()?;
224 let version: String = source.get("version")?;
225 if version != CURRENT_CONFIG_VERSION {
226 return Err(ConfigError::VersionError(format!(
227 "Unsupported configuration version: {version}"
228 )));
229 }
230 Ok(source.try_deserialize()?)
231 }
232
233 fn activate(self) -> Result<Arc<RwLock<Self>>, ConfigError> {
235 self.validate()?;
236 self.ensure_paths()?;
237 Ok(Arc::new(RwLock::new(self)))
238 }
239
240 pub fn save_to_file<P: AsRef<Path>>(
249 &self,
250 path: P,
251 ) -> Result<(), ConfigError> {
252 let config_string =
253 toml::to_string_pretty(self).map_err(|e| {
254 ConfigError::FileWriteError(format!(
255 "Failed to serialize config to TOML: {e}"
256 ))
257 })?;
258 fs::write(path, config_string).map_err(|e| {
259 ConfigError::FileWriteError(format!(
260 "Failed to write config file: {e}"
261 ))
262 })?;
263 Ok(())
264 }
265
266 pub fn set<T: Serialize>(
272 &mut self,
273 key: &str,
274 value: T,
275 ) -> Result<(), ConfigError> {
276 let val = serde_json::to_value(value)
277 .map_err(|e| ConfigError::ValidationError(e.to_string()))?;
278 let (_, setter) = SETTERS
279 .iter()
280 .find(|(name, _)| *name == key)
281 .ok_or_else(|| {
282 ConfigError::ValidationError(format!(
283 "Unknown configuration key: {key}"
284 ))
285 })?;
286 setter(self, val)
287 }
288
289 pub fn validate(&self) -> Result<(), ConfigError> {
295 let mut v = crate::commons::validation::Validator::new();
296 v.check("version", || not_empty(&self.version))
297 .check("profile", || not_empty(&self.profile))
298 .check("log_format", || not_empty(&self.log_format));
299 self.validate_destinations()?;
302 for (key, value) in &self.env_vars {
303 v.check(&format!("env_var_key_{key}"), || not_empty(key));
304 v.check(&format!("env_var_val_{key}"), || not_empty(value));
305 }
306 v.finish().map_err(|errors| {
307 let msgs: Vec<String> = errors
308 .iter()
309 .map(|(f, e)| format!("{f}: {e}"))
310 .collect();
311 ConfigError::ValidationError(msgs.join("; "))
312 })
313 }
314
315 fn validate_destinations(&self) -> Result<(), ConfigError> {
316 if self.log_file_path.as_os_str().is_empty() {
317 return Err(ConfigError::ValidationError(
318 "Log file path cannot be empty".into(),
319 ));
320 }
321 if self.logging_destinations.is_empty() {
322 return Err(ConfigError::ValidationError(
323 "At least one logging destination must be specified"
324 .into(),
325 ));
326 }
327 Ok(())
328 }
329
330 pub fn ensure_paths(&self) -> Result<(), ConfigError> {
336 if let Some(LoggingDestination::File(path)) =
337 self.logging_destinations.first()
338 {
339 if let Some(parent_dir) = path.parent() {
340 fs::create_dir_all(parent_dir).map_err(|e| {
341 ConfigError::ValidationError(format!(
342 "Failed to create directory for log file: {e}"
343 ))
344 })?;
345 }
346 OpenOptions::new()
347 .create(true)
348 .append(true)
349 .open(path)
350 .map_err(|e| {
351 ConfigError::ValidationError(format!(
352 "Log file is not writable: {e}"
353 ))
354 })?;
355 }
356 Ok(())
357 }
358
359 #[must_use]
361 pub fn expand_env_vars(&self) -> Self {
362 let mut new_config = self.clone();
363 for (key, value) in &mut new_config.env_vars {
364 if let Ok(env_value) = env::var(key) {
365 *value = env_value;
366 }
367 }
368 new_config
369 }
370
371 #[must_use]
373 pub fn diff(
374 config1: &Self,
375 config2: &Self,
376 ) -> HashMap<String, String> {
377 let mut diffs = HashMap::new();
378 macro_rules! config_diff_fields {
379 ($c1:expr, $c2:expr, $diffs:expr;
380 $( display $field:ident; )*
381 $( debug $dfield:ident; )*
382 $( path $pfield:ident; )*
383 ) => {
384 $(
385 if $c1.$field != $c2.$field {
386 $diffs.insert(
387 stringify!($field).to_string(),
388 format!("{} -> {}", $c1.$field, $c2.$field),
389 );
390 }
391 )*
392 $(
393 if $c1.$dfield != $c2.$dfield {
394 $diffs.insert(
395 stringify!($dfield).to_string(),
396 format!("{:?} -> {:?}", $c1.$dfield, $c2.$dfield),
397 );
398 }
399 )*
400 $(
401 if $c1.$pfield != $c2.$pfield {
402 $diffs.insert(
403 stringify!($pfield).to_string(),
404 format!("{} -> {}", $c1.$pfield.display(), $c2.$pfield.display()),
405 );
406 }
407 )*
408 };
409 }
410 config_diff_fields!(config1, config2, diffs;
411 display version;
412 display profile;
413 display log_format;
414 debug log_level;
415 debug log_rotation;
416 debug logging_destinations;
417 debug env_vars;
418 path log_file_path;
419 );
420 diffs
421 }
422
423 #[must_use]
425 pub fn override_with(&self, other: &Self) -> Self {
426 let mut env_vars = self.env_vars.clone();
427 env_vars.extend(other.env_vars.clone());
428 Self {
429 version: other.version.clone(),
430 profile: other.profile.clone(),
431 log_file_path: other.log_file_path.clone(),
432 log_level: other.log_level,
433 log_rotation: other.log_rotation,
434 log_format: other.log_format.clone(),
435 logging_destinations: other.logging_destinations.clone(),
436 env_vars,
437 }
438 }
439}
440
441impl TryFrom<env::Vars> for Config {
442 type Error = ConfigError;
443 fn try_from(vars: env::Vars) -> Result<Self, Self::Error> {
444 envy::from_iter(vars).map_err(ConfigError::EnvVarParseError)
445 }
446}
447
448fn not_empty(
450 value: &str,
451) -> crate::commons::validation::ValidationResult<()> {
452 crate::commons::validation::validate_not_empty(value.trim())
453 .map(|_| ())
454}
455
456fn read_error(e: &std::io::Error) -> ConfigError {
458 ConfigError::FileReadError(e.to_string())
459}
460
461#[cfg(all(test, not(miri)))]
462mod tests;