use super::{literal_aliases, setting_for_key, settings_meta};
use miette::{Context, IntoDiagnostic, miette};
use std::path::{Path, PathBuf};
pub(super) struct AubeConfigEdit {
table: toml::map::Map<String, toml::Value>,
}
impl AubeConfigEdit {
pub(super) fn load(path: &Path) -> miette::Result<Self> {
let raw = match std::fs::read_to_string(path) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Self {
table: toml::map::Map::new(),
});
}
Err(e) => {
return Err(e)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read {}", path.display()));
}
};
let value = raw
.parse::<toml::Value>()
.into_diagnostic()
.wrap_err_with(|| format!("failed to parse {}", path.display()))?;
let toml::Value::Table(table) = value else {
return Err(miette!("{} must contain a TOML table", path.display()));
};
Ok(Self { table })
}
pub(super) fn entries(&self) -> Vec<(String, String)> {
self.table
.iter()
.filter_map(|(key, value)| toml_value_to_raw(value).map(|raw| (key.clone(), raw)))
.collect()
}
pub(super) fn set(
&mut self,
meta: &settings_meta::SettingMeta,
raw: &str,
) -> miette::Result<()> {
let value = raw_to_toml_value(meta, raw)?;
for alias in literal_aliases(meta.npmrc_keys) {
self.table.remove(&alias);
}
self.table.insert(meta.name.to_string(), value);
Ok(())
}
pub(super) fn remove_aliases(&mut self, aliases: &[String]) -> bool {
let before = self.table.len();
for alias in aliases {
self.table.remove(alias);
}
before != self.table.len()
}
pub(super) fn save(&self, path: &Path) -> miette::Result<()> {
let out = toml::to_string_pretty(&self.table)
.into_diagnostic()
.wrap_err("failed to serialize aube config")?;
aube_util::fs_atomic::atomic_write(path, out.as_bytes())
.into_diagnostic()
.wrap_err_with(|| format!("failed to write {}", path.display()))
}
}
pub(crate) fn user_aube_config_path() -> miette::Result<PathBuf> {
if let Some(dir) = aube_util::env::xdg_config_home() {
return Ok(dir.join("aube").join("config.toml"));
}
let home = aube_util::env::home_dir().ok_or_else(|| {
miette!("could not locate home directory. set HOME or USERPROFILE to point at aube config")
})?;
Ok(home.join(".config").join("aube").join("config.toml"))
}
pub(crate) fn load_user_entries() -> Vec<(String, String)> {
let Ok(path) = user_aube_config_path() else {
return Vec::new();
};
match AubeConfigEdit::load(&path) {
Ok(edit) => edit.entries(),
Err(err) => {
tracing::warn!("failed to load aube config at {}: {err}", path.display());
Vec::new()
}
}
}
pub(super) fn is_aube_config_key(key: &str) -> Option<&'static settings_meta::SettingMeta> {
let meta = setting_for_key(key)?;
is_aube_config_setting(meta).then_some(meta)
}
fn is_aube_config_setting(meta: &settings_meta::SettingMeta) -> bool {
!meta.typed_accessor_unused
&& (matches!(
meta.type_,
"bool" | "string" | "path" | "url" | "int" | "list<string>"
) || meta.type_.starts_with('"'))
}
fn raw_to_toml_value(meta: &settings_meta::SettingMeta, raw: &str) -> miette::Result<toml::Value> {
match meta.type_ {
"bool" => aube_settings::parse_bool(raw)
.map(toml::Value::Boolean)
.ok_or_else(|| miette!("{} expects a boolean value", meta.name)),
"int" => raw
.trim()
.parse::<i64>()
.map(toml::Value::Integer)
.map_err(|_| miette!("{} expects an integer value", meta.name)),
"list<string>" => Ok(toml::Value::Array(
parse_string_list(raw)
.into_iter()
.map(toml::Value::String)
.collect(),
)),
_ => Ok(toml::Value::String(raw.to_string())),
}
}
fn toml_value_to_raw(value: &toml::Value) -> Option<String> {
match value {
toml::Value::String(s) => Some(s.clone()),
toml::Value::Integer(n) => Some(n.to_string()),
toml::Value::Float(n) => Some(n.to_string()),
toml::Value::Boolean(b) => Some(b.to_string()),
toml::Value::Array(items) => {
let values: Vec<String> = items.iter().filter_map(toml_value_to_raw).collect();
Some(values.join(","))
}
toml::Value::Datetime(d) => Some(d.to_string()),
toml::Value::Table(_) => None,
}
}
fn parse_string_list(raw: &str) -> Vec<String> {
let trimmed = raw.trim();
if let Some(inner) = trimmed.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
return inner
.split(',')
.map(|s| s.trim().trim_matches(['"', '\'']).to_string())
.filter(|s| !s.is_empty())
.collect();
}
trimmed
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aube_config_roundtrips_typed_entries() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
let meta = settings_meta::find("minimumReleaseAge").unwrap();
let mut edit = AubeConfigEdit::load(&path).unwrap();
edit.set(meta, "2880").unwrap();
edit.save(&path).unwrap();
let edit = AubeConfigEdit::load(&path).unwrap();
assert_eq!(
edit.entries(),
vec![("minimumReleaseAge".to_string(), "2880".to_string())]
);
}
}