use core::fmt::Debug;
mod error;
mod redacted;
pub use error::{Error, MissingField};
pub use redacted::Redacted;
#[cfg(feature = "derive")]
pub use partial_config_derive::HasPartial;
#[cfg(feature = "derive")]
pub use partial_config_derive::EnvSourced;
pub trait Partial: Default {
type Target: HasPartial<Partial = Self>;
type Error: Debug;
fn build(self) -> Result<Self::Target, Self::Error>;
fn source<T: Source<Self::Target>>(self, value: T) -> Result<Self, Self::Error>
where
<Self as Partial>::Error: From<<T as Source<<Self as Partial>::Target>>::Error>,
{
#[cfg(feature = "tracing")]
tracing::info!("Sourcing configuration from `{}`", value.name());
#[cfg(feature = "log")]
log::info!("Sourcing configuration from `{}`", value.name());
#[cfg(not(any(feature = "tracing", feature = "log")))]
println!("Sourcing configuration from `{}`", value.name());
let partial = value.to_partial()?;
Ok(self.override_with(partial))
}
fn override_with(self, other: Self) -> Self;
}
pub trait HasPartial {
type Partial: Partial<Target = Self>;
}
pub trait Source<C: HasPartial> {
type Error: Debug;
fn to_partial(self) -> Result<C::Partial, Self::Error>;
fn name(&self) -> String;
}
impl<T, C, E> Source<C> for Option<T>
where
C: HasPartial,
T: Source<C, Error = E>,
E: Debug,
{
type Error = E;
fn to_partial(self) -> Result<C::Partial, E> {
self.map_or_else(|| Ok(C::Partial::default()), |v| v.to_partial())
}
fn name(&self) -> String {
self.as_ref().map_or("Unspecified".to_owned(), |v| v.name())
}
}
pub mod env {
pub trait EnvSourced<'a>: super::HasPartial + Sized {
type Source: 'a + super::Source<Self> + Default;
}
pub fn extract(candidates: &[&str]) -> Result<Option<String>, super::Error> {
let mut found = None;
for candidate in candidates {
match (&found, std::env::var(candidate)) {
(_, Err(std::env::VarError::NotPresent)) => continue,
(_, Err(std::env::VarError::NotUnicode(thing))) => {
#[cfg(feature = "tracing")]
tracing::warn!("The value of the environment variable for `{candidate}` was not Unicode. Got {thing:?}");
#[cfg(feature = "log")]
log::warn!("The value of the environment variable for `{candidate}` was not Unicode. Got {thing:?}");
#[cfg(not(any(feature = "log", feature = "tracing")))]
eprintln!("The value of the environment variable for `{candidate}` was not Unicode. Got {thing:?}");
}
(None, Ok(value)) => found = Some((candidate, value)),
(Some((previous_key, previous_string)), Ok(value)) if *previous_string == value => {
#[cfg(feature = "tracing")]
tracing::warn!("Redundant specification of the environment variable {candidate}, which was previously set via {previous_key}");
#[cfg(feature = "log")]
log::warn!("Redundant specification of the environment variable {candidate}, which was previously set via {previous_key}");
#[cfg(not(any(feature = "log", feature = "tracing")))]
eprintln!("Redundant specification of the environment variable {candidate}, which was previously set via {previous_key}");
}
(Some((previous_key, previous_string)), Ok(value)) => {
#[cfg(feature = "tracing")]
tracing::error!("Inconsistent specification via environment variable {candidate}. Expected {previous_string} found {value}");
#[cfg(feature = "log")]
log::error!("Inconsistent specification via environment variable {candidate}. Expected {previous_string} found {value}");
#[cfg(not(any(feature = "log", feature = "tracing")))]
eprintln!("Inconsistent specification via environment variable {candidate}. Expected {previous_string} found {value}");
let err = super::Error::InconsistentSetting {
first_source: format!("Environment variable {previous_key}"),
first_setting: previous_string.clone(),
second_source: format!("Environment variable {candidate}"),
second_setting: value,
};
return Err(err);
}
}
}
Ok(found.map(|(_, value)| value))
}
}
#[cfg(feature = "serde")]
pub mod serde_support {
use super::{HasPartial, Source};
#[cfg(feature = "toml")]
use std::io::Read;
#[derive(Debug)]
#[non_exhaustive]
pub enum FileReadError {
Open(std::io::Error),
#[cfg(feature = "toml")]
Toml(toml::de::Error),
#[cfg(feature = "json")]
Json(serde_json::Error), NoFile(std::path::PathBuf),
UnsupportedExtension(String),
NoExtension,
}
impl From<std::io::Error> for FileReadError {
fn from(value: std::io::Error) -> Self {
Self::Open(value)
}
}
impl core::fmt::Display for FileReadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NoExtension => {
write!(f, "No file extension provided. Aborting")
}
Self::UnsupportedExtension(s) => {
write!(f, "The file extension {s} is not supported")
}
Self::NoFile(path) => {
write!(f, "The file {path:?} could not be found")
}
Self::Open(err) => {
write!(f, "The file system reported the following error {err}")
}
#[cfg(feature = "toml")]
Self::Toml(te) => {
write!(f, "Error parsing TOML file {te}")
}
#[cfg(feature = "json")]
Self::Json(je) => {
write!(f, "Error parsing JSON file {je}")
}
}
}
}
impl std::error::Error for FileReadError {}
#[cfg(feature = "toml")]
pub struct Toml<'a>(pub &'a std::path::Path);
#[cfg(feature = "json")]
pub struct Json<'a>(pub &'a std::path::Path);
#[cfg(feature = "json")]
impl<'pth, C> Source<C> for Json<'pth>
where
C: HasPartial,
C::Partial: serde::de::DeserializeOwned,
{
type Error = FileReadError;
fn to_partial(self) -> Result<C::Partial, FileReadError> {
let Self(path) = self;
let file = std::fs::OpenOptions::new().read(true).open(path)?;
let partial: C::Partial = serde_json::from_reader(file).map_err(FileReadError::Json)?;
Ok(partial)
}
fn name(&self) -> String {
format!("JSON file at {:?}", self.0)
}
}
#[cfg(feature = "toml")]
impl<'pth, C> Source<C> for Toml<'pth>
where
C: HasPartial,
C::Partial: serde::de::DeserializeOwned,
{
type Error = FileReadError;
fn to_partial(self) -> Result<C::Partial, FileReadError> {
let Self(path) = self;
let mut file = std::fs::OpenOptions::new().read(true).open(path)?;
let mut buffer: String = String::new();
file.read_to_string(&mut buffer)?;
let partial: C::Partial = toml::from_str(&buffer).map_err(FileReadError::Toml)?;
Ok(partial)
}
fn name(&self) -> String {
format!("TOML file at {:?}", self.0)
}
}
impl<C> Source<C> for std::path::PathBuf
where
C: HasPartial,
C::Partial: serde::de::DeserializeOwned,
{
type Error = FileReadError;
fn to_partial(self) -> Result<C::Partial, FileReadError> {
if !self.exists() {
Err(FileReadError::NoFile(self))
} else {
match self.extension() {
Some(os_str) => match os_str.to_str().expect("Failed conversion from OsStr") {
#[cfg(feature = "toml")]
"toml" | "tml" => <Toml<'_> as Source<C>>::to_partial(Toml(&self)),
#[cfg(feature = "json")]
"json" | "js" => <Json<'_> as Source<C>>::to_partial(Json(&self)),
rest => Err(FileReadError::UnsupportedExtension(rest.to_owned())),
},
None => Err(FileReadError::NoExtension),
}
}
}
fn name(&self) -> String {
format!("Configuration file at `{:?}`", self)
}
}
}
pub trait ConfigPath<T: AsRef<std::path::Path>> {
fn config_path(&self) -> Option<T>;
}