Skip to content

Commit

Permalink
support multi-config with toml config file
Browse files Browse the repository at this point in the history
  • Loading branch information
solidiquis committed Jun 26, 2023
1 parent d841f3d commit b9d09c6
Show file tree
Hide file tree
Showing 11 changed files with 712 additions and 106 deletions.
303 changes: 303 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ ansi_term = "0.12.1"
chrono = "0.4.24"
clap = { version = "4.1.1", features = ["derive"] }
clap_complete = "4.1.1"
config = { version = "0.13.3", features = ["toml"] }
crossterm = "0.26.1"
dirs = "5.0"
errno = "0.3.1"
Expand Down
22 changes: 22 additions & 0 deletions src/context/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const ERDTREE_CONFIG_TOML: &str = ".erdtree.toml";
const ERDTREE_TOML_PATH: &str = "ERDTREE_TOML_PATH";

const ERDTREE_CONFIG_NAME: &str = ".erdtreerc";
const ERDTREE_CONFIG_PATH: &str = "ERDTREE_CONFIG_PATH";

const ERDTREE_DIR: &str = "erdtree";

#[cfg(unix)]
const CONFIG_DIR: &str = ".config";

#[cfg(unix)]
const HOME: &str = "HOME";

#[cfg(unix)]
const XDG_CONFIG_HOME: &str = "XDG_CONFIG_HOME";

/// Concerned with loading `.erdtreerc`.
pub mod rc;

/// Concerned with loading `.erdtree.toml`.
pub mod toml;
58 changes: 20 additions & 38 deletions src/context/config.rs → src/context/config/rc.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,4 @@
use std::{
env, fs,
path::{Path, PathBuf},
};

const ERDTREE_CONFIG_NAME: &str = ".erdtreerc";
const ERDTREE_CONFIG_PATH: &str = "ERDTREE_CONFIG_PATH";
const ERDTREE_DIR: &str = "erdtree";

#[cfg(unix)]
const CONFIG_DIR: &str = ".config";

#[cfg(unix)]
const HOME: &str = "HOME";

#[cfg(unix)]
const XDG_CONFIG_HOME: &str = "XDG_CONFIG_HOME";
use std::{env, fs, path::PathBuf};

/// Reads the config file into a `String` if there is one. When `None` is provided then the config
/// is looked for in the following locations in order:
Expand All @@ -25,25 +9,19 @@ const XDG_CONFIG_HOME: &str = "XDG_CONFIG_HOME";
/// - `$HOME/.config/erdtree/.erdtreerc`
/// - `$HOME/.erdtreerc`
#[cfg(unix)]
pub fn read_config_to_string<T: AsRef<Path>>(path: Option<T>) -> Option<String> {
path.map(fs::read_to_string)
.and_then(Result::ok)
.or_else(config_from_config_path)
pub fn read_config_to_string() -> Option<String> {
config_from_config_path()
.or_else(config_from_xdg_path)
.or_else(config_from_home)
.map(|e| prepend_arg_prefix(&e))
}

/// Reads the config file into a `String` if there is one. When `None` is provided then the config
/// is looked for in the following locations in order (Windows specific):
///
/// - `$ERDTREE_CONFIG_PATH`
/// - `%APPDATA%/erdtree/.erdtreerc`
#[cfg(windows)]
pub fn read_config_to_string<T: AsRef<Path>>(path: Option<T>) -> Option<String> {
path.map(fs::read_to_string)
.and_then(Result::ok)
.or_else(config_from_config_path)
pub fn read_config_to_string() -> Option<String> {
config_from_config_path()
.or_else(config_from_appdata)
.map(|e| prepend_arg_prefix(&e))
}
Expand All @@ -61,13 +39,13 @@ pub fn parse<'a>(config: &'a str) -> Vec<&'a str> {
.next()
.map_or(true, |ch| ch != '#')
})
.flat_map(str::split_ascii_whitespace)
.flat_map(str::split_whitespace)
.collect::<Vec<&'a str>>()
}

