use serde::{Deserialize, Serialize};
use crate::support::processing::template::OutputFormat;
pub(crate) fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GlobalConfig {
#[serde(default = "default_global_level")]
pub level: String,
#[serde(default = "default_global_format")]
pub format: String,
#[serde(default = "default_true", alias = "sanitizer_enabled")]
pub masking_enabled: bool,
#[serde(default = "default_true")]
pub auto_fallback: bool,
#[serde(default = "default_fallback_initial_delay")]
pub fallback_initial_delay_ms: u64,
#[serde(default = "default_fallback_max_delay")]
pub fallback_max_delay_ms: u64,
#[serde(default = "default_fallback_max_retries")]
pub fallback_max_retries: u32,
#[serde(default)]
pub output_format: OutputFormat,
}
fn default_global_level() -> String {
"info".to_string()
}
fn default_global_format() -> String {
"{timestamp} [{level}] {target} - {message}".to_string()
}
fn default_fallback_initial_delay() -> u64 {
1000
}
fn default_fallback_max_delay() -> u64 {
60000
}
fn default_fallback_max_retries() -> u32 {
10
}
impl Default for GlobalConfig {
fn default() -> Self {
Self {
level: default_global_level(),
format: default_global_format(),
masking_enabled: default_true(),
auto_fallback: default_true(),
fallback_initial_delay_ms: default_fallback_initial_delay(),
fallback_max_delay_ms: default_fallback_max_delay(),
fallback_max_retries: default_fallback_max_retries(),
output_format: OutputFormat::default(),
}
}
}
impl GlobalConfig {
pub fn validate(&mut self) -> Result<(), String> {
if self.fallback_initial_delay_ms > self.fallback_max_delay_ms {
tracing::warn!("{}", crate::i18n::tr("warn-delay_clamp"));
self.fallback_initial_delay_ms = self.fallback_max_delay_ms;
}
if self.fallback_max_retries == 0 {
tracing::warn!("{}", crate::i18n::tr("warn-fallback_retries_zero"));
self.fallback_max_retries = 1;
}
if !self.level.is_empty() && !crate::LogLevel::is_valid_level(&self.level) {
return Err(format!(
"invalid log level \"{}\", expected one of: {}",
self.level,
crate::LogLevel::VALID_LEVEL_STRINGS.join(", ")
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_values() {
let cfg = GlobalConfig::default();
assert_eq!(cfg.level, "info");
assert_eq!(cfg.format, "{timestamp} [{level}] {target} - {message}");
assert!(cfg.masking_enabled);
assert!(cfg.auto_fallback);
assert_eq!(cfg.fallback_initial_delay_ms, 1000);
assert_eq!(cfg.fallback_max_delay_ms, 60000);
assert_eq!(cfg.fallback_max_retries, 10);
}
#[test]
fn test_validate_clamps_initial_delay() {
let mut cfg = GlobalConfig::default();
cfg.fallback_initial_delay_ms = 99999;
cfg.fallback_max_delay_ms = 5000;
cfg.validate().unwrap();
assert_eq!(cfg.fallback_initial_delay_ms, 5000);
}
#[test]
fn test_validate_resets_zero_retries() {
let mut cfg = GlobalConfig::default();
cfg.fallback_max_retries = 0;
cfg.validate().unwrap();
assert_eq!(cfg.fallback_max_retries, 1);
}
#[test]
fn test_validate_no_change_when_valid() {
let mut cfg = GlobalConfig::default();
cfg.fallback_initial_delay_ms = 1000;
cfg.fallback_max_delay_ms = 60000;
cfg.fallback_max_retries = 3;
cfg.validate().unwrap();
assert_eq!(cfg.fallback_initial_delay_ms, 1000);
assert_eq!(cfg.fallback_max_retries, 3);
}
#[test]
fn test_validate_rejects_invalid_level() {
let mut cfg = GlobalConfig::default();
cfg.level = "verbose".to_string();
let err = cfg.validate().unwrap_err();
assert!(err.contains("verbose"), "unexpected error: {err}");
}
#[test]
fn test_validate_accepts_valid_levels() {
for level in ["trace", "debug", "info", "warn", "error", "fatal", "INFO"] {
let mut cfg = GlobalConfig::default();
cfg.level = level.to_string();
assert!(cfg.validate().is_ok(), "level '{level}' should be accepted");
}
}
#[test]
fn test_validate_allows_empty_level() {
let mut cfg = GlobalConfig::default();
cfg.level = String::new();
assert!(cfg.validate().is_ok());
}
#[test]
fn test_deserialize_masking_enabled_legacy_and_alias_keys() {
let legacy: GlobalConfig =
toml::from_str("level = \"info\"\nmasking_enabled = false\n").unwrap();
assert!(!legacy.masking_enabled);
let renamed: GlobalConfig =
toml::from_str("level = \"info\"\nsanitizer_enabled = false\n").unwrap();
assert!(!renamed.masking_enabled);
}
#[test]
fn test_serialize_emits_legacy_masking_enabled_key() {
let cfg = GlobalConfig {
masking_enabled: false,
..Default::default()
};
let rendered = toml::to_string(&cfg).unwrap();
assert!(
rendered.contains("masking_enabled = false"),
"serialized TOML must keep the legacy key, got: {rendered}"
);
assert!(
!rendered.contains("sanitizer_enabled"),
"alias must not be emitted on serialization, got: {rendered}"
);
}
}