#[macro_use]
extern crate log;
use std::path::Path;
use std::collections::HashMap;
use std::env;
use serde_json::Value;
use toml_edit::{DocumentMut, Item, Table};
use std::sync::OnceLock;
type StdBoxError = Box<dyn std::error::Error + Send + Sync>;
type R<V = ()> = Result<V, StdBoxError>;
static CONFIG_STATE: OnceLock<ConfigState> = OnceLock::new();
#[derive(Debug)]
struct ConfigState {
file_path: Option<String>,
args_map: HashMap<String, String>,
}
impl ConfigState {
fn new() -> Self {
Self {
file_path: None,
args_map: HashMap::new(),
}
}
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());
}
fn set_file_path<P: AsRef<Path>>(&mut self, path: P) {
let path_str = path.as_ref().to_string_lossy().to_string();
self.file_path = Some(path_str);
info!("Config file path set to: {}", self.file_path.as_ref().unwrap());
}
fn get_file_path(&self) -> Option<&String> {
self.file_path.as_ref()
}
fn get_arg(&self, key: &str) -> Option<&String> {
self.args_map.get(key)
}
fn has_arg(&self, key: &str) -> bool {
self.args_map.contains_key(key)
}
}
struct ConfigManager;
impl ConfigManager {
fn load_document(file_path: &str) -> R<DocumentMut> {
let path = Path::new(file_path);
if path.exists() {
let content = std::fs::read_to_string(path)?;
let document = content.parse::<DocumentMut>().map_err(|e| {
format!("Failed to parse TOML file '{}': {}", file_path, e)
})?;
Ok(document)
} else {
Ok(DocumentMut::new())
}
}
fn save_document(file_path: &str, document: &DocumentMut) -> R {
if let Some(parent) = Path::new(file_path).parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(file_path, document.to_string())?;
info!("Config saved to file: {}", file_path);
Ok(())
}
fn get_value_with_priority(path: &str, state: &ConfigState) -> Option<String> {
if let Some(value) = state.get_arg(path) {
return Some(value.clone());
}
if let Some(file_path) = state.get_file_path() {
match Self::load_document(file_path) {
Ok(document) => {
if let Some(config_value) = Self::get_config_value(&document, path) {
if let Some(string_value) = Self::json_value_to_string(&config_value) {
return Some(string_value);
}
}
}
Err(e) => {
warn!("Failed to load config file '{}': {}", file_path, e);
}
}
}
if let Ok(env_value) = env::var(path.to_uppercase().replace('.', "_")) {
return Some(env_value);
}
None
}
fn get_config_value(document: &DocumentMut, path: &str) -> Option<Value> {
let keys: Vec<&str> = path.split('.').collect();
let mut current = 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(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(s: &str) -> Option<i64> {
s.parse::<i64>().ok()
}
fn string_to_f64(s: &str) -> Option<f64> {
s.parse::<f64>().ok()
}
fn string_to_bool(s: &str) -> Option<bool> {
match s.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => None,
}
}
fn set_config_value(path: &str, value: Value, state: &ConfigState) -> R {
if state.has_arg(path) {
return Err("当前为命令行参数, 不能保存".into());
}
let env_key = path.to_uppercase().replace('.', "_");
if env::var(&env_key).is_ok() {
if let Some(file_path) = state.get_file_path() {
if let Ok(document) = Self::load_document(file_path) {
if Self::get_config_value(&document, path).is_none() {
return Err("当前为环境变量参数, 不能保存".into());
}
}
}
}
let file_path = state.get_file_path().ok_or("No file path specified")?;
let mut document = Self::load_document(file_path)?;
let keys: Vec<&str> = path.split('.').collect();
let toml_value = Self::json_value_to_toml(&value)?;
let mut current = document.as_table_mut();
for (i, key) in keys.iter().enumerate() {
if i == keys.len() - 1 {
current.insert(key, toml_value);
info!("Config value set: {} = {:?}", path, value);
Self::save_document(file_path, &document)?;
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(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(value: &Value) -> R<Item> {
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(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(val)?);
}
Ok(Item::Table(toml_table))
},
Value::Null => Err("TOML does not support null values".into()),
}
}
}
fn get_config_state() -> &'static ConfigState {
CONFIG_STATE.get().expect("Config not initialized. Call init_config() first.")
}
pub fn init_config<P: AsRef<Path>>(config_path: P) -> R {
let mut state = ConfigState::new();
let args: Vec<String> = env::args().collect();
state.parse_args(args);
state.set_file_path(config_path);
CONFIG_STATE.set(state).map_err(|_| "Config already initialized")?;
Ok(())
}
pub fn init_config_file_only<P: AsRef<Path>>(config_path: P) -> R {
let mut state = ConfigState::new();
state.set_file_path(config_path);
CONFIG_STATE.set(state).map_err(|_| "Config already initialized")?;
Ok(())
}
pub fn get_arg(path: &str) -> Value {
let state = get_config_state();
if let Some(value) = state.get_arg(path) {
if let Ok(json_value) = serde_json::from_str::<Value>(value) {
return json_value;
} else {
return Value::String(value.clone());
}
}
if let Some(file_path) = state.get_file_path() {
match ConfigManager::load_document(file_path) {
Ok(document) => {
if let Some(config_value) = ConfigManager::get_config_value(&document, path) {
return config_value; }
}
Err(_) => {
}
}
}
let env_key = path.to_uppercase().replace('.', "_");
if let Ok(env_value) = env::var(&env_key) {
if let Ok(json_value) = serde_json::from_str::<Value>(&env_value) {
return json_value;
} else {
return Value::String(env_value);
}
}
Value::Null
}
pub fn set_arg(path: &str, value: Value) -> R {
let state = get_config_state();
ConfigManager::set_config_value(path, value, state)?;
Ok(())
}
pub fn save_config() -> R {
Ok(())
}
pub fn get_string(path: &str) -> Option<String> {
let state = get_config_state();
ConfigManager::get_value_with_priority(path, state)
}
pub fn get_i64(path: &str) -> Option<i64> {
let state = get_config_state();
if let Some(value) = ConfigManager::get_value_with_priority(path, state) {
ConfigManager::string_to_i64(&value)
} else {
None
}
}
pub fn get_f64(path: &str) -> Option<f64> {
let state = get_config_state();
if let Some(value) = ConfigManager::get_value_with_priority(path, state) {
ConfigManager::string_to_f64(&value)
} else {
None
}
}
pub fn get_bool(path: &str) -> Option<bool> {
let state = get_config_state();
if let Some(value) = ConfigManager::get_value_with_priority(path, state) {
ConfigManager::string_to_bool(&value)
} else {
None
}
}
pub fn set_string(path: &str, value: String) -> R {
set_arg(path, Value::String(value))
}
pub fn set_i64(path: &str, value: i64) -> R {
set_arg(path, Value::Number(serde_json::Number::from(value)))
}
pub fn set_f64(path: &str, value: f64) -> R {
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) -> R {
set_arg(path, Value::Bool(value))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_config_basic_operations() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("basic_test_config_unique.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();
let mut state = ConfigState::new();
state.set_file_path(&config_path);
assert_eq!(
ConfigManager::get_value_with_priority("database.host", &state),
Some("localhost".to_string())
);
assert_eq!(
ConfigManager::string_to_i64(
&ConfigManager::get_value_with_priority("database.port", &state).unwrap()
),
Some(5432)
);
assert_eq!(
ConfigManager::string_to_bool(
&ConfigManager::get_value_with_priority("database.enabled", &state).unwrap()
),
Some(true)
);
ConfigManager::set_config_value("database.host", Value::String("127.0.0.1".to_string()), &state).unwrap();
ConfigManager::set_config_value("database.port", Value::Number(serde_json::Number::from(3306)), &state).unwrap();
assert_eq!(
ConfigManager::get_value_with_priority("database.host", &state),
Some("127.0.0.1".to_string())
);
assert_eq!(
ConfigManager::string_to_i64(
&ConfigManager::get_value_with_priority("database.port", &state).unwrap()
),
Some(3306)
);
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() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("nested_config_test_unique.toml");
let mut state = ConfigState::new();
state.set_file_path(&config_path);
ConfigManager::set_config_value("a.b.c", Value::String("deep_value".to_string()), &state).unwrap();
ConfigManager::set_config_value("x.y.z", Value::Number(serde_json::Number::from(42)), &state).unwrap();
assert_eq!(
ConfigManager::get_value_with_priority("a.b.c", &state),
Some("deep_value".to_string())
);
assert_eq!(
ConfigManager::string_to_i64(
&ConfigManager::get_value_with_priority("x.y.z", &state).unwrap()
),
Some(42)
);
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() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("args_test_config_unique.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 state = ConfigState::new();
state.parse_args(test_args);
state.set_file_path(&config_path);
assert_eq!(
ConfigManager::get_value_with_priority("database.host", &state),
Some("127.0.0.1".to_string())
);
assert_eq!(
ConfigManager::string_to_i64(
&ConfigManager::get_value_with_priority("database.port", &state).unwrap()
),
Some(3306)
);
assert_eq!(
ConfigManager::string_to_bool(
&ConfigManager::get_value_with_priority("debug", &state).unwrap()
),
Some(true)
);
}
#[test]
fn test_type_conversions() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("type_test_config_unique.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 state = ConfigState::new();
state.parse_args(test_args);
state.set_file_path(&config_path);
assert_eq!(
ConfigManager::string_to_i64(
&ConfigManager::get_value_with_priority("int_val", &state).unwrap()
),
Some(42)
);
assert_eq!(
ConfigManager::string_to_f64(
&ConfigManager::get_value_with_priority("float_val", &state).unwrap()
),
Some(3.14)
);
assert_eq!(
ConfigManager::string_to_bool(
&ConfigManager::get_value_with_priority("bool_val", &state).unwrap()
),
Some(true)
);
assert_eq!(
ConfigManager::get_value_with_priority("str_val", &state),
Some("hello".to_string())
);
}
#[test]
fn test_source_protection() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("source_test_config_unique.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 state = ConfigState::new();
state.parse_args(test_args);
state.set_file_path(&config_path);
assert_eq!(
ConfigManager::get_value_with_priority("cmd_arg", &state),
Some("from_cmd".to_string())
);
assert_eq!(
ConfigManager::get_value_with_priority("ENV_VAR", &state),
Some("from_env".to_string())
);
assert_eq!(
ConfigManager::get_value_with_priority("database.host", &state),
Some("localhost".to_string())
);
let result = ConfigManager::set_config_value("cmd_arg", Value::String("modified".to_string()), &state);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "当前为命令行参数, 不能保存");
let result = ConfigManager::set_config_value("ENV_VAR", Value::String("modified".to_string()), &state);
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "当前为环境变量参数, 不能保存");
let result = ConfigManager::set_config_value("database.host", Value::String("127.0.0.1".to_string()), &state);
assert!(result.is_ok());
assert_eq!(
ConfigManager::get_value_with_priority("database.host", &state),
Some("127.0.0.1".to_string())
);
let result = ConfigManager::set_config_value("new_config", Value::String("new_value".to_string()), &state);
assert!(result.is_ok());
assert_eq!(
ConfigManager::get_value_with_priority("new_config", &state),
Some("new_value".to_string())
);
unsafe { env::remove_var("ENV_VAR"); }
}
#[test]
fn test_concurrent_file_operations() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("concurrent_test_config_unique.toml");
let config_content = r#"
[test]
counter = 0
"#;
fs::write(&config_path, config_content).unwrap();
let mut state = ConfigState::new();
state.set_file_path(&config_path);
assert_eq!(
ConfigManager::string_to_i64(
&ConfigManager::get_value_with_priority("test.counter", &state).unwrap()
),
Some(0)
);
let external_content = r#"
[test]
counter = 100
external_value = "added_by_external"
"#;
fs::write(&config_path, external_content).unwrap();
assert_eq!(
ConfigManager::string_to_i64(
&ConfigManager::get_value_with_priority("test.counter", &state).unwrap()
),
Some(100)
);
assert_eq!(
ConfigManager::get_value_with_priority("test.external_value", &state),
Some("added_by_external".to_string())
);
ConfigManager::set_config_value("test.counter", Value::Number(serde_json::Number::from(200)), &state).unwrap();
let final_content = fs::read_to_string(&config_path).unwrap();
assert!(final_content.contains("200"));
assert!(final_content.contains("added_by_external"));
assert_eq!(
ConfigManager::string_to_i64(
&ConfigManager::get_value_with_priority("test.counter", &state).unwrap()
),
Some(200)
);
assert_eq!(
ConfigManager::get_value_with_priority("test.external_value", &state),
Some("added_by_external".to_string())
);
}
}