#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::{
ffi::{OsStr, OsString},
marker::PhantomData,
path::PathBuf,
};
#[cfg(feature = "derive")]
pub use tenvy_derive::Tenvy;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("environment variable `{}` is missing", .0.display())]
Missing(OsString),
#[error("cannot parse `{}`", var.display())]
InvalidValue {
var: OsString,
#[source]
error: Box<dyn std::error::Error + Send + Sync>,
},
}
#[derive(Debug)]
pub struct Env {
var: OsString,
}
pub trait Tenvy<T = Self> {
fn from_env(env: &Env) -> Result<T, Error>;
}
pub fn from_env<T: Tenvy>() -> Result<T, Error> {
T::from_env(&Env {
var: OsString::default(),
})
}
impl Error {
pub fn invalid_value<E: std::error::Error + Send + Sync + 'static>(
env: &Env,
error: E,
) -> Self {
Self::InvalidValue {
var: env.var.clone(),
error: Box::new(error),
}
}
pub fn replace_missing<T>(self, f: impl FnOnce() -> T) -> Result<T, Error> {
match self {
Error::Missing(_) => Ok(f()),
other => Err(other),
}
}
}
impl Env {
pub fn value(&self) -> Result<OsString, Error> {
std::env::var_os(&self.var).ok_or_else(|| Error::Missing(self.var.clone()))
}
pub fn nest<K: AsRef<OsStr>>(&self, field: K) -> Self {
let mut var = self.var.clone();
if !var.is_empty() && !field.as_ref().is_empty() {
var.push("_");
}
var.push(field);
Self { var }
}
pub fn field<K: AsRef<OsStr>, T: Tenvy>(&self, field: K) -> Result<T, Error> {
T::from_env(&self.nest(field))
}
}
#[derive(Debug)]
pub struct FromStr(std::convert::Infallible);
#[derive(Debug)]
pub struct From<U>(PhantomData<U>);
#[derive(Debug)]
pub struct TryFrom<U>(PhantomData<U>);
impl<T> Tenvy<T> for FromStr
where
T: std::str::FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
{
fn from_env(env: &Env) -> Result<T, Error> {
String::from_env(env)
.and_then(|value| T::from_str(&value).map_err(|error| Error::invalid_value(env, error)))
}
}
impl<U, T> Tenvy<T> for From<U>
where
T: std::convert::From<U>,
U: Tenvy,
{
fn from_env(env: &Env) -> Result<T, Error> {
U::from_env(env).map(T::from)
}
}
impl<U, T> Tenvy<T> for TryFrom<U>
where
T: std::convert::TryFrom<U>,
T::Error: std::error::Error + Send + Sync + 'static,
U: Tenvy,
{
fn from_env(env: &Env) -> Result<T, Error> {
U::from_env(env)
.and_then(|value| T::try_from(value).map_err(|error| Error::invalid_value(env, error)))
}
}
impl Tenvy for () {
fn from_env(_: &Env) -> Result<Self, Error> {
Ok(())
}
}
impl Tenvy for std::convert::Infallible {
fn from_env(env: &Env) -> Result<Self, Error> {
Err(Error::Missing(env.var.clone()))
}
}
impl<T> Tenvy for std::marker::PhantomData<T> {
fn from_env(_: &Env) -> Result<Self, Error> {
Ok(Self)
}
}
impl Tenvy for std::marker::PhantomPinned {
fn from_env(_: &Env) -> Result<Self, Error> {
Ok(Self)
}
}
impl<T: Tenvy> Tenvy for std::num::Saturating<T> {
fn from_env(env: &Env) -> Result<Self, Error> {
T::from_env(env).map(std::num::Saturating)
}
}
impl<T: Tenvy> Tenvy for std::num::Wrapping<T> {
fn from_env(env: &Env) -> Result<Self, Error> {
T::from_env(env).map(std::num::Wrapping)
}
}
impl<T: Tenvy> Tenvy for Option<T> {
fn from_env(env: &Env) -> Result<Self, Error> {
T::from_env(env)
.map(Some)
.or_else(|error| error.replace_missing(|| None))
}
}
impl Tenvy for OsString {
fn from_env(env: &Env) -> Result<Self, Error> {
env.value()
}
}
impl Tenvy for PathBuf {
fn from_env(env: &Env) -> Result<Self, Error> {
env.value().map(PathBuf::from)
}
}
impl Tenvy for String {
fn from_env(env: &Env) -> Result<Self, Error> {
#[derive(Debug, thiserror::Error)]
#[error("value contains invalid Unicode data: {}", .0.display())]
struct UnicodeError(OsString);
env.value().and_then(|value| {
value
.into_string()
.map_err(|value| Error::invalid_value(env, UnicodeError(value)))
})
}
}
macro_rules! wrap_with_new {
($t:ident, $typ:ty) => {
impl<$t: Tenvy> Tenvy for $typ {
fn from_env(env: &Env) -> Result<Self, Error> {
$t::from_env(env).map(Self::new)
}
}
};
}
wrap_with_new!(T, Box<T>);
wrap_with_new!(T, std::cell::Cell<T>);
wrap_with_new!(T, std::cell::RefCell<T>);
wrap_with_new!(T, std::cell::UnsafeCell<T>);
wrap_with_new!(T, std::rc::Rc<T>);
wrap_with_new!(T, std::sync::Arc<T>);
wrap_with_new!(T, std::sync::Mutex<T>);
wrap_with_new!(T, std::sync::RwLock<T>);
macro_rules! delegate_to_from_str {
($typ:ty) => {
impl Tenvy for $typ {
fn from_env(env: &Env) -> Result<Self, Error> {
FromStr::from_env(env)
}
}
};
}
delegate_to_from_str!(f32);
delegate_to_from_str!(f64);
delegate_to_from_str!(bool);
delegate_to_from_str!(char);
delegate_to_from_str!(i8);
delegate_to_from_str!(i16);
delegate_to_from_str!(i32);
delegate_to_from_str!(i64);
delegate_to_from_str!(i128);
delegate_to_from_str!(isize);
delegate_to_from_str!(u8);
delegate_to_from_str!(u16);
delegate_to_from_str!(u32);
delegate_to_from_str!(u64);
delegate_to_from_str!(u128);
delegate_to_from_str!(usize);
delegate_to_from_str!(std::net::IpAddr);
delegate_to_from_str!(std::net::Ipv4Addr);
delegate_to_from_str!(std::net::Ipv6Addr);
delegate_to_from_str!(std::net::SocketAddr);
delegate_to_from_str!(std::net::SocketAddrV4);
delegate_to_from_str!(std::net::SocketAddrV6);
delegate_to_from_str!(std::num::NonZeroI8);
delegate_to_from_str!(std::num::NonZeroI16);
delegate_to_from_str!(std::num::NonZeroI32);
delegate_to_from_str!(std::num::NonZeroI64);
delegate_to_from_str!(std::num::NonZeroI128);
delegate_to_from_str!(std::num::NonZeroIsize);
delegate_to_from_str!(std::num::NonZeroU8);
delegate_to_from_str!(std::num::NonZeroU16);
delegate_to_from_str!(std::num::NonZeroU32);
delegate_to_from_str!(std::num::NonZeroU64);
delegate_to_from_str!(std::num::NonZeroU128);
delegate_to_from_str!(std::num::NonZeroUsize);