use std::env;
use std::env::VarError;
use std::fmt;
use std::ops::Deref;
use std::sync::OnceLock;
use serde::de::DeserializeOwned;
use thiserror::Error;
use crate::misc::serde_parse::StringParseDeserializer;
pub struct EnvVar<T = String> {
value: OnceLock<Result<T, EnvError>>,
name: &'static str,
default: Option<fn() -> T>,
}
impl<T: DeserializeOwned> EnvVar<T> {
pub const fn required(name: &'static str) -> Self {
Self {
name,
value: OnceLock::new(),
default: None,
}
}
pub const fn optional(name: &'static str, default: fn() -> T) -> Self {
Self {
name,
value: OnceLock::new(),
default: Some(default),
}
}
pub fn get(&self) -> &T {
self.try_get().unwrap_or_else(|error| panic!("{error}"))
}
pub fn load(&self) -> Result<(), &EnvError> {
self.try_get().map(|_| ())
}
pub fn try_get(&self) -> Result<&T, &EnvError> {
self.value
.get_or_init(|| {
let value = match env::var(self.name) {
Ok(value) => value,
Err(VarError::NotUnicode(_)) => {
return Err(EnvError {
name: self.name,
reason: EnvErrorReason::NotUtf8,
});
}
Err(VarError::NotPresent) => {
return match self.default {
None => Err(EnvError {
name: self.name,
reason: EnvErrorReason::Missing,
}),
Some(default) => Ok(default()),
};
}
};
let is_empty = value.is_empty();
match T::deserialize(StringParseDeserializer::new(value)) {
Ok(value) => Ok(value),
Err(error) => match self.default {
Some(default) if is_empty => Ok(default()),
_ => Err(EnvError {
name: self.name,
reason: EnvErrorReason::Malformed(error.to_string()),
}),
},
}
})
.as_ref()
}
}
impl<T: DeserializeOwned> Deref for EnvVar<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.get()
}
}
impl<T: DeserializeOwned + fmt::Display> fmt::Display for EnvVar<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.get().fmt(f)
}
}
#[derive(Debug, Error, Clone)]
#[error("Environment variable '{name}' is {reason}")]
pub struct EnvError {
pub name: &'static str,
pub reason: EnvErrorReason,
}
#[derive(Debug, Error, Clone)]
pub enum EnvErrorReason {
#[error("not set")]
Missing,
#[error("not utf8")]
NotUtf8,
#[error("malformed: {0}")]
Malformed(String),
}