use std::{
char::ParseCharError,
collections::{BTreeMap, BTreeSet, HashSet},
convert::Infallible,
env::{VarError, remove_var},
ffi::OsStr,
fmt::{Debug, Display},
hash::{Hash, Hasher},
marker::PhantomData,
num::{
NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, NonZeroU8, NonZeroU16, NonZeroU32,
NonZeroU64, NonZeroU128, NonZeroUsize, ParseFloatError, ParseIntError,
},
rc::Rc,
str::ParseBoolError,
};
pub trait FromEnvValue: Sized {
fn from_env_value(value: String) -> Self;
}
impl FromEnvValue for String {
fn from_env_value(value: String) -> Self {
value
}
}
pub trait TryFromEnvValue: Sized {
type Error;
fn try_from_env_value(value: String) -> Result<Self, Self::Error>;
}
impl<K: TryFromEnvValue + Eq + Hash, V: TryFromEnvValue> TryFromEnvValue for std::collections::HashMap<K, V> {
type Error = MapTryFromEnvError<K::Error, V::Error>;
fn try_from_env_value(value: String) -> Result<Self, Self::Error> {
let elements = value.split(',');
let size_hint = elements.size_hint().0;
let mut map = std::collections::HashMap::with_capacity(size_hint);
for line in elements {
if let Some((key, value)) = line.split_once('=') {
if value.contains('=') {
continue;
}
let key = K::try_from_env_value(key.to_owned()).map_err(MapTryFromEnvError::Key)?;
let value = V::try_from_env_value(value.to_owned()).map_err(MapTryFromEnvError::Value)?;
map.insert(key, value);
}
}
Ok(map)
}
}
impl<K: TryFromEnvValue + Ord, V: TryFromEnvValue> TryFromEnvValue for BTreeMap<K, V> {
type Error = MapTryFromEnvError<K::Error, V::Error>;
fn try_from_env_value(value: String) -> Result<Self, Self::Error> {
let elements = value.split(',');
let mut map = BTreeMap::new();
for line in elements {
if let Some((key, value)) = line.split_once('=') {
if value.contains('=') {
continue;
}
let key = K::try_from_env_value(key.to_owned()).map_err(MapTryFromEnvError::Key)?;
let value = V::try_from_env_value(value.to_owned()).map_err(MapTryFromEnvError::Value)?;
map.insert(key, value);
}
}
Ok(map)
}
}
#[derive(Debug)]
pub enum MapTryFromEnvError<K, V> {
Key(K),
Value(V),
}
impl<K: Display, V: Display> Display for MapTryFromEnvError<K, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Key(s) => Display::fmt(s, f),
Self::Value(v) => Display::fmt(v, f),
}
}
}
impl<K: std::error::Error + 'static, V: std::error::Error + 'static> std::error::Error for MapTryFromEnvError<K, V> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Key(k) => Some(k),
Self::Value(v) => Some(v),
}
}
}
impl<T: TryFromEnvValue + Eq + Hash> TryFromEnvValue for HashSet<T> {
type Error = T::Error;
fn try_from_env_value(value: String) -> Result<Self, Self::Error> {
value
.split(',')
.map(|v| T::try_from_env_value(v.to_owned()))
.collect::<Result<_, T::Error>>()
}
}
impl<T: TryFromEnvValue + Ord> TryFromEnvValue for BTreeSet<T> {
type Error = T::Error;
fn try_from_env_value(value: String) -> Result<Self, Self::Error> {
value
.split(',')
.map(|v| T::try_from_env_value(v.to_owned()))
.collect::<Result<_, T::Error>>()
}
}
impl<T: TryFromEnvValue> TryFromEnvValue for Vec<T> {
type Error = T::Error;
fn try_from_env_value(value: String) -> Result<Self, Self::Error> {
value
.split(',')
.map(|v| T::try_from_env_value(v.to_owned()))
.collect::<Result<Vec<_>, T::Error>>()
}
}
#[cfg(feature = "tracing")]
#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "tracing")))]
impl TryFromEnvValue for tracing::Level {
type Error = InvalidLevel;
fn try_from_env_value(value: String) -> Result<Self, Self::Error> {
match &*value.to_ascii_lowercase() {
"trace" => Ok(tracing::Level::TRACE),
"info" | "information" => Ok(tracing::Level::INFO),
"debug" => Ok(tracing::Level::DEBUG),
"warn" | "warning" => Ok(tracing::Level::WARN),
"error" => Ok(tracing::Level::ERROR),
level => Err(InvalidLevel(level.to_owned())),
}
}
}
#[cfg(feature = "tracing")]
#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "tracing")))]
#[derive(Debug)]
pub struct InvalidLevel(String);
#[cfg(feature = "tracing")]
#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "tracing")))]
impl std::fmt::Display for InvalidLevel {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "invalid log level: '{}'", self.0)
}
}
#[cfg(feature = "tracing")]
#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "tracing")))]
impl std::error::Error for InvalidLevel {}
macro_rules! impl_try_from_env {
($($(#[$meta:meta])* $Ty:ty: $Error:ty;)*) => {
$(
$(#[$meta])*
/// This implementation will forward to the [`FromStr`] implementation
/// of the concrete type.
impl $crate::env::TryFromEnvValue for $Ty {
type Error = $Error;
fn try_from_env_value(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
)*
};
}
impl_try_from_env!(
bool: ParseBoolError;
char: ParseCharError;
f32: ParseFloatError;
f64: ParseFloatError;
NonZeroI8: ParseIntError;
NonZeroI16: ParseIntError;
NonZeroI32: ParseIntError;
NonZeroI64: ParseIntError;
NonZeroI128: ParseIntError;
NonZeroIsize: ParseIntError;
i8: ParseIntError;
i16: ParseIntError;
i32: ParseIntError;
i64: ParseIntError;
i128: ParseIntError;
isize: ParseIntError;
NonZeroU8: ParseIntError;
NonZeroU16: ParseIntError;
NonZeroU32: ParseIntError;
NonZeroU64: ParseIntError;
NonZeroU128: ParseIntError;
NonZeroUsize: ParseIntError;
u8: ParseIntError;
u16: ParseIntError;
u32: ParseIntError;
u64: ParseIntError;
u128: ParseIntError;
usize: ParseIntError;
std::path::PathBuf: Infallible;
#[cfg(feature = "sentry")]
#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "sentry")))]
sentry_types::Dsn: sentry_types::ParseDsnError;
#[cfg(feature = "url")]
#[cfg_attr(any(noeldoc, docsrs), doc(cfg(feature = "url")))]
url::Url: url::ParseError;
);
impl<T: FromEnvValue> TryFromEnvValue for T {
type Error = Infallible;
fn try_from_env_value(value: String) -> Result<Self, Self::Error> {
Ok(T::from_env_value(value))
}
}
pub fn parse<K: Into<String>, V: FromEnvValue>(key: K) -> Result<V, VarError> {
std::env::var(key.into()).map(V::from_env_value)
}
pub fn try_parse<K: Into<String>, V: TryFromEnvValue>(key: K) -> Result<V, TryParseError<V::Error>> {
match std::env::var(key.into()) {
Ok(value) => V::try_from_env_value(value).map_err(TryParseError::Parse),
Err(e) => Err(TryParseError::System(e)),
}
}
pub fn try_parse_or<K: Into<String>, V: TryFromEnvValue>(
key: K,
default: impl FnOnce() -> V,
) -> Result<V, TryParseError<V::Error>> {
match try_parse(key) {
Ok(value) => Ok(value),
Err(TryParseError::System(std::env::VarError::NotPresent)) => Ok(default()),
Err(e) => Err(e),
}
}
pub fn try_parse_or_else<K: Into<String>, V: TryFromEnvValue>(
key: K,
default: V,
) -> Result<V, TryParseError<V::Error>> {
match std::env::var(key.into()) {
Ok(value) => V::try_from_env_value(value).map_err(TryParseError::Parse),
Err(VarError::NotPresent) => Ok(default),
Err(e) => Err(TryParseError::System(e)),
}
}
pub fn try_parse_optional<K: Into<String>, V: TryFromEnvValue>(key: K) -> Result<Option<V>, TryParseError<V::Error>> {
match std::env::var(key.into()) {
Ok(value) => V::try_from_env_value(value).map(Some).map_err(TryParseError::Parse),
Err(VarError::NotPresent) => Ok(None),
Err(e) => Err(TryParseError::System(e)),
}
}
#[derive(Debug)]
pub enum TryParseError<V> {
System(VarError),
Parse(V),
}
impl<V: Display> Display for TryParseError<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TryParseError::System(s) => Display::fmt(s, f),
TryParseError::Parse(s) => Display::fmt(s, f),
}
}
}
impl<V: std::error::Error + 'static> std::error::Error for TryParseError<V> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::System(v) => Some(v),
Self::Parse(v) => Some(v),
}
}
}
#[deprecated(
since = "0.1.0",
note = "trait is no longer needed as of azalia v0.1.0 (preparation of crates.io ver)"
)]
pub trait FromEnv: Sized {
fn from_env() -> Self;
}
pub trait TryFromEnv: Sized {
type Error;
fn try_from_env() -> Result<Self, Self::Error>;
}
#[allow(deprecated)]
impl<T: FromEnv> TryFromEnv for T {
type Error = Infallible;
fn try_from_env() -> Result<Self, Self::Error> {
Ok(T::from_env())
}
}
pub struct EnvGuard {
name: String,
_non_send_and_sync: PhantomData<Rc<()>>,
}
impl EnvGuard {
pub fn enter(name: impl Into<String>) -> Self {
EnvGuard::enter_with(name, "1")
}
pub fn enter_with(name: impl Into<String>, value: impl AsRef<OsStr>) -> Self {
let name = name.into();
unsafe { std::env::set_var(&name, value) };
EnvGuard {
name,
_non_send_and_sync: PhantomData,
}
}
}
impl PartialEq for EnvGuard {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for EnvGuard {}
impl Hash for EnvGuard {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe { remove_var(&self.name) }
}
}
pub struct MultipleEnvGuard {
_variables: HashSet<EnvGuard>,
_non_send_sync: PhantomData<Rc<()>>,
}
impl MultipleEnvGuard {
pub fn enter(values: impl IntoIterator<Item = (impl Into<String>, impl AsRef<OsStr>)>) -> Self {
MultipleEnvGuard {
_non_send_sync: PhantomData,
_variables: values
.into_iter()
.map(|(key, value)| EnvGuard::enter_with(key, value))
.collect(),
}
}
}
pub fn enter(key: impl Into<String>, f: impl FnOnce()) {
let _guard = EnvGuard::enter(key);
f()
}
pub fn enter_with(key: impl Into<String>, value: impl AsRef<OsStr>, f: impl FnOnce()) {
let _guard = EnvGuard::enter_with(key, value);
f()
}
pub fn enter_multiple(iter: impl IntoIterator<Item = (impl Into<String>, impl AsRef<OsStr>)>, f: impl FnOnce()) {
let _guard = MultipleEnvGuard::enter(iter);
f()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
#[test]
fn drop_multiple_env_guard() {
{
let mut guards = HashSet::new();
guards.insert(EnvGuard::enter("HELLO"));
assert!(std::env::var("HELLO").is_ok());
}
assert!(std::env::var("HELLO").is_err());
{
let _guard = MultipleEnvGuard::enter([("HELLO", "world")]);
assert!(std::env::var("HELLO").is_ok());
}
assert!(std::env::var("HELLO").is_err());
}
#[test]
fn map_try_from_env_value() {
assert!(<HashMap<String, String> as TryFromEnvValue>::try_from_env_value("hello=world".into()).is_ok());
assert!(<HashMap<String, String> as TryFromEnvValue>::try_from_env_value("helloworld".into()).is_ok());
assert!(<HashMap<String, String> as TryFromEnvValue>::try_from_env_value("".into()).is_ok());
assert!(
<HashMap<String, String> as TryFromEnvValue>::try_from_env_value(
"hello=world,weow=fluff;wwww,s=true".into()
)
.is_ok()
);
assert!(<BTreeMap<String, String> as TryFromEnvValue>::try_from_env_value("hello=world".into()).is_ok());
assert!(<BTreeMap<String, String> as TryFromEnvValue>::try_from_env_value("helloworld".into()).is_ok());
assert!(<BTreeMap<String, String> as TryFromEnvValue>::try_from_env_value("".into()).is_ok());
assert!(
<BTreeMap<String, String> as TryFromEnvValue>::try_from_env_value(
"hello=world,weow=fluff;wwww,s=true".into()
)
.is_ok()
);
}
#[test]
fn set_try_from_env_value() {
assert!(<HashSet<String> as TryFromEnvValue>::try_from_env_value("hello,world".into()).is_ok());
assert!(<HashSet<String> as TryFromEnvValue>::try_from_env_value("helloworld".into()).is_ok());
assert!(<HashSet<String> as TryFromEnvValue>::try_from_env_value("".into()).is_ok());
assert!(<HashSet<String> as TryFromEnvValue>::try_from_env_value("hello,world,weow,fluff".into()).is_ok());
assert!(<BTreeSet<String> as TryFromEnvValue>::try_from_env_value("hello,world".into()).is_ok());
assert!(<BTreeSet<String> as TryFromEnvValue>::try_from_env_value("helloworld".into()).is_ok());
assert!(<BTreeSet<String> as TryFromEnvValue>::try_from_env_value("".into()).is_ok());
assert!(<BTreeSet<String> as TryFromEnvValue>::try_from_env_value("hello,world,weow,fluff".into()).is_ok());
}
}