use std::collections::BTreeMap;
use std::io;
use std::path::PathBuf;
use std::str::FromStr;
use std::{borrow::Cow, env::VarError};
use anyhow::anyhow;
use serde::Serialize;
use super::plugin::{PluginFilter, PluginSet};
use crate::plugin::PluginMetadata;
use error::*;
pub struct Loader<'d> {
file: PathBuf,
default_provider: Option<Box<dyn DefaultConfigProvider + 'd>>,
save_default: bool,
overrides: Option<toml::Table>,
substitute_env: bool,
}
pub trait DefaultConfigProvider {
fn default_config(&self) -> anyhow::Result<toml::Table>;
fn default_config_string(&self) -> anyhow::Result<String> {
let config = self.default_config()?;
let string = toml::to_string_pretty(&toml::Value::Table(config))?;
Ok(string)
}
}
pub struct AutoDefaultConfigProvider<'p, A: Serialize, F: Fn() -> A> {
plugins: &'p PluginSet,
default_general_options: F,
}
pub struct NoDefaultConfigProvider;
impl<'d> Loader<'d> {
pub fn parse_file<P: Into<PathBuf>>(config_file: P) -> Self {
Self {
file: config_file.into(),
default_provider: None,
save_default: false,
overrides: None,
substitute_env: false,
}
}
pub fn or_default<D: DefaultConfigProvider + 'd>(mut self, default_provider: D, save_to_file: bool) -> Self {
self.default_provider = Some(Box::new(default_provider));
self.save_default = save_to_file;
self
}
pub fn or_default_boxed(
mut self,
default_provider: Box<dyn DefaultConfigProvider + 'd>,
save_to_file: bool,
) -> Self {
self.default_provider = Some(default_provider);
self.save_default = save_to_file;
self
}
pub fn with_override(mut self, config_override: toml::Table) -> Self {
match &mut self.overrides {
Some(existing) => merge_override(existing, config_override),
None => self.overrides = Some(config_override),
}
self
}
pub fn substitute_env_variables(mut self, substitute_env: bool) -> Self {
self.substitute_env = substitute_env;
self
}
pub fn load(mut self) -> Result<toml::Table, LoadError> {
self.load_impl().map_err(|e| LoadError {
config_file: self.file,
kind: e,
})
}
fn load_impl(&mut self) -> Result<toml::Table, LoadErrorCause> {
let config_content = self.read_config_or_default()?;
let config_content = substitute_env(&config_content)?;
let mut parsed_config = toml::Table::from_str(&config_content)?;
if let Some(overrides) = self.overrides.take() {
merge_override(&mut parsed_config, overrides);
}
Ok(parsed_config)
}
fn read_config_or_default(&mut self) -> Result<String, LoadErrorCause> {
match std::fs::read_to_string(&self.file) {
Ok(s) => Ok(s),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
if let Some(default_provider) = self.default_provider.take() {
let default_content = default_provider
.default_config_string()
.map_err(LoadErrorCause::DefaultProvider)?;
if self.save_default {
std::fs::write(&self.file, &default_content).map_err(LoadErrorCause::DefaultWrite)?;
}
Ok(default_content)
} else {
Err(LoadErrorCause::Read(e))
}
}
Err(e) => Err(LoadErrorCause::Read(e)),
}
}
}
impl<'f, F: Fn() -> anyhow::Result<toml::Table> + 'f> DefaultConfigProvider for F {
fn default_config(&self) -> anyhow::Result<toml::Table> {
let table = self()?;
Ok(table)
}
}
impl<'p, A: Serialize, F: Fn() -> A> AutoDefaultConfigProvider<'p, A, F> {
pub fn new(plugins: &'p PluginSet, default_general_options: F) -> Self {
Self {
plugins,
default_general_options,
}
}
}
impl<'p, A: Serialize, F: Fn() -> A> DefaultConfigProvider for AutoDefaultConfigProvider<'p, A, F> {
fn default_config(&self) -> anyhow::Result<toml::Table> {
let mut config = toml::Table::try_from((self.default_general_options)())?;
let plugins_table = generate_plugin_configs(self.plugins.metadata(PluginFilter::Enabled))?;
config.insert(String::from("plugins"), toml::Value::Table(plugins_table));
Ok(config)
}
}
impl DefaultConfigProvider for NoDefaultConfigProvider {
fn default_config(&self) -> anyhow::Result<toml::Table> {
Err(anyhow!("no default config available"))
}
}
pub fn substitute_env(mut input: &str) -> Result<Cow<str>, InvalidSubstitutionError> {
let first = input.find("${");
if first.is_none() {
return Ok(Cow::Borrowed(input));
}
let mut res = String::with_capacity(input.len());
let mut next = first;
while let Some(begin) = next {
let next_start;
if begin == 0 || input.as_bytes().get(begin - 1) != Some(&b'\\') {
res.push_str(&input[..begin]);
input = &input[begin..];
match input.find('}') {
None => {
return Err(InvalidSubstitutionError::WrongSyntax);
}
Some(end) => {
let env_var_name = &input[2..end];
match std::env::var(env_var_name) {
Ok(env_var_value) => {
res.push_str(&env_var_value);
}
Err(VarError::NotPresent) => {
return Err(InvalidSubstitutionError::Missing(env_var_name.to_owned()))
}
Err(VarError::NotUnicode(_)) => {
return Err(InvalidSubstitutionError::InvalidValue(env_var_name.to_owned()))
}
}
next_start = end + 1;
}
}
} else {
next_start = begin + 1;
res.push_str(&input[..(begin - 1)]);
res.push('$');
}
if let Some(more_input) = &input.get(next_start..) {
input = more_input;
next = input.find("${");
} else {
next = None;
}
}
res.push_str(input);
Ok(Cow::Owned(res))
}
pub fn merge_override(original: &mut toml::Table, overrider: toml::Table) {
for (key, value) in overrider.into_iter() {
match original.entry(key.clone()) {
toml::map::Entry::Vacant(vacant_entry) => {
vacant_entry.insert(value);
}
toml::map::Entry::Occupied(mut occupied_entry) => {
let existing_value = occupied_entry.get_mut();
match (existing_value, value) {
(toml::Value::Table(map), toml::Value::Table(map_override)) => {
merge_override(map, map_override);
}
(_, value) => {
occupied_entry.insert(value);
}
};
}
};
}
}
pub fn extract_plugins_config(config: &mut toml::Table) -> Result<BTreeMap<String, (bool, toml::Table)>, BadTypeError> {
fn process_plugin_config(
plugin_name: &str,
config_section: toml::Value,
) -> Result<(bool, toml::Table), BadTypeError> {
match config_section {
toml::Value::Table(mut plugin_config) => {
let enabled_val = plugin_config
.remove("enabled")
.or_else(|| plugin_config.remove("enable"))
.unwrap_or(toml::Value::Boolean(true));
let enabled = enabled_val.as_bool().ok_or_else(|| {
BadTypeError::new(format!("plugins.{}.enabled", plugin_name), "boolean", enabled_val)
})?;
Ok((enabled, plugin_config))
}
bad => {
Err(BadTypeError::new(format!("plugins.{}", plugin_name), "table", bad))
}
}
}
let plugins_table = match config.remove("plugins") {
Some(toml::Value::Table(t)) => Ok(t),
Some(bad) => Err(BadTypeError::new(String::from("plugins"), "table", bad)),
None => Ok(toml::Table::new()),
}?;
let mut res = BTreeMap::new();
for (plugin, section) in plugins_table {
let (enabled, config) = process_plugin_config(&plugin, section)?;
res.insert(plugin, (enabled, config));
}
Ok(res)
}
pub fn generate_plugin_configs<'p, I: IntoIterator<Item = &'p PluginMetadata>>(
plugins: I,
) -> Result<toml::Table, PluginDefaultConfigError> {
let plugins = plugins.into_iter();
let (lower, _) = plugins.size_hint();
let mut table = toml::Table::with_capacity(lower);
for p in plugins {
let plugin_config = (p.default_config)().map_err(|e| PluginDefaultConfigError {
plugin_name: p.name.clone(),
source: e,
})?;
if let Some(config) = plugin_config {
table.insert(p.name.clone(), toml::Value::Table(config.0));
}
}
Ok(table)
}
pub mod error {
use std::{io, path::PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
#[error("could not load config from '{config_file}'")]
pub struct LoadError {
pub config_file: PathBuf,
#[source]
pub(super) kind: LoadErrorCause,
}
#[derive(Error, Debug)]
pub(super) enum LoadErrorCause {
#[error("read failed")]
Read(#[source] io::Error),
#[error("default provider returned an error")]
DefaultProvider(#[source] anyhow::Error),
#[error("write (of default config) failed")]
DefaultWrite(#[source] io::Error),
#[error("env var substitution failed")]
Substitution(#[from] InvalidSubstitutionError),
#[error("invalid TOML config")]
InvalidToml(#[from] toml::de::Error),
}
#[derive(Error, Debug, PartialEq)]
pub enum InvalidSubstitutionError {
#[error("the environment variable {0} does not exist")]
Missing(String),
#[error("value of env var {0} is not valid UTF-8")]
InvalidValue(String),
#[error("env var name {0} is not valid")]
InvalidName(String),
#[error("wrong use of the substitution syntax, it should be ${{ENV_VAR}}")]
WrongSyntax,
}
#[derive(Error, Debug)]
#[error("unexpected type for {path}: expected {expected}, got {actual}")]
pub struct BadTypeError {
pub path: String,
pub expected: &'static str,
pub actual: &'static str,
}
impl BadTypeError {
pub fn new(path: String, expected: &'static str, actual: toml::Value) -> Self {
Self {
path,
expected,
actual: actual.type_str(),
}
}
}
#[derive(Error, Debug)]
#[error("plugin {plugin_name} failed to generate a default configuration")]
pub struct PluginDefaultConfigError {
pub plugin_name: String,
#[source]
pub(super) source: anyhow::Error,
}
}
#[cfg(test)]
mod tests_substitute_env {
use std::borrow::Cow;
use super::{substitute_env, InvalidSubstitutionError};
const ENV_VAR_NAME: &str = "CARGO_PKG_NAME";
const ENV_VAR_VALUE: &str = env!("CARGO_PKG_NAME");
const SUBSTITUTION: &str = "${CARGO_PKG_NAME}";
const ESCAPED_SUBST: &str = "\\${CARGO_PKG_NAME}";
#[test]
fn no_substitution() {
let input = "";
assert_eq!(Cow::Borrowed(input), substitute_env(input).unwrap());
let input = "
config_option = 123
[table]
list = [a, b, 'd', 1.5]
";
assert_eq!(Cow::Borrowed(input), substitute_env(input).unwrap());
let input = "
config_option = 123
[table]
list = [a, b, '$', 1.5]
";
assert_eq!(Cow::Borrowed(input), substitute_env(input).unwrap());
}
#[test]
fn basic() {
assert_eq!(
std::env::var(ENV_VAR_NAME).as_deref(),
Ok(ENV_VAR_VALUE),
"env var {} should be the same at compile-time and runtime",
ENV_VAR_NAME
);
let input = SUBSTITUTION;
let expected = ENV_VAR_VALUE;
assert_eq!(
expected,
substitute_env(input).unwrap(),
"wrong result on input: {}",
input
);
let input = format!("something${SUBSTITUTION}");
let expected = format!("something${ENV_VAR_VALUE}");
assert_eq!(expected, substitute_env(&input).unwrap());
let input = format!("${SUBSTITUTION}something");
let expected = format!("${ENV_VAR_VALUE}something");
assert_eq!(expected, substitute_env(&input).unwrap());
let input = format!("list = [a, b, '${SUBSTITUTION}', 1.5]");
let expected = input.replace(SUBSTITUTION, ENV_VAR_VALUE);
assert_eq!(expected, substitute_env(&input).unwrap());
}
#[test]
fn multiple() {
assert_eq!(
std::env::var(ENV_VAR_NAME).as_deref(),
Ok(ENV_VAR_VALUE),
"env var {} should be the same at compile-time and runtime",
ENV_VAR_NAME
);
let input = format!(
r#"
config_option = "${SUBSTITUTION}"
[table]
list = [a, b, '${SUBSTITUTION}', 1.5]
echo = "${SUBSTITUTION}${SUBSTITUTION}"
[[${SUBSTITUTION}.${SUBSTITUTION}]]
"#
);
let expected = input.replace(SUBSTITUTION, ENV_VAR_VALUE);
assert_eq!(expected, substitute_env(&input).unwrap());
}
#[test]
fn escaped() {
assert_eq!(
std::env::var(ENV_VAR_NAME).as_deref(),
Ok(ENV_VAR_VALUE),
"env var {} should be the same at compile-time and runtime",
ENV_VAR_NAME
);
let input = ESCAPED_SUBST;
let expected = SUBSTITUTION;
assert_eq!(
expected,
substitute_env(input).unwrap(),
"wrong result on input: {}",
input
);
let input = format!("something${ESCAPED_SUBST}");
let expected = format!("something${SUBSTITUTION}");
assert_eq!(expected, substitute_env(&input).unwrap());
let input = format!("${ESCAPED_SUBST}something");
let expected = format!("${SUBSTITUTION}something");
assert_eq!(expected, substitute_env(&input).unwrap());
let input = format!("${ESCAPED_SUBST}${ESCAPED_SUBST}");
let expected = format!("${SUBSTITUTION}${SUBSTITUTION}");
assert_eq!(expected, substitute_env(&input).unwrap());
}
#[test]
fn escaped_unescaped_mix() {
let input = format!(" ${ESCAPED_SUBST} ${SUBSTITUTION}");
let expected = format!(" ${SUBSTITUTION} ${ENV_VAR_VALUE}");
assert_eq!(expected, substitute_env(&input).unwrap());
}
#[test]
fn unclosed() {
let input = "${";
assert_eq!(substitute_env(input), Err(InvalidSubstitutionError::WrongSyntax));
let input = "abc${";
assert_eq!(substitute_env(input), Err(InvalidSubstitutionError::WrongSyntax));
let input = "${UNCLOSED_VAR ${";
assert_eq!(substitute_env(input), Err(InvalidSubstitutionError::WrongSyntax));
let input = "k = true\n${UNCLOSED_VAR\ntest = 1";
assert_eq!(substitute_env(input), Err(InvalidSubstitutionError::WrongSyntax));
}
}