#[macro_use]
extern crate log;
use std::path::Path;
use std::collections::HashMap;
use std::env;
use parking_lot::RwLock;
use serde_json::Value;
use toml_edit::{DocumentMut, Item, Table};
use std::sync::LazyLock as Lazy;
static CONFIG_MANAGER: Lazy<RwLock<ConfigManager>> = Lazy::new(|| {
RwLock::new(ConfigManager::new())
});
#[derive(Debug)]
pub struct ConfigManager {
document: DocumentMut,
file_path: Option<String>,
is_modified: bool,
args_map: HashMap<String, String>,
}
impl ConfigManager {
pub fn new() -> Self {
Self {
document: DocumentMut::new(),
file_path: None,
is_modified: false,
args_map: HashMap::new(),
}
}
pub fn parse_args(&mut self, args: Vec<String>) {
self.args_map.clear();
for (index, arg) in args.iter().skip(1).enumerate() {
if arg.contains('=') {
let parts: Vec<&str> = arg.splitn(2, '=').collect();
if parts.len() == 2 {
let key = parts[0].trim();
let value = parts[1].trim();
let cleaned_value = if (value.starts_with('"') && value.ends_with('"')) ||
(value.starts_with('\'') && value.ends_with('\'')) {
&value[1..value.len()-1]
} else {
value
};
self.args_map.insert(key.to_string(), cleaned_value.to_string());
}
} else {
let key = format!("arg{}", index);
self.args_map.insert(key, arg.clone());
}
}
info!("Parsed {} command line arguments", self.args_map.len());
}
pub fn load_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Box<dyn std::error::Error>> {
let path = path.as_ref();
let path_str = path.to_string_lossy().to_string();
if path.exists() {
let content = std::fs::read_to_string(path)?;
self.document = content.parse::<DocumentMut>().map_err(|e| {
format!("Failed to parse TOML file '{}': {}", path_str, e)
})?;
info!("Config loaded from file: {}", path_str);
} else {
self.document = DocumentMut::new();
info!("Config file not found, created empty config: {}", path_str);
}
self.file_path = Some(path_str);
self.is_modified = false;
Ok(())
}
pub fn save_to_file(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if let Some(ref path) = self.file_path {
if self.is_modified {
if let Some(parent) = Path::new(path).parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, self.document.to_string())?;
self.is_modified = false;
info!("Config saved to file: {}", path);
}
} else {
return Err("No file path specified".into());
}
Ok(())
}
pub fn get_value(&self, path: &str) -> Option<String> {
if let Some(value) = self.args_map.get(path) {
return Some(value.clone());
}
if let Some(config_value) = self.get_config_value(path) {
if let Some(string_value) = self.json_value_to_string(&config_value) {
return Some(string_value);
}
}
if let Ok(env_value) = env::var(path.to_uppercase().replace('.', "_")) {
return Some(env_value);
}
None
}
fn get_config_value(&self, path: &str) -> Option<Value> {
let keys: Vec<&str> = path.split('.').collect();
let mut current = self.document.as_table();
for (i, key) in keys.iter().enumerate() {
if i == keys.len() - 1 {
if let Some(item) = current.get(key) {
return self.item_to_json_value(item);
}
} else {
if let Some(Item::Table(table)) = current.get(key) {
current = table;
} else {
return None;
}
}
}
None
}
fn json_value_to_string(&self, value: &Value) -> Option<String> {
match value {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(b.to_string()),
_ => None,
}
}
fn string_to_i64(&self, s: &str) -> Option<i64> {
s.parse::<i64>().ok()
}
fn string_to_f64(&self, s: &str) -> Option<f64> {
s.parse::<f64>().ok()
}
fn string_to_bool(&self, s: &str) -> Option<bool> {
match s.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => None,
}
}
pub fn get_i64(&self, path: &str) -> Option<i64> {
if let Some(value) = self.get_value(path) {
self.string_to_i64(&value)
} else {
None
}
}
pub fn get_f64(&self, path: &str) -> Option<f64> {
if let Some(value) = self.get_value(path) {
self.string_to_f64(&value)
} else {
None
}
}
pub fn get_bool(&self, path: &str) -> Option<bool> {
if let Some(value) = self.get_value(path) {
self.string_to_bool(&value)
} else {
None
}
}
pub fn get_string(&self, path: &str) -> Option<String> {
self.get_value(path)
}
pub fn set_value(&mut self, path: &str, value: Value) -> Result<(), Box<dyn std::error::Error>> {
if self.args_map.contains_key(path) {
return Err("当前为命令行参数, 不能保存".into());
}
let env_key = path.to_uppercase().replace('.', "_");
if env::var(&env_key).is_ok() {
if self.get_config_value(path).is_none() {
return Err("当前为环境变量参数, 不能保存".into());
}
}
let keys: Vec<&str> = path.split('.').collect();
let toml_value = Self::json_value_to_toml_static(&value)?;
let mut current = self.document.as_table_mut();
for (i, key) in keys.iter().enumerate() {
if i == keys.len() - 1 {
current.insert(key, toml_value);
self.is_modified = true;
info!("Config value set: {} = {:?}", path, value);
self.save_to_file()?;
return Ok(());
} else {
if !current.contains_key(key) {
current.insert(key, Item::Table(Table::new()));
}
if let Some(Item::Table(table)) = current.get_mut(key) {
current = table;
} else {
return Err(format!("Key '{}' in path '{}' is not a table", key, path).into());
}
}
}
Err("Failed to set value".into())
}
fn item_to_json_value(&self, item: &Item) -> Option<Value> {
match item {
Item::Value(value) => {
match value {
toml_edit::Value::String(s) => Some(Value::String(s.value().to_string())),
toml_edit::Value::Integer(i) => Some(Value::Number(serde_json::Number::from(*i.value()))),
toml_edit::Value::Float(f) => {
if let Some(n) = serde_json::Number::from_f64(*f.value()) {
Some(Value::Number(n))
} else {
None
}
},
toml_edit::Value::Boolean(b) => Some(Value::Bool(*b.value())),
toml_edit::Value::Array(arr) => {
let mut json_array = Vec::new();
for item in arr.iter() {
if let Some(json_val) = self.item_to_json_value(&Item::Value(item.clone())) {
json_array.push(json_val);
}
}
Some(Value::Array(json_array))
},
toml_edit::Value::InlineTable(table) => {
let mut json_obj = serde_json::Map::new();
for (key, value) in table.iter() {
if let Some(json_val) = self.item_to_json_value(&Item::Value(value.clone())) {
json_obj.insert(key.to_string(), json_val);
}
}
Some(Value::Object(json_obj))
},
_ => None,
}
},
Item::Table(table) => {
let mut json_obj = serde_json::Map::new();
for (key, item) in table.iter() {
if let Some(json_val) = self.item_to_json_value(item) {
json_obj.insert(key.to_string(), json_val);
}
}
Some(Value::Object(json_obj))
},
_ => None,
}
}
fn json_value_to_toml_static(value: &Value) -> Result<Item, Box<dyn std::error::Error>> {
match value {
Value::String(s) => {
let string_value = toml_edit::Value::String(toml_edit::Formatted::new(s.clone()));
Ok(Item::Value(string_value))
},
Value::Number(n) => {
if let Some(i) = n.as_i64() {
let int_value = toml_edit::Value::Integer(toml_edit::Formatted::new(i));
Ok(Item::Value(int_value))
} else if let Some(f) = n.as_f64() {
let float_value = toml_edit::Value::Float(toml_edit::Formatted::new(f));
Ok(Item::Value(float_value))
} else {
Err("Invalid number format".into())
}
},
Value::Bool(b) => {
let bool_value = toml_edit::Value::Boolean(toml_edit::Formatted::new(*b));
Ok(Item::Value(bool_value))
},
Value::Array(arr) => {
let mut toml_array = toml_edit::Array::new();
for item in arr {
if let Item::Value(toml_val) = Self::json_value_to_toml_static(item)? {
toml_array.push(toml_val);
}
}
Ok(Item::Value(toml_edit::Value::Array(toml_array)))
},
Value::Object(obj) => {
let mut toml_table = Table::new();
for (key, val) in obj {
toml_table.insert(key, Self::json_value_to_toml_static(val)?);
}
Ok(Item::Table(toml_table))
},
Value::Null => Err("TOML does not support null values".into()),
}
}
}
pub fn init_config<P: AsRef<Path>>(config_path: P) -> Result<(), Box<dyn std::error::Error>> {
let mut manager = CONFIG_MANAGER.write();
let args: Vec<String> = env::args().collect();
manager.parse_args(args);
manager.load_from_file(config_path)?;
Ok(())
}
pub fn init_config_file_only<P: AsRef<Path>>(config_path: P) -> Result<(), Box<dyn std::error::Error>> {
let mut manager = CONFIG_MANAGER.write();
manager.load_from_file(config_path)?;
Ok(())
}
pub fn reset_config() {
let mut manager = CONFIG_MANAGER.write();
*manager = ConfigManager::new();
}
pub fn get_arg(path: &str) -> Value {
let manager = CONFIG_MANAGER.read();
if let Some(string_value) = manager.get_value(path) {
if let Ok(json_value) = serde_json::from_str::<Value>(&string_value) {
json_value
} else {
Value::String(string_value)
}
} else {
Value::Null
}
}
pub fn set_arg(path: &str, value: Value) -> Result<(), Box<dyn std::error::Error>> {
let mut manager = CONFIG_MANAGER.write();
manager.set_value(path, value)?;
Ok(())
}
pub fn save_config() -> Result<(), Box<dyn std::error::Error>> {
let mut manager = CONFIG_MANAGER.write();
manager.save_to_file()?;
Ok(())
}
pub fn get_string(path: &str) -> Option<String> {
let manager = CONFIG_MANAGER.read();
manager.get_string(path)
}
pub fn get_i64(path: &str) -> Option<i64> {
let manager = CONFIG_MANAGER.read();
manager.get_i64(path)
}
pub fn get_f64(path: &str) -> Option<f64> {
let manager = CONFIG_MANAGER.read();
manager.get_f64(path)
}
pub fn get_bool(path: &str) -> Option<bool> {
let manager = CONFIG_MANAGER.read();
manager.get_bool(path)
}
pub fn set_string(path: &str, value: String) -> Result<(), Box<dyn std::error::Error>> {
set_arg(path, Value::String(value))
}
pub fn set_i64(path: &str, value: i64) -> Result<(), Box<dyn std::error::Error>> {
set_arg(path, Value::Number(serde_json::Number::from(value)))
}
pub fn set_f64(path: &str, value: f64) -> Result<(), Box<dyn std::error::Error>> {
if let Some(n) = serde_json::Number::from_f64(value) {
set_arg(path, Value::Number(n))
} else {
Err("Invalid float value".into())
}
}
pub fn set_bool(path: &str, value: bool) -> Result<(), Box<dyn std::error::Error>> {
set_arg(path, Value::Bool(value))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_config_basic_operations() {
reset_config();
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("basic_test_config.toml");
let config_content = r#"
# 这是一个测试配置文件
[database]
host = "localhost"
port = 5432
enabled = true
[app]
name = "test_app"
version = "1.0.0"
[app.features]
logging = true
metrics = false
"#;
fs::write(&config_path, config_content).unwrap();
init_config_file_only(&config_path).unwrap();
assert_eq!(get_string("database.host"), Some("localhost".to_string()));
assert_eq!(get_i64("database.port"), Some(5432));
assert_eq!(get_bool("database.enabled"), Some(true));
assert_eq!(get_string("app.name"), Some("test_app".to_string()));
assert_eq!(get_bool("app.features.logging"), Some(true));
assert_eq!(get_bool("app.features.metrics"), Some(false));
set_string("database.host", "127.0.0.1".to_string()).unwrap();
set_i64("database.port", 3306).unwrap();
set_bool("app.features.metrics", true).unwrap();
assert_eq!(get_string("database.host"), Some("127.0.0.1".to_string()));
assert_eq!(get_i64("database.port"), Some(3306));
assert_eq!(get_bool("app.features.metrics"), Some(true));
save_config().unwrap();
let saved_content = fs::read_to_string(&config_path).unwrap();
assert!(saved_content.contains("127.0.0.1"));
assert!(saved_content.contains("3306"));
assert!(saved_content.contains("# 这是一个测试配置文件"));
}
#[test]
fn test_nested_config() {
reset_config();
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("nested_config_test.toml");
init_config_file_only(&config_path).unwrap();
set_string("a.b.c", "deep_value".to_string()).unwrap();
set_i64("x.y.z", 42).unwrap();
assert_eq!(get_string("a.b.c"), Some("deep_value".to_string()));
assert_eq!(get_i64("x.y.z"), Some(42));
save_config().unwrap();
let saved_content = fs::read_to_string(&config_path).unwrap();
assert!(saved_content.contains("deep_value"));
assert!(saved_content.contains("42"));
}
#[test]
fn test_command_line_args() {
reset_config();
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("args_test_config.toml");
let config_content = r#"
[database]
host = "localhost"
port = 5432
"#;
fs::write(&config_path, config_content).unwrap();
let test_args = vec![
"program_name".to_string(),
"database.host=127.0.0.1".to_string(),
"database.port=3306".to_string(),
"debug=true".to_string(),
];
let mut manager = CONFIG_MANAGER.write();
manager.parse_args(test_args);
manager.load_from_file(&config_path).unwrap();
drop(manager);
assert_eq!(get_string("database.host"), Some("127.0.0.1".to_string()));
assert_eq!(get_i64("database.port"), Some(3306));
assert_eq!(get_bool("debug"), Some(true));
}
#[test]
fn test_type_conversions() {
reset_config();
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("type_test_config.toml");
let test_args = vec![
"program_name".to_string(),
"int_val=42".to_string(),
"float_val=3.14".to_string(),
"bool_val=true".to_string(),
"str_val=hello".to_string(),
];
let mut manager = CONFIG_MANAGER.write();
manager.parse_args(test_args);
manager.load_from_file(&config_path).unwrap();
drop(manager);
assert_eq!(get_i64("int_val"), Some(42));
assert_eq!(get_f64("float_val"), Some(3.14));
assert_eq!(get_bool("bool_val"), Some(true));
assert_eq!(get_string("str_val"), Some("hello".to_string()));
}
#[test]
fn test_source_protection() {
reset_config();
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("source_test_config.toml");
let config_content = r#"
[database]
host = "localhost"
port = 5432
"#;
fs::write(&config_path, config_content).unwrap();
let test_args = vec![
"program_name".to_string(),
"cmd_arg=from_cmd".to_string(),
];
unsafe { env::set_var("ENV_VAR", "from_env"); }
let mut manager = CONFIG_MANAGER.write();
manager.parse_args(test_args);
manager.load_from_file(&config_path).unwrap();
drop(manager);
assert_eq!(get_string("cmd_arg"), Some("from_cmd".to_string()));
assert_eq!(get_string("ENV_VAR"), Some("from_env".to_string()));
assert_eq!(get_string("database.host"), Some("localhost".to_string()));
let result = set_string("cmd_arg", "modified".to_string());
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "当前为命令行参数, 不能保存");
let result = set_string("ENV_VAR", "modified".to_string());
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "当前为环境变量参数, 不能保存");
let result = set_string("database.host", "127.0.0.1".to_string());
assert!(result.is_ok());
assert_eq!(get_string("database.host"), Some("127.0.0.1".to_string()));
let result = set_string("new_config", "new_value".to_string());
assert!(result.is_ok());
assert_eq!(get_string("new_config"), Some("new_value".to_string()));
unsafe { env::remove_var("ENV_VAR"); }
}
}