use std::{fs, path::Path, sync::Arc};
use toml::Value;
use crate::{
error::Result,
store::{DefaultStore, Store},
};
pub struct Config<S = DefaultStore> {
store: Arc<S>,
}
impl<S> Clone for Config<S> {
fn clone(&self) -> Self {
Self {
store: Arc::clone(&self.store),
}
}
}
impl Config {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::from_file_with(path)
}
pub fn from_toml(content: &str) -> Result<Self> {
Self::from_toml_with(content)
}
}
macro_rules! impl_getters {
($( $name:ident => $ret:ty, $method:ident, $doc:literal );* $(;)?) => {
$(
#[doc = $doc]
#[must_use]
pub fn $name(&self, key: &str) -> Option<$ret> {
self.get(key).and_then(Value::$method)
}
)*
};
}
impl<S> Config<S>
where
S: Store,
{
pub fn from_file_with<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = fs::read_to_string(path)?;
Self::from_toml_with(&content)
}
pub fn from_toml_with(content: &str) -> Result<Self> {
let root: Value = toml::from_str(content)?;
let mut store = S::default();
flatten_value(&mut store, "", root);
Ok(Self {
store: Arc::new(store),
})
}
#[must_use]
pub fn shared(&self) -> Self {
self.clone()
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&Value> {
self.store.get(key)
}
impl_getters! {
get_string => &str, as_str, "Helper to get a String value.";
get_int => i64, as_integer, "Helper to get an integer value.";
get_float => f64, as_float, "Helper to get a float value.";
get_bool => bool, as_bool, "Helper to get a boolean value.";
}
pub fn flatten(&self) -> impl Iterator<Item = (String, String)> + '_ {
self.store.iter().map(|(k, v)| {
let value = v
.as_str()
.map_or_else(|| v.to_string(), ToString::to_string);
(k.clone(), value)
})
}
#[must_use]
pub fn flatten_into<C>(&self) -> C
where
C: FromIterator<(String, String)>,
{
self.flatten().collect()
}
}
fn flatten_value<S: Store>(store: &mut S, prefix: &str, value: Value) {
match value {
Value::Table(table) => {
for (k, v) in table {
let key = if prefix.is_empty() {
k
} else {
format!("{prefix}.{k}")
};
flatten_value(store, &key, v);
}
}
Value::Array(array) => {
let is_table_array = array.first().is_some_and(Value::is_table);
if is_table_array {
for (i, v) in array.into_iter().enumerate() {
let key = format!("{prefix}[{i}]");
flatten_value(store, &key, v);
}
} else if !prefix.is_empty() {
store.insert(prefix.to_string(), Value::Array(array));
}
}
other => {
if !prefix.is_empty() {
store.insert(prefix.to_string(), other);
}
}
}
}