use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use crate::{error::SourceError, format::Format, value::ConfigValue};
pub trait Source {
fn collect(&self) -> Result<ConfigValue, SourceError>;
#[must_use]
fn with_app_name(self, _name: &str) -> Self
where
Self: Sized,
{
self
}
}
#[derive(Default)]
pub struct EnvSource {
prefix: String,
}
impl From<String> for EnvSource {
fn from(prefix: String) -> Self {
Self { prefix }
}
}
impl From<&str> for EnvSource {
fn from(prefix: &str) -> Self {
Self {
prefix: prefix.to_owned(),
}
}
}
impl Source for EnvSource {
fn with_app_name(mut self, name: &str) -> Self {
if self.prefix.is_empty() {
name.clone_into(&mut self.prefix);
}
self
}
fn collect(&self) -> Result<ConfigValue, SourceError> {
if self.prefix.is_empty() {
return Ok(ConfigValue::Table(BTreeMap::new()));
}
let prefix = format!("{}_", self.prefix.to_uppercase());
let mut table = BTreeMap::new();
for (key, val) in std::env::vars() {
let Some(stripped) = key.strip_prefix(&prefix) else {
continue;
};
let path: Vec<String> = stripped
.to_lowercase()
.split("__")
.map(String::from)
.collect();
insert_nested(&mut table, &path, val.parse().unwrap());
}
Ok(ConfigValue::Table(table))
}
}
fn insert_nested(table: &mut BTreeMap<String, ConfigValue>, path: &[String], value: ConfigValue) {
let Some((head, tail)) = path.split_first() else {
return;
};
if tail.is_empty() {
table.insert(head.clone(), value);
return;
}
let entry = table
.entry(head.clone())
.or_insert_with(|| ConfigValue::Table(BTreeMap::new()));
if let ConfigValue::Table(inner) = entry {
insert_nested(inner, tail, value);
}
}
pub struct CliSource {
values: BTreeMap<String, ConfigValue>,
}
impl FromIterator<(String, ConfigValue)> for CliSource {
fn from_iter<I: IntoIterator<Item = (String, ConfigValue)>>(iter: I) -> Self {
Self {
values: iter.into_iter().collect(),
}
}
}
impl Source for CliSource {
fn collect(&self) -> Result<ConfigValue, SourceError> {
Ok(ConfigValue::Table(self.values.clone()))
}
}
pub struct FileSource<F: Format> {
candidates: Vec<PathBuf>,
format: F,
}
impl<F: Format> FileSource<F> {
#[must_use]
pub fn new(candidates: impl IntoIterator<Item = PathBuf>, format: F) -> Self {
Self {
candidates: candidates.into_iter().collect(),
format,
}
}
#[must_use]
pub fn resolve(&self) -> Option<&Path> {
self.candidates
.iter()
.find(|p| p.exists())
.map(PathBuf::as_path)
}
}
impl<F: Format> Source for FileSource<F> {
fn collect(&self) -> Result<ConfigValue, SourceError> {
let Some(path) = self.resolve() else {
return Ok(ConfigValue::Table(BTreeMap::new()));
};
let bytes = std::fs::read(path).map_err(|e| SourceError::Io {
path: path.to_owned(),
source: e,
})?;
self.format.parse(path, &bytes)
}
}
#[cfg(feature = "hot-reload")]
pub trait WatchableSource: Source + Send + Sync {
fn watch_paths(&self) -> Vec<std::path::PathBuf>;
}
#[cfg(feature = "hot-reload")]
impl<F: Format + Send + Sync> WatchableSource for FileSource<F> {
fn watch_paths(&self) -> Vec<std::path::PathBuf> {
self.candidates.clone()
}
}
#[cfg(feature = "async")]
pub type CollectFuture<'a> = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<ConfigValue, SourceError>> + Send + 'a>,
>;
#[cfg(feature = "async")]
pub trait AsyncSource: Send + Sync {
fn collect(&self) -> CollectFuture<'_>;
}
#[cfg(feature = "async")]
impl<T: Source + Send + Sync> AsyncSource for T {
fn collect(&self) -> CollectFuture<'_> {
Box::pin(std::future::ready(Source::collect(self)))
}
}