use std::marker::PhantomData;
use std::path::PathBuf;
use std::sync::Arc;
use arc_swap::ArcSwap;
use figment::providers::{Env, Format, Yaml};
use figment::Figment;
use serde::de::DeserializeOwned;
use tokio::sync::watch;
use crate::error::ConfigError;
pub struct Config<C = ()>
where
C: DeserializeOwned + Send + Sync + 'static,
{
current: Arc<ArcSwap<C>>,
tx: Arc<watch::Sender<Arc<C>>>,
pub(crate) sources: Arc<Sources>,
}
impl<C> std::fmt::Debug for Config<C>
where
C: DeserializeOwned + Send + Sync + 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Config")
.field("files", &self.sources.files)
.field("env_prefixes", &self.sources.envs)
.field("embedded_layers", &self.sources.embedded.len())
.finish_non_exhaustive()
}
}
impl<C> Clone for Config<C>
where
C: DeserializeOwned + Send + Sync + 'static,
{
fn clone(&self) -> Self {
Self {
current: Arc::clone(&self.current),
tx: Arc::clone(&self.tx),
sources: Arc::clone(&self.sources),
}
}
}
impl<C> Default for Config<C>
where
C: DeserializeOwned + Default + Send + Sync + 'static,
{
fn default() -> Self {
let initial = Arc::new(C::default());
let (tx, _rx) = watch::channel(Arc::clone(&initial));
Self {
current: Arc::new(ArcSwap::from(initial)),
tx: Arc::new(tx),
sources: Arc::new(Sources::default()),
}
}
}
impl<C> Config<C>
where
C: DeserializeOwned + Send + Sync + 'static,
{
pub fn builder() -> ConfigBuilder<C> {
ConfigBuilder::new()
}
#[must_use]
pub fn with_value(value: C) -> Self {
let initial = Arc::new(value);
let (tx, _rx) = watch::channel(Arc::clone(&initial));
Self {
current: Arc::new(ArcSwap::from(initial)),
tx: Arc::new(tx),
sources: Arc::new(Sources::default()),
}
}
#[must_use]
pub fn get(&self) -> Arc<C> {
self.current.load_full()
}
pub fn reload(&self) -> Result<(), ConfigError> {
let parsed = Arc::new(self.sources.parse::<C>()?);
self.current.store(Arc::clone(&parsed));
self.tx.send_replace(parsed);
Ok(())
}
#[must_use]
pub fn subscribe(&self) -> watch::Receiver<Arc<C>> {
self.tx.subscribe()
}
}
#[derive(Default)]
pub(crate) struct Sources {
embedded: Vec<&'static str>,
pub(crate) files: Vec<PathBuf>,
envs: Vec<String>,
}
impl Sources {
fn parse<C: DeserializeOwned>(&self) -> Result<C, ConfigError> {
let mut figment = Figment::new();
for yaml in &self.embedded {
figment = figment.merge(Yaml::string(yaml));
}
for path in &self.files {
if path.exists() && !path.is_file() {
return Err(ConfigError::Io {
path: path.clone(),
source: std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"config path is not a regular file",
),
});
}
figment = figment.merge(Yaml::file(path));
}
for prefix in &self.envs {
figment = figment.merge(Env::prefixed(prefix).split("_"));
}
let parsed: C = figment.extract()?;
Ok(parsed)
}
}
#[must_use]
pub struct ConfigBuilder<C>
where
C: DeserializeOwned + Send + Sync + 'static,
{
sources: Sources,
_phantom: PhantomData<fn() -> C>,
}
impl<C> Default for ConfigBuilder<C>
where
C: DeserializeOwned + Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<C> ConfigBuilder<C>
where
C: DeserializeOwned + Send + Sync + 'static,
{
pub fn new() -> Self {
Self { sources: Sources::default(), _phantom: PhantomData }
}
pub fn embedded_default(mut self, yaml: &'static str) -> Self {
self.sources.embedded.push(yaml);
self
}
pub fn user_file(mut self, path: impl Into<PathBuf>) -> Self {
self.sources.files.push(path.into());
self
}
pub fn env_prefixed(mut self, prefix: impl Into<String>) -> Self {
self.sources.envs.push(prefix.into());
self
}
pub fn build(self) -> Result<Config<C>, ConfigError> {
let parsed = Arc::new(self.sources.parse::<C>()?);
let (tx, _rx) = watch::channel(Arc::clone(&parsed));
Ok(Config {
current: Arc::new(ArcSwap::from(parsed)),
tx: Arc::new(tx),
sources: Arc::new(self.sources),
})
}
}