use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to read config file: {0}")]
IoError(#[from] std::io::Error),
#[error("failed to parse YAML config: {0}")]
ParseError(#[from] serde_yaml::Error),
#[error("config validation error: {0}")]
ValidationError(String),
}
pub fn parse_duration(s: &str) -> Result<Duration, String> {
let s = s.trim();
if s.is_empty() {
return Err("duration string is empty".to_string());
}
humantime::parse_duration(s).map_err(|e| e.to_string())
}
pub fn expand_env_vars(input: &str) -> String {
static ENV_VAR_REGEX: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let regex = ENV_VAR_REGEX.get_or_init(|| {
regex::Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}")
.expect("failed to compile env var regex")
});
regex
.replace_all(input, |caps: ®ex::Captures| {
let var_name = &caps[1];
let default_value = caps.get(2).map(|m| m.as_str()).unwrap_or("");
std::env::var(var_name).unwrap_or_else(|_| default_value.to_string())
})
.into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_duration_valid() {
assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
}
#[test]
fn test_parse_duration_invalid() {
assert!(parse_duration("").is_err());
assert!(parse_duration("abc").is_err());
assert!(parse_duration("30x").is_err());
assert!(parse_duration("30").is_err());
}
#[test]
fn test_parse_duration_extended_formats() {
assert_eq!(parse_duration("100ms").unwrap(), Duration::from_millis(100));
assert_eq!(parse_duration("1h30m").unwrap(), Duration::from_secs(5400));
assert_eq!(parse_duration("1d").unwrap(), Duration::from_secs(86400));
assert_eq!(parse_duration("2h 30m").unwrap(), Duration::from_secs(9000));
}
#[test]
fn test_expand_env_vars_no_vars() {
assert_eq!(expand_env_vars("hello world"), "hello world");
}
#[test]
fn test_expand_env_vars_with_default() {
let result = expand_env_vars("Bearer ${NONEXISTENT_TOKEN_12345:-default_token}");
assert_eq!(result, "Bearer default_token");
}
#[test]
fn test_expand_env_vars_from_env() {
unsafe {
std::env::set_var("TEST_VAR_EXPAND", "secret_value");
}
let result = expand_env_vars("Authorization: ${TEST_VAR_EXPAND}");
assert_eq!(result, "Authorization: secret_value");
unsafe {
std::env::remove_var("TEST_VAR_EXPAND");
}
}
}