use std::path::PathBuf;
use std::{env::current_dir, path::Path};
use anyhow::Context;
use clap::ValueEnum;
use comfy_table::Table;
use config::Config;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::paths::berg_config_dir;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum ConfigLocation {
Global,
Local,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BergConfig {
pub base_url: String,
pub protocol: String,
pub fancy_tables: bool,
pub no_color: bool,
}
impl BergConfig {
fn iter_default_field_value() -> impl Iterator<Item = (&'static str, config::Value)> {
let BergConfig {
base_url,
protocol,
fancy_tables,
no_color,
} = Default::default();
[
("base_url", config::Value::from(base_url)),
("protocol", config::Value::from(protocol)),
("fancy_tables", config::Value::from(fancy_tables)),
("no_color", config::Value::from(no_color)),
]
.into_iter()
}
}
impl Default for BergConfig {
fn default() -> Self {
Self {
base_url: String::from("codeberg.org/"),
protocol: String::from("https"),
fancy_tables: true,
no_color: false,
}
}
}
impl BergConfig {
pub fn new() -> anyhow::Result<Self> {
let config = Self::raw()?.try_deserialize::<BergConfig>()?;
Ok(config)
}
pub fn raw() -> anyhow::Result<Config> {
let local_config_path = current_dir().map(add_berg_config_file)?;
let global_config_path = berg_config_dir().map(add_berg_config_file)?;
let mut config_builder = Config::builder();
config_builder = config_builder.add_source(file_from_path(global_config_path.as_path()));
tracing::debug!("config search in: {global_config_path:?}");
let mut walk_up = local_config_path.clone();
let walking_up = std::iter::from_fn(move || {
walk_up
.parent()
.and_then(|parent| parent.parent())
.map(add_berg_config_file)
.map(|parent| {
walk_up = parent.clone();
parent
})
});
let pwd = std::iter::once(local_config_path);
let local_paths = pwd.chain(walking_up).collect::<Vec<_>>();
for path in local_paths.iter().rev() {
tracing::debug!("config search in: {path:?}");
config_builder = config_builder.add_source(file_from_path(path));
}
config_builder = config_builder.add_source(config::Environment::with_prefix("BERG"));
for (field_name, default_value) in BergConfig::iter_default_field_value() {
config_builder = config_builder.set_default(field_name, default_value)?;
}
config_builder.build().map_err(anyhow::Error::from)
}
}
impl BergConfig {
pub fn url(&self) -> anyhow::Result<Url> {
let url = format!(
"{protoc}://{url}",
protoc = self.protocol,
url = self.base_url
);
Url::parse(url.as_str())
.context("The protocol + base url in the config don't add up to a valid url")
}
pub fn make_table(&self) -> Table {
let mut table = Table::new();
let preset = if self.fancy_tables {
comfy_table::presets::UTF8_FULL
} else {
table.set_width(u16::MAX);
comfy_table::presets::NOTHING
};
table.load_preset(preset);
table
}
}
fn file_from_path(
path: impl AsRef<Path>,
) -> config::File<config::FileSourceFile, config::FileFormat> {
config::File::new(
path.as_ref().to_str().unwrap_or_default(),
config::FileFormat::Toml,
)
.required(false)
}
fn add_berg_config_file(dir: impl AsRef<Path>) -> PathBuf {
dir.as_ref().join("berg.toml")
}
#[cfg(test)]
mod tests {
use super::*;
fn make_config(path: PathBuf, config: BergConfig) -> anyhow::Result<()> {
let config_path = add_berg_config_file(path);
let toml = toml::to_string(&config)?;
std::fs::write(config_path, toml)?;
Ok(())
}
fn delete_config(path: PathBuf) -> anyhow::Result<()> {
let config_path = add_berg_config_file(path);
std::fs::remove_file(config_path)?;
Ok(())
}
#[test]
#[ignore = "doesn't work on nix in 'ci' because of no r/w permissions on the system"]
fn berg_config_integration_test() -> anyhow::Result<()> {
let local_dir = current_dir()?;
std::fs::create_dir_all(local_dir)?;
let global_dir = berg_config_dir()?;
std::fs::create_dir_all(global_dir)?;
let config = BergConfig {
base_url: String::from("local"),
..Default::default()
};
make_config(current_dir()?, config)?;
let config = BergConfig::new();
assert!(config.is_ok(), "{config:?}");
let config = config.unwrap();
assert_eq!(config.base_url.as_str(), "local");
delete_config(current_dir()?)?;
let config = BergConfig {
base_url: String::from("global"),
..Default::default()
};
make_config(berg_config_dir()?, config)?;
let config = BergConfig::new();
assert!(config.is_ok(), "{config:?}");
let config = config.unwrap();
assert_eq!(config.base_url.as_str(), "global", "{0:?}", config.base_url);
delete_config(berg_config_dir()?)?;
let config = BergConfig::new();
assert!(config.is_ok(), "{config:?}");
let config = config.unwrap();
assert_eq!(config.base_url.as_str(), "codeberg.org/");
{
let config = BergConfig {
base_url: String::from("local"),
..Default::default()
};
make_config(current_dir()?, config)?;
}
{
let config = BergConfig {
base_url: String::from("global"),
..Default::default()
};
make_config(berg_config_dir()?, config)?;
}
let config = BergConfig::new();
assert!(config.is_ok(), "{config:?}");
let config = config.unwrap();
assert_eq!(config.base_url.as_str(), "local");
delete_config(current_dir()?)?;
delete_config(berg_config_dir()?)?;
Ok(())
}
}