use std::path::Path;
use thiserror::Error;
use crate::config::UrsulaConfig;
use crate::preset::Preset;
use crate::validate::ValidationError;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("config file not found: {0}")]
NotFound(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("TOML parse error: {0}")]
TomlParse(#[from] toml::de::Error),
#[error("validation error: {0}")]
Validation(#[from] ValidationError),
#[error("{0}")]
Other(String),
}
pub fn find_default_config() -> Option<std::path::PathBuf> {
let mut candidates = vec![
std::path::PathBuf::from("./ursula.toml"),
std::path::PathBuf::from("/etc/ursula/ursula.toml"),
];
if let Some(config_dir) = dirs::config_dir() {
candidates.push(config_dir.join("ursula").join("config.toml"));
}
for path in &candidates {
if path.exists() {
return Some(path.clone());
}
}
None
}
pub fn load_config(
path: Option<&Path>,
preset: Option<Preset>,
node_id: Option<u64>,
) -> Result<UrsulaConfig, ConfigError> {
let user_table = match path {
Some(path) => {
if path.extension().and_then(|e| e.to_str()) != Some("toml") {
return Err(ConfigError::Other(format!(
"unsupported config file extension for '{}': only TOML is supported",
path.display()
)));
}
let raw = std::fs::read_to_string(path)?;
raw.parse::<toml::Table>()?
}
None => toml::Table::new(),
};
let mut base_table = match preset {
Some(p) => {
let preset_config = UrsulaConfig::from(p);
toml::Value::try_from(preset_config)
.map_err(|e| ConfigError::Other(format!("serialize preset: {e}")))?
.as_table()
.cloned()
.ok_or_else(|| ConfigError::Other("preset is not a table".into()))?
}
None => toml::Table::new(),
};
merge_tables(&mut base_table, user_table);
let mut config: UrsulaConfig = base_table.try_into()?;
if let Some(id) = node_id {
config.raft.node_id = id;
}
if config.raft.init_membership_per_group {
config.raft.init_membership = true;
}
config.validate()?;
Ok(config)
}
fn merge_tables(base: &mut toml::Table, user: toml::Table) {
for (key, user_value) in user {
match base.get_mut(&key) {
Some(toml::Value::Table(base_sub)) => {
if let toml::Value::Table(user_sub) = user_value {
merge_tables(base_sub, user_sub);
continue;
}
}
Some(toml::Value::Array(base_arr)) => {
if let toml::Value::Array(user_arr) = user_value {
*base_arr = user_arr;
continue;
}
}
_ => {}
}
base.insert(key, user_value);
}
}
#[cfg(test)]
pub fn merge_tables_for_test(base: &mut toml::Table, user: toml::Table) {
merge_tables(base, user);
}