use std::{
env, fs,
path::{Path, PathBuf},
};
use thiserror::Error;
use toml::Table;
use crate::deps::Dependency;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("Config file not found: {0}")]
NotFound(PathBuf),
#[error("Failed to read config at {}: {source}", path.display())]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to parse config at {}: {source}", path.display())]
Parse {
path: PathBuf,
#[source]
source: toml::de::Error,
},
#[error("Config File is missing dependencies")]
MissingDeps,
#[error("Key '{key}' at {path} should not be in double quotes")]
InvalidKey { path: PathBuf, key: String },
#[error("Value for key '{key}' at {path} must be a quoted string")]
InvalidValue { path: PathBuf, key: String },
}
pub struct Config {
pub deps: Vec<Dependency>,
}
impl Config {
pub fn load(config_path: Option<&Path>) -> Result<Config, ConfigError> {
let path = config_path
.map(PathBuf::from)
.unwrap_or_else(Self::default_path);
if !path.exists() {
return Err(ConfigError::NotFound(path));
}
let content = fs::read_to_string(&path).map_err(|source| ConfigError::Read {
path: path.clone(),
source,
})?;
let deps_table: Table = content.parse().map_err(|err| ConfigError::Parse {
path: path.clone(),
source: err,
})?;
Self::validate_config(deps_table, path)
}
pub fn init_config(config_path: Option<&Path>) -> Result<PathBuf, String> {
let config_path = config_path
.map(PathBuf::from)
.unwrap_or_else(Self::default_path);
if let Some(config_dir) = config_path.parent().filter(|p| !p.as_os_str().is_empty())
&& !config_dir.exists()
{
fs::create_dir_all(config_dir)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
}
if config_path.exists() {
return Err(format!(
"Config file already exists at {}",
config_path.display()
));
}
let default_config = r#"[deps]"#;
fs::write(&config_path, default_config)
.map_err(|e| format!("Failed to write config file: {}", e))?;
Ok(config_path)
}
fn default_path() -> PathBuf {
PathBuf::from(env::var("HOME").unwrap_or_default()).join(".config/upsft/config.toml")
}
fn validate_config(table: Table, config_path: PathBuf) -> Result<Config, ConfigError> {
let deps = table
.get("deps")
.and_then(|v| v.as_table())
.ok_or(ConfigError::MissingDeps)?;
let mut validated_deps: Vec<Dependency> = Vec::new();
for (key, value) in deps.iter() {
let update_command = value.as_str().ok_or_else(|| ConfigError::InvalidValue {
path: config_path.clone(),
key: key.clone(),
})?;
let deps = Dependency::new(key.clone(), update_command.to_owned());
validated_deps.push(deps);
}
let validated_config = Config {
deps: validated_deps,
};
Ok(validated_config)
}
}