use std::collections::BTreeMap;
use serde::{Serialize, de::DeserializeOwned};
use crate::{
crypto::{Encryptor, decrypt_fields},
deserialize::ConfigValueDeserializer,
error::LoadError,
merge::{FieldStrategies, merge},
migrate::MigrationChain,
source::Source,
value::ConfigValue,
};
#[cfg(feature = "async")]
use crate::source::AsyncSource;
pub trait ConfigSchema: DeserializeOwned + Serialize {
const APP_NAME: &'static str;
const SCHEMA_VERSION: u32;
#[must_use]
fn field_strategies() -> FieldStrategies {
&[]
}
#[must_use]
fn encrypted_fields() -> &'static [&'static str] {
&[]
}
}
pub(crate) struct LoaderEntry {
pub(crate) source: std::sync::Arc<dyn Source + Send + Sync>,
#[cfg(feature = "hot-reload")]
pub(crate) watch_paths: Vec<std::path::PathBuf>,
}
pub struct ConfigLoader<S: ConfigSchema> {
entries: Vec<LoaderEntry>,
migrations: Option<std::sync::Arc<MigrationChain>>,
encryptor: Option<std::sync::Arc<dyn Encryptor>>,
_phantom: std::marker::PhantomData<S>,
}
impl<S: ConfigSchema> Clone for ConfigLoader<S> {
fn clone(&self) -> Self {
Self {
entries: self
.entries
.iter()
.map(|e| LoaderEntry {
source: std::sync::Arc::clone(&e.source),
#[cfg(feature = "hot-reload")]
watch_paths: e.watch_paths.clone(),
})
.collect(),
migrations: self.migrations.clone(),
encryptor: self.encryptor.clone(),
_phantom: std::marker::PhantomData,
}
}
}
impl<S: ConfigSchema> Default for ConfigLoader<S> {
fn default() -> Self {
Self {
entries: Vec::new(),
migrations: None,
encryptor: None,
_phantom: std::marker::PhantomData,
}
}
}
impl<S: ConfigSchema> ConfigLoader<S> {
#[must_use]
pub fn with_source(mut self, source: impl Source + Send + Sync + 'static) -> Self {
let source = source.with_app_name(S::APP_NAME);
self.entries.push(LoaderEntry {
source: std::sync::Arc::new(source),
#[cfg(feature = "hot-reload")]
watch_paths: Vec::new(),
});
self
}
#[cfg(feature = "hot-reload")]
#[must_use]
pub fn with_watched_source(
mut self,
source: impl crate::source::WatchableSource + 'static,
) -> Self {
let source = source.with_app_name(S::APP_NAME);
let watch_paths = source.watch_paths();
self.entries.push(LoaderEntry {
source: std::sync::Arc::new(source),
watch_paths,
});
self
}
#[must_use]
pub fn with_migrations(mut self, migrations: MigrationChain) -> Self {
self.migrations = Some(std::sync::Arc::new(migrations));
self
}
#[must_use]
pub fn with_encryptor(mut self, encryptor: impl Encryptor + 'static) -> Self {
self.encryptor = Some(std::sync::Arc::new(encryptor));
self
}
pub fn load(&self) -> Result<S, LoadError> {
let strategies = S::field_strategies();
let mut merged = ConfigValue::Table(BTreeMap::new());
for entry in &self.entries {
let collected = entry.source.collect()?;
merged = merge(merged, collected, strategies);
}
let current_version = match &merged {
ConfigValue::Table(t) => match t.get("__schema_version") {
Some(ConfigValue::Integer(v)) => u32::try_from(*v).unwrap_or(0),
_ => 0,
},
_ => 0,
};
if current_version < S::SCHEMA_VERSION
&& let Some(chain) = &self.migrations
{
merged = chain.run(merged, current_version, S::SCHEMA_VERSION)?;
}
if let Some(enc) = &self.encryptor {
decrypt_fields(&mut merged, S::encrypted_fields(), enc.as_ref())?;
}
Ok(S::deserialize(ConfigValueDeserializer(merged))?)
}
}
#[cfg(feature = "hot-reload")]
impl<S: ConfigSchema + Clone + Send + Sync + 'static> ConfigLoader<S> {
pub fn watch(
self,
watch_path: &std::path::Path,
options: crate::handle::WatchOptions,
) -> Result<
(crate::handle::ConfigHandle<S>, crate::handle::WatcherHandle),
crate::handle::HotReloadError,
> {
use std::sync::Arc;
let initial = self.load()?;
let loader = Arc::new(self);
let reload: Arc<dyn Fn() -> Result<S, LoadError> + Send + Sync> =
Arc::new(move || loader.load());
crate::handle::ConfigHandle::spawn_watcher(
initial,
watch_path,
&reload,
None,
options.debounce,
)
}
pub fn watch_auto(
self,
options: crate::handle::WatchOptions,
) -> Result<
(crate::handle::ConfigHandle<S>, crate::handle::WatcherHandle),
crate::handle::HotReloadError,
> {
use std::sync::Arc;
use crate::handle::SelectiveEntry;
let strategies = S::field_strategies();
let migrations = self.migrations.clone();
let encryptor = self.encryptor.clone();
let mut selective_entries = Vec::with_capacity(self.entries.len());
let mut initial_merged = ConfigValue::Table(BTreeMap::new());
for entry in &self.entries {
let cache = entry.source.collect().map_err(LoadError::from)?;
initial_merged = merge(initial_merged, cache.clone(), strategies);
selective_entries.push(SelectiveEntry {
source: Arc::clone(&entry.source),
cache: std::sync::Mutex::new(cache),
watch_paths: entry.watch_paths.clone(),
});
}
let transform: Arc<dyn Fn(ConfigValue) -> Result<S, LoadError> + Send + Sync> =
Arc::new(move |mut merged: ConfigValue| {
let current_version = match &merged {
ConfigValue::Table(t) => match t.get("__schema_version") {
Some(ConfigValue::Integer(v)) => u32::try_from(*v).unwrap_or(0),
_ => 0,
},
_ => 0,
};
if current_version < S::SCHEMA_VERSION
&& let Some(chain) = &migrations
{
merged = chain.run(merged, current_version, S::SCHEMA_VERSION)?;
}
if let Some(enc) = &encryptor {
decrypt_fields(&mut merged, S::encrypted_fields(), enc.as_ref())?;
}
Ok(S::deserialize(ConfigValueDeserializer(merged))?)
});
let initial = transform(initial_merged.clone())?;
crate::handle::ConfigHandle::spawn_selective_watcher(
initial,
initial_merged,
selective_entries,
strategies,
transform,
options.debounce,
)
}
}
#[cfg(feature = "async")]
pub struct AsyncConfigLoader<S: ConfigSchema> {
sources: Vec<std::sync::Arc<dyn AsyncSource + Send + Sync>>,
migrations: Option<std::sync::Arc<MigrationChain>>,
encryptor: Option<std::sync::Arc<dyn Encryptor>>,
_phantom: std::marker::PhantomData<S>,
}
#[cfg(feature = "async")]
impl<S: ConfigSchema> Default for AsyncConfigLoader<S> {
fn default() -> Self {
Self {
sources: Vec::new(),
migrations: None,
encryptor: None,
_phantom: std::marker::PhantomData,
}
}
}
#[cfg(feature = "async")]
impl<S: ConfigSchema> AsyncConfigLoader<S> {
#[must_use]
pub fn with_source(mut self, source: impl AsyncSource + 'static) -> Self {
self.sources.push(std::sync::Arc::new(source));
self
}
#[must_use]
pub fn with_migrations(mut self, migrations: MigrationChain) -> Self {
self.migrations = Some(std::sync::Arc::new(migrations));
self
}
#[must_use]
pub fn with_encryptor(mut self, encryptor: impl Encryptor + 'static) -> Self {
self.encryptor = Some(std::sync::Arc::new(encryptor));
self
}
pub async fn load(&self) -> Result<S, LoadError> {
let strategies = S::field_strategies();
let mut merged = ConfigValue::Table(std::collections::BTreeMap::new());
for source in &self.sources {
let collected = source.collect().await?;
merged = merge(merged, collected, strategies);
}
let current_version = match &merged {
ConfigValue::Table(t) => match t.get("__schema_version") {
Some(ConfigValue::Integer(v)) => u32::try_from(*v).unwrap_or(0),
_ => 0,
},
_ => 0,
};
if current_version < S::SCHEMA_VERSION
&& let Some(chain) = &self.migrations
{
merged = chain.run(merged, current_version, S::SCHEMA_VERSION)?;
}
if let Some(enc) = &self.encryptor {
decrypt_fields(&mut merged, S::encrypted_fields(), enc.as_ref())?;
}
Ok(S::deserialize(ConfigValueDeserializer(merged))?)
}
}