use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
};
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use tokio::fs;
use toml::Value;
use toml_edit::DocumentMut;
#[derive(Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct LoadedConfig {
pub lock: Option<bool>,
pub template: Option<bool>,
pub set: Option<HashMap<String, HashMap<String, Value>>>,
pub vars: Option<HashMap<String, String>>,
pub command: Option<HashMap<String, Command>>,
pub brew: Option<Brew>,
pub remote: Option<Remote>,
#[serde(skip)]
pub path: PathBuf,
}
#[derive(Deserialize, Default)]
pub struct LockOnlyLoadedConfig {
pub lock: Option<bool>,
}
#[derive(Deserialize, PartialEq, Eq, Default, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct Remote {
pub url: String,
pub autosync: Option<bool>,
}
#[derive(Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct Command {
pub run: String,
pub ensure_first: Option<bool>,
pub required: Option<Vec<String>>,
pub flag: Option<bool>,
pub sudo: Option<bool>,
}
#[derive(Deserialize, PartialEq, Eq, Clone, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct Brew {
pub formulae: Option<HashSet<String>>,
pub casks: Option<HashSet<String>>,
pub taps: Option<HashSet<String>>,
pub no_deps: Option<bool>,
}
pub struct Config {
path: PathBuf,
}
impl Config {
#[must_use]
pub const fn new(path: PathBuf) -> Self {
Self { path }
}
#[must_use]
pub fn path(&self) -> &Path {
self.path.as_path()
}
#[must_use]
pub fn is_loadable(&self) -> bool {
!self.path.as_os_str().is_empty() && self.path.try_exists().unwrap_or_default()
}
pub async fn is_locked(&self) -> bool {
if self.is_loadable() {
let data = fs::read_to_string(&self.path).await;
match data {
Ok(data) => {
let cfg: LockOnlyLoadedConfig = toml::from_str(&data).unwrap_or_default();
cfg.lock.unwrap_or_default()
}
Err(_) => false,
}
} else {
false
}
}
pub async fn load(&self) -> Result<LoadedConfig> {
if self.is_loadable() {
let data = fs::read_to_string(&self.path).await?;
let mut config: LoadedConfig =
toml::from_str(&data).context("Failed to parse config data from valid TOML.")?;
config.path = self.path.to_owned();
Ok(config)
} else {
bail!("Config path does not exist!")
}
}
pub async fn load_as_mut(&self) -> Result<DocumentMut> {
if self.is_loadable() {
let data = fs::read_to_string(&self.path).await?;
let _: LoadedConfig =
toml::from_str(&data).context("Failed to parse config data from valid TOML.")?;
let doc = data.parse::<DocumentMut>()?;
Ok(doc)
} else {
bail!("Config path does not exist!")
}
}
}
pub trait ConfigCoreMethods {
fn save(&self, path: &Path) -> impl Future<Output = Result<()>>;
}
impl ConfigCoreMethods for DocumentMut {
async fn save(&self, path: &Path) -> Result<()> {
if let Some(dir) = path.parent() {
fs::create_dir_all(dir).await?;
}
let data = self.to_string();
fs::write(path, data).await?;
Ok(())
}
}