use crate::UtilsError;
use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ConfigValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<ConfigValue>),
Object(HashMap<String, ConfigValue>),
Null,
}
impl ConfigValue {
pub fn as_string(&self) -> Option<String> {
match self {
ConfigValue::String(s) => Some(s.clone()),
ConfigValue::Integer(i) => Some(i.to_string()),
ConfigValue::Float(f) => Some(f.to_string()),
ConfigValue::Boolean(b) => Some(b.to_string()),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
ConfigValue::Integer(i) => Some(*i),
ConfigValue::Float(f) => Some(*f as i64),
ConfigValue::String(s) => s.parse().ok(),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
ConfigValue::Float(f) => Some(*f),
ConfigValue::Integer(i) => Some(*i as f64),
ConfigValue::String(s) => s.parse().ok(),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
ConfigValue::Boolean(b) => Some(*b),
ConfigValue::String(s) => match s.to_lowercase().as_str() {
"true" | "yes" | "on" | "1" => Some(true),
"false" | "no" | "off" | "0" => Some(false),
_ => None,
},
ConfigValue::Integer(i) => Some(*i != 0),
_ => None,
}
}
pub fn as_array(&self) -> Option<&Vec<ConfigValue>> {
match self {
ConfigValue::Array(arr) => Some(arr),
_ => None,
}
}
pub fn as_object(&self) -> Option<&HashMap<String, ConfigValue>> {
match self {
ConfigValue::Object(obj) => Some(obj),
_ => None,
}
}
pub fn is_null(&self) -> bool {
matches!(self, ConfigValue::Null)
}
}
impl From<String> for ConfigValue {
fn from(s: String) -> Self {
ConfigValue::String(s)
}
}
impl From<&str> for ConfigValue {
fn from(s: &str) -> Self {
ConfigValue::String(s.to_string())
}
}
impl From<i64> for ConfigValue {
fn from(i: i64) -> Self {
ConfigValue::Integer(i)
}
}
impl From<f64> for ConfigValue {
fn from(f: f64) -> Self {
ConfigValue::Float(f)
}
}
impl From<bool> for ConfigValue {
fn from(b: bool) -> Self {
ConfigValue::Boolean(b)
}
}
#[derive(Debug, Clone)]
pub struct Config {
data: HashMap<String, ConfigValue>,
sources: Vec<ConfigSource>,
metadata: ConfigMetadata,
}
#[derive(Debug, Clone)]
pub struct ConfigMetadata {
pub loaded_at: SystemTime,
pub source_files: Vec<PathBuf>,
pub env_vars_used: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum ConfigSource {
File(PathBuf),
Environment,
CommandLine,
Default,
}
impl Default for Config {
fn default() -> Self {
Self::new()
}
}
impl Config {
pub fn new() -> Self {
Self {
data: HashMap::new(),
sources: Vec::new(),
metadata: ConfigMetadata {
loaded_at: SystemTime::now(),
source_files: Vec::new(),
env_vars_used: Vec::new(),
},
}
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, UtilsError> {
let path = path.as_ref();
let content = fs::read_to_string(path).map_err(|e| {
UtilsError::InvalidParameter(format!("Failed to read config file: {e}"))
})?;
let mut config = Self::new();
config.sources.push(ConfigSource::File(path.to_path_buf()));
config.metadata.source_files.push(path.to_path_buf());
match path.extension().and_then(|ext| ext.to_str()) {
Some("json") => config.load_json(&content)?,
Some("toml") => config.load_toml(&content)?,
Some("yaml") | Some("yml") => config.load_yaml(&content)?,
_ => {
return Err(UtilsError::InvalidParameter(
"Unsupported config file format".to_string(),
))
}
}
Ok(config)
}
fn load_json(&mut self, content: &str) -> Result<(), UtilsError> {
let json_value: serde_json::Value = serde_json::from_str(content)
.map_err(|e| UtilsError::InvalidParameter(format!("Invalid JSON: {e}")))?;
self.data = Self::json_to_config_map(json_value);
Ok(())
}
fn load_toml(&mut self, _content: &str) -> Result<(), UtilsError> {
Err(UtilsError::InvalidParameter(
"TOML support not implemented".to_string(),
))
}
fn load_yaml(&mut self, _content: &str) -> Result<(), UtilsError> {
Err(UtilsError::InvalidParameter(
"YAML support not implemented".to_string(),
))
}
fn json_to_config_map(json: serde_json::Value) -> HashMap<String, ConfigValue> {
match json {
serde_json::Value::Object(map) => map
.into_iter()
.map(|(k, v)| (k, Self::json_value_to_config_value(v)))
.collect(),
_ => HashMap::new(),
}
}
fn json_value_to_config_value(json: serde_json::Value) -> ConfigValue {
match json {
serde_json::Value::String(s) => ConfigValue::String(s),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
ConfigValue::Integer(i)
} else if let Some(f) = n.as_f64() {
ConfigValue::Float(f)
} else {
ConfigValue::Null
}
}
serde_json::Value::Bool(b) => ConfigValue::Boolean(b),
serde_json::Value::Array(arr) => ConfigValue::Array(
arr.into_iter()
.map(Self::json_value_to_config_value)
.collect(),
),
serde_json::Value::Object(obj) => ConfigValue::Object(
obj.into_iter()
.map(|(k, v)| (k, Self::json_value_to_config_value(v)))
.collect(),
),
serde_json::Value::Null => ConfigValue::Null,
}
}
pub fn load_environment(&mut self, prefix: &str) -> Result<(), UtilsError> {
self.sources.push(ConfigSource::Environment);
for (key, value) in env::vars() {
if let Some(stripped) = key.strip_prefix(prefix) {
let config_key = stripped.to_lowercase().replace('_', ".");
self.data
.insert(config_key, ConfigValue::String(value.clone()));
self.metadata.env_vars_used.push(key);
}
}
Ok(())
}
pub fn get(&self, key: &str) -> Option<&ConfigValue> {
if key.contains('.') {
self.get_nested(key)
} else {
self.data.get(key)
}
}
fn get_nested(&self, key: &str) -> Option<&ConfigValue> {
let parts: Vec<&str> = key.split('.').collect();
let mut current = self.data.get(parts[0])?;
for part in &parts[1..] {
if let ConfigValue::Object(obj) = current {
current = obj.get(*part)?;
} else {
return None;
}
}
Some(current)
}
pub fn set(&mut self, key: &str, value: ConfigValue) {
if key.contains('.') {
self.set_nested(key, value);
} else {
self.data.insert(key.to_string(), value);
}
}
fn set_nested(&mut self, key: &str, value: ConfigValue) {
let parts: Vec<&str> = key.split('.').collect();
if parts.is_empty() {
return;
}
if parts.len() == 1 {
self.data.insert(parts[0].to_string(), value);
return;
}
Self::set_nested_recursive(&mut self.data, &parts, 0, value);
}
fn set_nested_recursive(
current: &mut HashMap<String, ConfigValue>,
parts: &[&str],
index: usize,
value: ConfigValue,
) {
if index == parts.len() - 1 {
current.insert(parts[index].to_string(), value);
return;
}
let part = parts[index].to_string();
let entry = current
.entry(part)
.or_insert_with(|| ConfigValue::Object(HashMap::new()));
match entry {
ConfigValue::Object(ref mut obj) => {
Self::set_nested_recursive(obj, parts, index + 1, value);
}
_ => {
*entry = ConfigValue::Object(HashMap::new());
if let ConfigValue::Object(ref mut obj) = entry {
Self::set_nested_recursive(obj, parts, index + 1, value);
}
}
}
}
pub fn get_string(&self, key: &str, default: &str) -> String {
self.get(key)
.and_then(|v| v.as_string())
.unwrap_or_else(|| default.to_string())
}
pub fn get_i64(&self, key: &str, default: i64) -> i64 {
self.get(key).and_then(|v| v.as_i64()).unwrap_or(default)
}
pub fn get_f64(&self, key: &str, default: f64) -> f64 {
self.get(key).and_then(|v| v.as_f64()).unwrap_or(default)
}
pub fn get_bool(&self, key: &str, default: bool) -> bool {
self.get(key).and_then(|v| v.as_bool()).unwrap_or(default)
}
pub fn merge(&mut self, other: &Config) {
for (key, value) in &other.data {
self.data.insert(key.clone(), value.clone());
}
self.sources.extend(other.sources.clone());
}
pub fn keys(&self) -> Vec<String> {
let mut keys = Vec::new();
self.collect_keys("", &self.data, &mut keys);
keys
}
#[allow(clippy::only_used_in_recursion)]
fn collect_keys(
&self,
prefix: &str,
data: &HashMap<String, ConfigValue>,
keys: &mut Vec<String>,
) {
for (key, value) in data {
let full_key = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
keys.push(full_key.clone());
if let ConfigValue::Object(obj) = value {
self.collect_keys(&full_key, obj, keys);
}
}
}
pub fn to_json(&self) -> Result<String, UtilsError> {
let json_value = self.config_map_to_json(&self.data);
serde_json::to_string_pretty(&json_value)
.map_err(|e| UtilsError::InvalidParameter(format!("Failed to serialize config: {e}")))
}
fn config_map_to_json(&self, data: &HashMap<String, ConfigValue>) -> serde_json::Value {
let mut map = serde_json::Map::new();
for (key, value) in data {
map.insert(key.clone(), self.config_value_to_json(value));
}
serde_json::Value::Object(map)
}
#[allow(clippy::only_used_in_recursion)]
fn config_value_to_json(&self, value: &ConfigValue) -> serde_json::Value {
match value {
ConfigValue::String(s) => serde_json::Value::String(s.clone()),
ConfigValue::Integer(i) => serde_json::Value::Number((*i).into()),
ConfigValue::Float(f) => serde_json::Value::Number(
serde_json::Number::from_f64(*f).unwrap_or_else(|| 0.into()),
),
ConfigValue::Boolean(b) => serde_json::Value::Bool(*b),
ConfigValue::Array(arr) => {
serde_json::Value::Array(arr.iter().map(|v| self.config_value_to_json(v)).collect())
}
ConfigValue::Object(obj) => {
let mut map = serde_json::Map::new();
for (k, v) in obj {
map.insert(k.clone(), self.config_value_to_json(v));
}
serde_json::Value::Object(map)
}
ConfigValue::Null => serde_json::Value::Null,
}
}
}
pub struct ConfigValidator;
impl ConfigValidator {
pub fn validate_required_keys(
config: &Config,
required_keys: &[&str],
) -> Result<(), UtilsError> {
for key in required_keys {
if config.get(key).is_none() {
return Err(UtilsError::InvalidParameter(format!(
"Required configuration key '{key}' is missing"
)));
}
}
Ok(())
}
pub fn validate_types(
config: &Config,
type_specs: &HashMap<&str, &str>,
) -> Result<(), UtilsError> {
for (key, expected_type) in type_specs {
if let Some(value) = config.get(key) {
let valid = match *expected_type {
"string" => matches!(value, ConfigValue::String(_)),
"integer" => matches!(value, ConfigValue::Integer(_)),
"float" => {
matches!(value, ConfigValue::Float(_))
|| matches!(value, ConfigValue::Integer(_))
}
"boolean" => matches!(value, ConfigValue::Boolean(_)),
"array" => matches!(value, ConfigValue::Array(_)),
"object" => matches!(value, ConfigValue::Object(_)),
_ => false,
};
if !valid {
return Err(UtilsError::InvalidParameter(format!(
"Configuration key '{key}' has wrong type, expected {expected_type}"
)));
}
}
}
Ok(())
}
pub fn validate_ranges(
config: &Config,
range_specs: &HashMap<&str, (f64, f64)>,
) -> Result<(), UtilsError> {
for (key, (min_val, max_val)) in range_specs {
if let Some(value) = config.get(key) {
let numeric_value = match value {
ConfigValue::Integer(i) => Some(*i as f64),
ConfigValue::Float(f) => Some(*f),
_ => None,
};
if let Some(val) = numeric_value {
if val < *min_val || val > *max_val {
return Err(UtilsError::InvalidParameter(format!(
"Configuration key '{key}' value {val} is outside valid range [{min_val}, {max_val}]"
)));
}
}
}
}
Ok(())
}
pub fn validate_custom<F>(config: &Config, validator: F) -> Result<(), UtilsError>
where
F: Fn(&Config) -> Result<(), String>,
{
validator(config).map_err(UtilsError::InvalidParameter)
}
}
pub struct HotReloadConfig {
config: Arc<RwLock<Config>>,
file_path: PathBuf,
last_modified: SystemTime,
check_interval: Duration,
}
impl HotReloadConfig {
pub fn new<P: AsRef<Path>>(file_path: P, check_interval: Duration) -> Result<Self, UtilsError> {
let file_path = file_path.as_ref().to_path_buf();
let config = Config::from_file(&file_path)?;
let last_modified = fs::metadata(&file_path)
.and_then(|m| m.modified())
.map_err(|e| {
UtilsError::InvalidParameter(format!("Failed to get file metadata: {e}"))
})?;
Ok(Self {
config: Arc::new(RwLock::new(config)),
file_path,
last_modified,
check_interval,
})
}
pub fn get_config(&self) -> Arc<RwLock<Config>> {
self.config.clone()
}
pub fn check_and_reload(&mut self) -> Result<bool, UtilsError> {
let current_modified = fs::metadata(&self.file_path)
.and_then(|m| m.modified())
.map_err(|e| {
UtilsError::InvalidParameter(format!("Failed to get file metadata: {e}"))
})?;
if current_modified > self.last_modified {
let new_config = Config::from_file(&self.file_path)?;
{
let mut config = self.config.write().expect("operation should succeed");
*config = new_config;
}
self.last_modified = current_modified;
Ok(true)
} else {
Ok(false)
}
}
pub fn start_auto_reload(mut self) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || loop {
std::thread::sleep(self.check_interval);
if let Err(e) = self.check_and_reload() {
eprintln!("Error reloading config: {e}");
}
})
}
}
pub struct ArgParser {
args: Vec<String>,
config: Config,
}
impl Default for ArgParser {
fn default() -> Self {
Self::new()
}
}
impl ArgParser {
pub fn new() -> Self {
let args: Vec<String> = env::args().collect();
Self {
args,
config: Config::new(),
}
}
pub fn parse(&mut self) -> Result<(), UtilsError> {
let mut i = 1;
while i < self.args.len() {
let arg = &self.args[i];
if let Some(stripped) = arg.strip_prefix("--") {
let key = stripped;
if let Some(eq_pos) = key.find('=') {
let (k, v) = key.split_at(eq_pos);
let value = &v[1..]; self.config.set(k, self.parse_value(value));
} else if i + 1 < self.args.len() && !self.args[i + 1].starts_with('-') {
i += 1;
let value = &self.args[i];
self.config.set(key, self.parse_value(value));
} else {
self.config.set(key, ConfigValue::Boolean(true));
}
} else if arg.starts_with('-') && arg.len() == 2 {
let key = &arg[1..];
if i + 1 < self.args.len() && !self.args[i + 1].starts_with('-') {
i += 1;
let value = &self.args[i];
self.config.set(key, self.parse_value(value));
} else {
self.config.set(key, ConfigValue::Boolean(true));
}
}
i += 1;
}
Ok(())
}
#[allow(clippy::only_used_in_recursion)]
fn parse_value(&self, value: &str) -> ConfigValue {
if let Ok(b) = value.parse::<bool>() {
ConfigValue::Boolean(b)
} else if let Ok(i) = value.parse::<i64>() {
ConfigValue::Integer(i)
} else if let Ok(f) = value.parse::<f64>() {
ConfigValue::Float(f)
} else if value.starts_with('[') && value.ends_with(']') {
let inner = &value[1..value.len() - 1];
let items: Vec<ConfigValue> = inner
.split(',')
.map(|s| self.parse_value(s.trim()))
.collect();
ConfigValue::Array(items)
} else {
ConfigValue::String(value.to_string())
}
}
pub fn get_config(self) -> Config {
self.config
}
}
pub struct ConfigBuilder {
config: Config,
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl ConfigBuilder {
pub fn new() -> Self {
Self {
config: Config::new(),
}
}
pub fn add_file<P: AsRef<Path>>(mut self, path: P) -> Result<Self, UtilsError> {
let file_config = Config::from_file(path)?;
self.config.merge(&file_config);
Ok(self)
}
pub fn add_env(mut self, prefix: &str) -> Result<Self, UtilsError> {
self.config.load_environment(prefix)?;
Ok(self)
}
pub fn add_args(mut self) -> Result<Self, UtilsError> {
let mut parser = ArgParser::new();
parser.parse()?;
self.config.merge(&parser.get_config());
Ok(self)
}
pub fn set_default(mut self, key: &str, value: ConfigValue) -> Self {
if self.config.get(key).is_none() {
self.config.set(key, value);
}
self
}
pub fn validate<F>(self, validator: F) -> Result<Self, UtilsError>
where
F: Fn(&Config) -> Result<(), String>,
{
ConfigValidator::validate_custom(&self.config, validator)?;
Ok(self)
}
pub fn build(self) -> Config {
self.config
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_config_value_conversions() {
let string_val = ConfigValue::String("test".to_string());
assert_eq!(string_val.as_string(), Some("test".to_string()));
let int_val = ConfigValue::Integer(42);
assert_eq!(int_val.as_i64(), Some(42));
assert_eq!(int_val.as_f64(), Some(42.0));
let bool_val = ConfigValue::Boolean(true);
assert_eq!(bool_val.as_bool(), Some(true));
let float_val = ConfigValue::Float(std::f64::consts::PI);
assert_eq!(float_val.as_f64(), Some(std::f64::consts::PI));
}
#[test]
fn test_config_get_set() {
let mut config = Config::new();
config.set("test.key", ConfigValue::String("value".to_string()));
assert_eq!(config.get_string("test.key", "default"), "value");
config.set("number", ConfigValue::Integer(42));
assert_eq!(config.get_i64("number", 0), 42);
config.set("flag", ConfigValue::Boolean(true));
assert!(config.get_bool("flag", false));
}
#[test]
fn test_json_config_loading() {
let json_content = r#"
{
"database": {
"host": "localhost",
"port": 5432,
"name": "test_db"
},
"debug": true,
"timeout": 30.5
}
"#;
let mut temp_file = NamedTempFile::with_suffix(".json").expect("operation should succeed");
temp_file
.write_all(json_content.as_bytes())
.expect("operation should succeed");
let config = Config::from_file(temp_file.path()).expect("operation should succeed");
assert_eq!(config.get_string("database.host", ""), "localhost");
assert_eq!(config.get_i64("database.port", 0), 5432);
assert_eq!(config.get_string("database.name", ""), "test_db");
assert!(config.get_bool("debug", false));
assert_eq!(config.get_f64("timeout", 0.0), 30.5);
}
#[test]
fn test_config_validation() {
let mut config = Config::new();
config.set("required_key", ConfigValue::String("value".to_string()));
config.set("number_key", ConfigValue::Integer(50));
let required_keys = vec!["required_key"];
assert!(ConfigValidator::validate_required_keys(&config, &required_keys).is_ok());
let missing_keys = vec!["missing_key"];
assert!(ConfigValidator::validate_required_keys(&config, &missing_keys).is_err());
let mut type_specs = HashMap::new();
type_specs.insert("required_key", "string");
type_specs.insert("number_key", "integer");
assert!(ConfigValidator::validate_types(&config, &type_specs).is_ok());
type_specs.insert("number_key", "string");
assert!(ConfigValidator::validate_types(&config, &type_specs).is_err());
let mut range_specs = HashMap::new();
range_specs.insert("number_key", (0.0, 100.0));
assert!(ConfigValidator::validate_ranges(&config, &range_specs).is_ok());
range_specs.insert("number_key", (60.0, 100.0));
assert!(ConfigValidator::validate_ranges(&config, &range_specs).is_err());
}
#[test]
fn test_arg_parser() {
let args = vec![
"program".to_string(),
"--host=localhost".to_string(),
"--port".to_string(),
"8080".to_string(),
"--debug".to_string(),
"-v".to_string(),
];
let mut parser = ArgParser {
args,
config: Config::new(),
};
parser.parse().expect("operation should succeed");
let config = parser.get_config();
assert_eq!(config.get_string("host", ""), "localhost");
assert_eq!(config.get_i64("port", 0), 8080);
assert!(config.get_bool("debug", false));
assert!(config.get_bool("v", false));
}
#[test]
fn test_config_builder() {
let config = ConfigBuilder::new()
.set_default("host", ConfigValue::String("localhost".to_string()))
.set_default("port", ConfigValue::Integer(8080))
.set_default("debug", ConfigValue::Boolean(false))
.build();
assert_eq!(config.get_string("host", ""), "localhost");
assert_eq!(config.get_i64("port", 0), 8080);
assert!(!config.get_bool("debug", true));
}
#[test]
fn test_config_merge() {
let mut config1 = Config::new();
config1.set("key1", ConfigValue::String("value1".to_string()));
config1.set("key2", ConfigValue::Integer(42));
let mut config2 = Config::new();
config2.set("key2", ConfigValue::Integer(100)); config2.set("key3", ConfigValue::Boolean(true));
config1.merge(&config2);
assert_eq!(config1.get_string("key1", ""), "value1");
assert_eq!(config1.get_i64("key2", 0), 100); assert!(config1.get_bool("key3", false)); }
}