/// Try to read in config from `ERDTREE_CONFIG_PATH`.
fn config_from_config_path() -> Option<String> {
env::var_os(ERDTREE_CONFIG_PATH)
env::var_os(super::ERDTREE_CONFIG_PATH)
.map(PathBuf::from)
.map(fs::read_to_string)
.and_then(Result::ok)
Expand All @@ -78,15 +56,15 @@ fn config_from_config_path() -> Option<String> {
/// - `$HOME/.erdtreerc`
#[cfg(not(windows))]
fn config_from_home() -> Option<String> {
let home = env::var_os(HOME).map(PathBuf::from)?;
let home = env::var_os(super::HOME).map(PathBuf::from)?;

let config_path = home
.join(CONFIG_DIR)
.join(ERDTREE_DIR)
.join(ERDTREE_CONFIG_NAME);
.join(super::CONFIG_DIR)
.join(super::ERDTREE_DIR)
.join(super::ERDTREE_CONFIG_NAME);

fs::read_to_string(config_path).ok().or_else(|| {
let config_path = home.join(ERDTREE_CONFIG_NAME);
let config_path = home.join(super::ERDTREE_CONFIG_NAME);
fs::read_to_string(config_path).ok()
})
}
Expand All @@ -97,7 +75,9 @@ fn config_from_home() -> Option<String> {
fn config_from_appdata() -> Option<String> {
let app_data = dirs::config_dir()?;

let config_path = app_data.join(ERDTREE_DIR).join(ERDTREE_CONFIG_NAME);
let config_path = app_data
.join(super::ERDTREE_DIR)
.join(super::ERDTREE_CONFIG_NAME);

fs::read_to_string(config_path).ok()
}
Expand All @@ -107,12 +87,14 @@ fn config_from_appdata() -> Option<String> {
/// - `$XDG_CONFIG_HOME/.erdtreerc`
#[cfg(unix)]
fn config_from_xdg_path() -> Option<String> {
let xdg_config = env::var_os(XDG_CONFIG_HOME).map(PathBuf::from)?;
let xdg_config = env::var_os(super::XDG_CONFIG_HOME).map(PathBuf::from)?;

let config_path = xdg_config.join(ERDTREE_DIR).join(ERDTREE_CONFIG_NAME);
let config_path = xdg_config
.join(super::ERDTREE_DIR)
.join(super::ERDTREE_CONFIG_NAME);

fs::read_to_string(config_path).ok().or_else(|| {
let config_path = xdg_config.join(ERDTREE_CONFIG_NAME);
let config_path = xdg_config.join(super::ERDTREE_CONFIG_NAME);
fs::read_to_string(config_path).ok()
})
}
Expand Down
19 changes: 19 additions & 0 deletions src/context/config/toml/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use config::ConfigError;

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to load .erdtree.toml")]
LoadConfig,

#[error("The configuration file is improperly formatted")]
InvalidFormat(#[from] ConfigError),

#[error("Alternate configuration '{0}' was not found in '.erdtree.toml'")]
MissingAltConfig(String),

#[error("'#{0}' is required to be a pointer-sized unsigned integer type")]
InvalidInteger(String),

#[error("'#{0}' has a type that is invalid")]
InvalidArgument(String),
}
222 changes: 222 additions & 0 deletions src/context/config/toml/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
use config::{Config, File, Value, ValueKind};
use error::Error;
use std::{env, ffi::OsString};

/// Errors associated with loading and parsing the toml config file.
pub mod error;

/// Testing related to `.erdtree.toml`.
pub mod test;

/// Represents an instruction on how to handle a single key-value pair, which makes up a single
/// command-line argument, when constructing the arguments vector.
enum ArgInstructions {
/// Used for bool arguments such as `--icons`. When `icons = true` is set in `.erdtree.toml`,
/// we only want `--icons` to be pushed into the ultimate arguments vector.
PushKeyOnly,

/// Used for arguments such as `--threads 10`.
PushKeyValue { parsed_value: OsString },

/// If a bool field is set to false in `.erdtree.toml` (e.g. `icons = false`) then we want to
/// completely omit the key-value pair from the arguments that we ultimately use.
Pass,
}

/// Takes in a `Config` that is generated from [`load_toml`] returning a `Vec<OsString>` which
/// represents command-line arguments from `.erdtree.toml`. If a `nested_table` is provided then
/// the top-level table in `.erdtree.toml` is ignored and the configurations specified in the
/// `nested_table` will be used instead.
pub fn parse(config: Config, nested_table: Option<&str>) -> Result<Vec<OsString>, Error> {
let mut args_map = config.cache.into_table()?;

if let Some(table) = nested_table {
let new_conf = args_map
.get(table)
.and_then(|conf| conf.clone().into_table().ok())
.ok_or_else(|| Error::MissingAltConfig(table.to_owned()))?;

args_map = new_conf;
} else {
args_map.retain(|_k, v| !matches!(v.kind, ValueKind::Table(_)));
}

let mut parsed_args = vec![OsString::from("--")];

let process_key = |s| OsString::from(format!("--{s}").replace('_', "-"));

for (k, v) in &args_map {
match parse_argument(k, v)? {
ArgInstructions::PushKeyValue { parsed_value } => {
let fmt_key = process_key(k);
parsed_args.push(fmt_key);
parsed_args.push(parsed_value);
}

ArgInstructions::PushKeyOnly => {
let fmt_key = process_key(k);
parsed_args.push(fmt_key);
}

ArgInstructions::Pass => continue,
}
}

Ok(parsed_args)
}

/// Reads in `.erdtree.toml` file.
pub fn load() -> Result<Config, Error> {
#[cfg(windows)]
return windows::load_toml().ok_or(Error::LoadConfig);

#[cfg(unix)]
unix::load_toml().ok_or(Error::LoadConfig)
}

/// Attempts to load in `.erdtree.toml` from `$ERDTREE_TOML_PATH`. Will return `None` for whatever
/// reason.
fn toml_from_env() -> Option<Config> {
let config = env::var_os(super::ERDTREE_TOML_PATH)
.map(OsString::into_string)
.and_then(Result::ok)?;

let file = config.strip_suffix(".toml").map(File::with_name)?;

Config::builder().add_source(file).build().ok()
}

/// Simple utility used to extract the underlying value from the [`Value`] enum that we get when
/// loading in the values from `.erdtree.toml`, returning instructions on how the argument should
/// be processed into the ultimate arguments vector.
fn parse_argument(keyword: &str, arg: &Value) -> Result<ArgInstructions, Error> {
macro_rules! try_parse_num {
($n:expr) => {
usize::try_from($n)
.map_err(|_e| Error::InvalidInteger(keyword.to_owned()))
.map(|num| {
let parsed = OsString::from(format!("{num}"));
ArgInstructions::PushKeyValue {
parsed_value: parsed,
}
})
};
}

match &arg.kind {
ValueKind::Boolean(val) => {
if *val {
Ok(ArgInstructions::PushKeyOnly)
} else {
Ok(ArgInstructions::Pass)
}
}
ValueKind::String(val) => Ok(ArgInstructions::PushKeyValue {
parsed_value: OsString::from(val),
}),
ValueKind::I64(val) => try_parse_num!(*val),
ValueKind::I128(val) => try_parse_num!(*val),
ValueKind::U64(val) => try_parse_num!(*val),
ValueKind::U128(val) => try_parse_num!(*val),
_ => Err(Error::InvalidArgument(keyword.to_owned())),
}
}

#[cfg(unix)]
mod unix {
use super::super::{CONFIG_DIR, ERDTREE_CONFIG_TOML, ERDTREE_DIR, HOME, XDG_CONFIG_HOME};
use config::{Config, File};
use std::{env, path::PathBuf};

/// Looks for `.erdtree.toml` in the following locations in order:
///
/// - `$ERDTREE_TOML_PATH`
/// - `$XDG_CONFIG_HOME/erdtree/.erdtree.toml`
/// - `$XDG_CONFIG_HOME/.erdtree.toml`
/// - `$HOME/.config/erdtree/.erdtree.toml`
/// - `$HOME/.erdtree.toml`
pub(super) fn load_toml() -> Option<Config> {
super::toml_from_env()
.or_else(toml_from_xdg_path)
.or_else(toml_from_home)
}

/// Looks for `.erdtree.toml` in the following locations in order:
///
/// - `$XDG_CONFIG_HOME/erdtree/.erdtree.toml`
/// - `$XDG_CONFIG_HOME/.erdtree.toml`
fn toml_from_xdg_path() -> Option<Config> {
let config = env::var_os(XDG_CONFIG_HOME).map(PathBuf::from)?;

let mut file = config
.join(ERDTREE_DIR)
.join(ERDTREE_CONFIG_TOML)
.to_str()
.and_then(|s| s.strip_suffix(".toml"))
.map(File::with_name);

if file.is_none() {
file = config
.join(ERDTREE_CONFIG_TOML)
.to_str()
.and_then(|s| s.strip_suffix(".toml"))
.map(File::with_name);
}

Config::builder().add_source(file?).build().ok()
}

/// Looks for `.erdtree.toml` in the following locations in order:
///
/// - `$HOME/.config/erdtree/.erdtree.toml`
/// - `$HOME/.erdtree.toml`
fn toml_from_home() -> Option<Config> {
let home = env::var_os(HOME).map(PathBuf::from)?;

let mut file = home
.join(CONFIG_DIR)
.join(ERDTREE_DIR)
.join(ERDTREE_CONFIG_TOML)
.to_str()
.and_then(|s| s.strip_suffix(".toml"))
.map(File::with_name);

if file.is_none() {
file = home
.join(ERDTREE_CONFIG_TOML)
.to_str()
.and_then(|s| s.strip_suffix(".toml"))
.map(File::with_name);
}

Config::builder().add_source(file?).build().ok()
}
}

#[cfg(windows)]
mod windows {
use super::super::{ERDTREE_CONFIG_TOML, ERDTREE_DIR};
use config::{Config, File};
use std::{env, path::PathBuf};

/// Try to read in config from the following location:
/// - `%APPDATA%/erdtree/.erdtreerc`
pub(super) fn load_toml() -> Option<Config> {
super::toml_from_env().or_else(toml_from_appdata)
}

/// Try to read in config from the following location:
/// - `%APPDATA%/erdtree/.erdtreerc`
fn toml_from_appdata() -> Option<Config> {
let app_data = dirs::config_dir()?;

let file = app_data
.join(ERDTREE_DIR)
.join(ERDTREE_CONFIG_TOML)
.to_str()
.and_then(|s| s.strip_prefix(".toml"))
.map(File::with_name)?;

Config::builder().add_source(file).build().ok()
}
}
Loading

0 comments on commit b9d09c6

Please sign in to comment.