use std::marker::PhantomData;
use std::path::Path;
use serde::de::DeserializeOwned;
use crate::cache::{CacheMode, Recovery};
type Validator<T> = fn(&T) -> Result<(), Error>;
use crate::error::{Error, ErrorKind};
use crate::source::{Format, LoadSpec, Source};
pub struct Builder<T> {
key: String,
files: Vec<(String, bool)>,
env: Option<String>,
nest: Option<String>,
allow_empty_env: bool,
strict_env: bool,
env_files: Vec<String>,
profile_env: Option<String>,
search: Option<(String, Vec<String>)>,
cache: Option<(String, CacheMode)>,
secrets: Option<Vec<String>>,
validate: Option<Validator<T>>,
fields: &'static [&'static str],
install: Option<fn(T)>,
register: Option<fn(&Self)>,
defaults: Option<&'static crate::Layer>,
overrides: Option<&'static crate::Layer>,
flags: Option<&'static crate::Layer>,
bindings: Option<&'static crate::EnvBindings>,
aliases: Option<&'static crate::Aliases>,
remote: Option<&'static crate::Remote>,
_marker: PhantomData<fn() -> T>,
}
impl<T> Clone for Builder<T> {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
files: self.files.clone(),
env: self.env.clone(),
nest: self.nest.clone(),
allow_empty_env: self.allow_empty_env,
strict_env: self.strict_env,
env_files: self.env_files.clone(),
profile_env: self.profile_env.clone(),
search: self.search.clone(),
cache: self.cache.clone(),
secrets: self.secrets.clone(),
validate: self.validate,
fields: self.fields,
install: self.install,
register: self.register,
defaults: self.defaults,
overrides: self.overrides,
flags: self.flags,
bindings: self.bindings,
aliases: self.aliases,
remote: self.remote,
_marker: PhantomData,
}
}
}
impl<T: DeserializeOwned> Builder<T> {
#[must_use]
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
files: Vec::new(),
env: None,
nest: None,
allow_empty_env: false,
strict_env: false,
env_files: Vec::new(),
profile_env: None,
search: None,
cache: None,
secrets: None,
validate: None,
fields: &[],
install: None,
register: None,
defaults: None,
overrides: None,
flags: None,
bindings: None,
aliases: None,
remote: None,
_marker: PhantomData,
}
}
#[doc(hidden)]
#[must_use]
pub fn with_installer(mut self, install: fn(T)) -> Self {
self.install = Some(install);
self
}
#[doc(hidden)]
#[must_use]
pub fn with_secrets(mut self, secrets: &[&str]) -> Self {
self.secrets = Some(secrets.iter().map(|name| (*name).to_owned()).collect());
self
}
#[doc(hidden)]
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn with_type_statics(
mut self,
defaults: &'static crate::Layer,
overrides: &'static crate::Layer,
flags: &'static crate::Layer,
bindings: &'static crate::EnvBindings,
aliases: &'static crate::Aliases,
remote: &'static crate::Remote,
register: fn(&Self),
) -> Self {
self.defaults = Some(defaults);
self.overrides = Some(overrides);
self.flags = Some(flags);
self.bindings = Some(bindings);
self.aliases = Some(aliases);
self.remote = Some(remote);
self.register = Some(register);
self
}
#[must_use]
pub fn validate(mut self, check: Validator<T>) -> Self {
self.validate = Some(check);
self
}
#[must_use]
pub fn file(mut self, path: impl Into<String>) -> Self {
self.files.push((path.into(), false));
self
}
#[cfg(feature = "decrypt")]
#[cfg_attr(docsrs, doc(cfg(feature = "decrypt")))]
#[must_use]
pub fn encrypted_file(mut self, path: impl Into<String>) -> Self {
self.files.push((path.into(), true));
self
}
#[must_use]
pub fn env(mut self, prefix: impl Into<String>) -> Self {
self.env = Some(prefix.into());
self
}
#[must_use]
pub fn nest(mut self, separator: impl Into<String>) -> Self {
self.nest = Some(separator.into());
self
}
#[must_use]
pub fn allow_empty_env(mut self) -> Self {
self.allow_empty_env = true;
self
}
#[must_use]
pub fn strict_env(mut self) -> Self {
self.strict_env = true;
self
}
#[must_use]
pub fn env_file(mut self, path: impl Into<String>) -> Self {
self.env_files.push(path.into());
self
}
#[must_use]
pub fn profile_env(mut self, variable: impl Into<String>) -> Self {
self.profile_env = Some(variable.into());
self
}
#[must_use]
pub fn discover(
mut self,
name: impl Into<String>,
paths: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.search = Some((name.into(), paths.into_iter().map(Into::into).collect()));
self
}
#[must_use]
pub fn cache(mut self, path: impl Into<String>, mode: CacheMode) -> Self {
self.cache = Some((path.into(), mode));
self
}
pub fn load(&self) -> Result<T, Error> {
let value: T = self.with_spec(crate::loader::load)?;
if let Some(check) = self.validate {
check(&value)?;
}
Ok(value)
}
pub fn init(&self) -> Result<(), Error> {
self.check_cache_mode()?;
let Some(install) = self.install else {
return Err(Error::new(
ErrorKind::Backend,
"this builder is tied to no config type, so there is nowhere \
to install; use `load()` here, or start from the generated \
`builder()` on a `#[dynamic_config]` type",
));
};
let outcome = match self.load() {
Ok(value) => {
install(value);
self.write_cache();
Ok(())
}
Err(failure) => {
let recovered = self.recover(failure)?;
if let Some(check) = self.validate {
check(&recovered)?;
}
install(recovered);
crate::log::warning!(
"{}: started from the last known good configuration",
self.key
);
Ok(())
}
};
if outcome.is_ok() {
if let Some(register) = self.register {
register(self);
}
}
outcome
}
pub fn prepare(&self) -> Result<crate::group::Commit, Error>
where
T: Send + 'static,
{
let Some(install) = self.install else {
return Err(Error::new(
ErrorKind::Backend,
"this builder is tied to no config type, so a prepared \
commit would have nowhere to install",
));
};
let value = self.load()?;
Ok(Box::new(move || install(value)))
}
fn check_cache_mode(&self) -> Result<(), Error> {
if let Some((_, mode)) = &self.cache {
if !matches!(mode, CacheMode::Full) && self.secrets.is_none() {
return Err(Error::new(
ErrorKind::Backend,
"a redacted or fingerprint cache needs to know which \
fields are secret, and only the generated `builder()` on \
a `#[dynamic_config]` type knows; use that, or \
`CacheMode::Full`, spelled out",
));
}
}
Ok(())
}
fn write_cache(&self) {
let Some((path, mode)) = &self.cache else {
return;
};
if !matches!(mode, CacheMode::Full) && self.secrets.is_none() {
crate::log::warning!(
"{}: not writing the cache at {path}: a redaction-dependent \
mode needs the generated builder's secret knowledge",
self.key
);
return;
}
let secrets = self.secrets.clone().unwrap_or_default();
let secret_refs: Vec<&str> = secrets.iter().map(String::as_str).collect();
let written = self.with_spec(|spec| {
let snapshot = crate::loader::snapshot(spec)?;
crate::cache::write(&snapshot, Path::new(path), *mode, &secret_refs)
});
if let Err(error) = written {
crate::log::warning!("could not write the configuration cache to {path}: {error}");
}
}
fn recover(&self, failure: Error) -> Result<T, Error> {
let Some((path, mode)) = &self.cache else {
return Err(failure);
};
let may_recover = mode.recovers();
let current = self.with_spec(crate::loader::snapshot).ok();
match crate::cache::read(Path::new(path), current.as_ref()) {
Ok(Recovery::Usable(snapshot)) if may_recover => self
.with_spec(|spec| crate::loader::recover::<T>(spec, &snapshot))
.map(|(value, _snapshot)| value),
Ok(Recovery::Usable(_)) => {
crate::log::warning!(
"{}: the cache at {path} holds values, but this builder \
is configured `Fingerprint`, which diagnoses and never \
recovers; refusing to start from it",
self.key
);
Err(failure)
}
Ok(Recovery::Drift(moved)) => {
crate::log::warning!(
"{}: cannot start: {failure}. Since the last good configuration: {}",
self.key,
match moved {
Some(paths) if paths.is_empty() => "nothing detectably moved".to_owned(),
Some(paths) => paths.join(", "),
None => "could not compare — the sources do not resolve".to_owned(),
}
);
Err(failure)
}
Ok(Recovery::Absent) | Err(_) => Err(failure),
}
}
fn with_spec<R>(
&self,
operation: impl FnOnce(&LoadSpec<'_>) -> Result<R, Error>,
) -> Result<R, Error> {
let sources = self
.files
.iter()
.map(|(file, encrypted)| {
Format::from_path(Path::new(file)).map(|format| {
if *encrypted {
Source::encrypted(file, format)
} else {
Source::file(file, format)
}
})
})
.collect::<Result<Vec<_>, _>>()?;
let env_files: Vec<&str> = self.env_files.iter().map(String::as_str).collect();
let mut spec = LoadSpec::new(&self.key, &sources)
.with_empty_env(self.allow_empty_env)
.with_strict_env(self.strict_env)
.with_env_files(&env_files);
if let Some(prefix) = &self.env {
spec = spec.with_env(prefix);
}
if let Some(separator) = &self.nest {
spec = spec.with_nest(separator);
}
if let Some(variable) = &self.profile_env {
spec = spec.with_profile_env(variable);
}
let search_paths: Vec<&str>;
if let Some((name, paths)) = &self.search {
search_paths = paths.iter().map(String::as_str).collect();
spec = spec.with_search(name, &search_paths);
}
if let Some(layer) = self.defaults {
spec = spec.with_defaults(layer);
}
if let Some(layer) = self.overrides {
spec = spec.with_overrides(layer);
}
if let Some(layer) = self.flags {
spec = spec.with_flags(layer);
}
if let Some(bindings) = self.bindings {
spec = spec.with_env_bindings(bindings);
}
if let Some(aliases) = self.aliases {
spec = spec.with_aliases(aliases);
}
if let Some(remote) = self.remote {
spec = spec.with_remote(remote);
}
operation(&spec)
}
pub fn explain(&self, path: &str) -> Result<crate::Explanation, Error> {
let explanation = self.with_spec(|spec| crate::explain::explain(spec, path))?;
if let Some(secrets) = &self.secrets {
let head = path.split('.').next().unwrap_or(path);
if secrets.iter().any(|secret| secret == head) {
return Ok(explanation.redacted());
}
}
Ok(explanation)
}
#[doc(hidden)]
#[must_use]
pub fn with_fields(mut self, fields: &'static [&'static str]) -> Self {
self.fields = fields;
self
}
pub fn source_of(&self, path: &str) -> Result<Option<crate::Origin>, Error> {
self.with_spec(|spec| crate::loader::source_of(spec, path))
}
pub fn is_set(&self, path: &str) -> Result<bool, Error> {
self.with_spec(|spec| crate::loader::is_set(spec, path))
}
pub fn snapshot(&self) -> Result<crate::Snapshot, Error> {
self.with_spec(crate::loader::snapshot)
}
pub fn check(&self) -> Result<crate::Report, Error> {
self.with_spec(|spec| crate::check::<T>(spec, self.fields))
}
pub fn reload(&self) -> Result<(), Error> {
let Some(install) = self.install else {
return Err(Error::new(
ErrorKind::Backend,
"this builder is tied to no config type, so a reload would \
have nowhere to install",
));
};
install(self.load()?);
self.write_cache();
Ok(())
}
}
#[doc(hidden)]
pub struct Configured<T> {
builder: std::sync::Mutex<Option<Builder<T>>>,
}
impl<T> Default for Configured<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Configured<T> {
#[must_use]
pub const fn new() -> Self {
Self {
builder: std::sync::Mutex::new(None),
}
}
pub fn set(&self, builder: Builder<T>) {
*self
.builder
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(builder);
}
pub fn get(&self, name: &str) -> Result<Builder<T>, Error> {
self.builder
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.ok_or_else(|| {
Error::new(
ErrorKind::Backend,
format!(
"`{name}` has not been configured yet; build and \
install one first: `{name}::builder(\"..\")\
.file(..).init()?`"
),
)
})
}
}
#[cfg(feature = "watch")]
#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
impl<T: DeserializeOwned + Send + Sync + 'static> Builder<T> {
pub fn watch(
&self,
debounce: core::time::Duration,
) -> std::io::Result<crate::watch::WatchHandle> {
self.watch_with(debounce, crate::watch::WatchMode::Native)
}
pub fn watch_with(
&self,
debounce: core::time::Duration,
mode: crate::watch::WatchMode,
) -> std::io::Result<crate::watch::WatchHandle> {
let Some(install) = self.install else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"this builder is tied to no config type, so a reload would \
have nowhere to install; start from the generated \
`builder()` on a `#[dynamic_config]` type",
));
};
let watched = self
.with_spec(|spec| Ok(crate::watch::Watched::from_spec(spec)))
.map_err(|error| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, error.to_string())
})?;
let reloader = self.clone();
let name = watch_name::<T>(&self.key);
let handle = crate::watch::spawn_with(
std::any::TypeId::of::<T>(),
name,
watched,
debounce,
mode,
move || {
let value = reloader.load()?;
install(value);
reloader.write_cache();
Ok(None)
},
)?;
if let Some(register) = self.register {
register(self);
}
Ok(handle)
}
}
#[cfg(feature = "watch")]
fn watch_name<T: 'static>(key: &str) -> &'static str {
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
static NAMES: OnceLock<Mutex<HashMap<std::any::TypeId, &'static str>>> = OnceLock::new();
let mut names = NAMES
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
names
.entry(std::any::TypeId::of::<T>())
.or_insert_with(|| Box::leak(format!("builder:{key}").into_boxed_str()))
}
#[cfg(feature = "schema")]
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
impl<T: DeserializeOwned> Builder<T> {
#[must_use]
pub fn schema(&self) -> serde_json::Value
where
T: schemars::JsonSchema,
{
let secrets = self.secrets.clone().unwrap_or_default();
let secret_refs: Vec<&str> = secrets.iter().map(String::as_str).collect();
crate::schema::section(&self.key, schemars::schema_for!(T).into(), &secret_refs)
}
}
#[cfg(feature = "async")]
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
impl<T: DeserializeOwned + Send + 'static> Builder<T> {
pub async fn load_async(&self) -> Result<T, Error> {
let this = self.clone();
crate::asynchronous::off_thread(move || this.load()).await
}
pub async fn init_async(&self) -> Result<(), Error> {
let this = self.clone();
crate::asynchronous::off_thread(move || this.init()).await
}
}
impl<T> std::fmt::Debug for Builder<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Builder")
.field("key", &self.key)
.field("files", &self.files)
.field("env", &self.env)
.field("env_files", &self.env_files)
.field("strict_env", &self.strict_env)
.field("installs", &self.install.is_some())
.finish_non_exhaustive()
}
}