#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, marker::PhantomData, str::FromStr};
use std::env;
pub mod prelude {
pub use crate::{
EnvVarName, EnvVarNameError, EnvVarReadError, EnvVarValue, TypedEnvVar, TypedEnvVarError,
is_valid_env_var_name, read_env_var, read_optional_env_var,
};
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EnvVarNameError {
Empty,
StartsWithDigit,
InvalidCharacter,
}
impl fmt::Display for EnvVarNameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("environment variable name cannot be empty"),
Self::StartsWithDigit => {
formatter.write_str("environment variable name cannot start with a digit")
},
Self::InvalidCharacter => formatter.write_str(
"environment variable name must use ASCII letters, digits, and underscores",
),
}
}
}
impl std::error::Error for EnvVarNameError {}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct EnvVarName {
name: String,
}
impl EnvVarName {
pub fn new(name: impl Into<String>) -> Result<Self, EnvVarNameError> {
let name = name.into();
validate_env_var_name(&name)?;
Ok(Self { name })
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.name
}
}
impl AsRef<str> for EnvVarName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for EnvVarName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.name)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct EnvVarValue {
value: String,
}
impl EnvVarValue {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.value
}
#[must_use]
pub fn into_string(self) -> String {
self.value
}
}
impl AsRef<str> for EnvVarValue {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<String> for EnvVarValue {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&str> for EnvVarValue {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl fmt::Display for EnvVarValue {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EnvVarReadError {
NotPresent { name: String },
NotUnicode { name: String },
}
impl fmt::Display for EnvVarReadError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotPresent { name } => {
write!(formatter, "environment variable {name} is not set")
},
Self::NotUnicode { name } => {
write!(
formatter,
"environment variable {name} is not valid Unicode"
)
},
}
}
}
impl std::error::Error for EnvVarReadError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TypedEnvVar<T> {
name: EnvVarName,
marker: PhantomData<fn() -> T>,
}
impl<T> TypedEnvVar<T> {
#[must_use]
pub const fn new(name: EnvVarName) -> Self {
Self {
name,
marker: PhantomData,
}
}
#[must_use]
pub const fn name(&self) -> &EnvVarName {
&self.name
}
pub fn read_with<E>(
&self,
parser: impl FnOnce(&str) -> Result<T, E>,
) -> Result<T, TypedEnvVarError<E>> {
let value = read_env_var(&self.name).map_err(TypedEnvVarError::Read)?;
parser(value.as_str()).map_err(TypedEnvVarError::Parse)
}
pub fn read_parse(&self) -> Result<T, TypedEnvVarError<T::Err>>
where
T: FromStr,
{
self.read_with(str::parse)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TypedEnvVarError<E> {
Read(EnvVarReadError),
Parse(E),
}
impl<E: fmt::Display> fmt::Display for TypedEnvVarError<E> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Read(error) => write!(formatter, "{error}"),
Self::Parse(error) => write!(formatter, "environment variable parse failed: {error}"),
}
}
}
impl<E> std::error::Error for TypedEnvVarError<E> where E: fmt::Debug + fmt::Display {}
#[must_use]
pub fn is_valid_env_var_name(name: &str) -> bool {
validate_env_var_name(name).is_ok()
}
pub fn read_env_var(name: &EnvVarName) -> Result<EnvVarValue, EnvVarReadError> {
env::var(name.as_str())
.map(EnvVarValue::new)
.map_err(|error| match error {
env::VarError::NotPresent => EnvVarReadError::NotPresent {
name: name.as_str().to_owned(),
},
env::VarError::NotUnicode(_) => EnvVarReadError::NotUnicode {
name: name.as_str().to_owned(),
},
})
}
pub fn read_optional_env_var(name: &EnvVarName) -> Result<Option<EnvVarValue>, EnvVarReadError> {
match read_env_var(name) {
Ok(value) => Ok(Some(value)),
Err(EnvVarReadError::NotPresent { .. }) => Ok(None),
Err(error) => Err(error),
}
}
fn validate_env_var_name(name: &str) -> Result<(), EnvVarNameError> {
let bytes = name.as_bytes();
if bytes.is_empty() {
return Err(EnvVarNameError::Empty);
}
if bytes[0].is_ascii_digit() {
return Err(EnvVarNameError::StartsWithDigit);
}
if bytes
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
{
Ok(())
} else {
Err(EnvVarNameError::InvalidCharacter)
}
}
#[cfg(test)]
mod tests {
use super::{
EnvVarName, EnvVarNameError, EnvVarValue, TypedEnvVar, is_valid_env_var_name,
read_optional_env_var,
};
#[test]
fn validates_env_var_names() {
assert!(is_valid_env_var_name("RUST_LOG"));
assert!(is_valid_env_var_name("_RUSTUSE"));
assert_eq!(EnvVarName::new(""), Err(EnvVarNameError::Empty));
assert_eq!(
EnvVarName::new("1RUST"),
Err(EnvVarNameError::StartsWithDigit)
);
assert_eq!(
EnvVarName::new("RUST-LOG"),
Err(EnvVarNameError::InvalidCharacter)
);
}
#[test]
fn stores_owned_values_and_typed_names() -> Result<(), EnvVarNameError> {
let name = EnvVarName::new("RUSTUSE_EXAMPLE")?;
let typed = TypedEnvVar::<u16>::new(name.clone());
let value = EnvVarValue::new("42");
assert_eq!(typed.name(), &name);
assert_eq!(value.as_str(), "42");
Ok(())
}
#[test]
fn optional_read_reports_missing_as_none() -> Result<(), Box<dyn std::error::Error>> {
let name = EnvVarName::new("RUSTUSE_USE_CLI_TEST_SHOULD_NOT_EXIST_9B6AE5E0")?;
assert_eq!(read_optional_env_var(&name)?, None);
Ok(())
}
}