#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EnvParseError {
MissingEquals,
InvalidKey,
}
impl fmt::Display for EnvParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingEquals => formatter.write_str("environment variable line needs `=`"),
Self::InvalidKey => formatter.write_str("invalid environment variable key"),
}
}
}
impl Error for EnvParseError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum EnvLineKind {
Blank,
Comment,
Variable,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EnvVar {
key: String,
value: String,
}
impl EnvVar {
pub fn new(key: impl AsRef<str>, value: impl Into<String>) -> Result<Self, EnvParseError> {
let key = key.as_ref().trim();
validate_key(key)?;
Ok(Self {
key: key.to_string(),
value: value.into(),
})
}
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
impl fmt::Display for EnvVar {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}={}", self.key, self.value)
}
}
impl FromStr for EnvVar {
type Err = EnvParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let Some((key, env_value)) = value.split_once('=') else {
return Err(EnvParseError::MissingEquals);
};
Self::new(key, env_value)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EnvLine {
original: String,
kind: EnvLineKind,
variable: Option<EnvVar>,
}
impl EnvLine {
pub fn parse(value: impl AsRef<str>) -> Result<Self, EnvParseError> {
let original = value.as_ref().to_string();
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
return Ok(Self {
original,
kind: EnvLineKind::Blank,
variable: None,
});
}
if trimmed.starts_with('#') {
return Ok(Self {
original,
kind: EnvLineKind::Comment,
variable: None,
});
}
let variable = trimmed.parse()?;
Ok(Self {
original,
kind: EnvLineKind::Variable,
variable: Some(variable),
})
}
#[must_use]
pub fn original(&self) -> &str {
&self.original
}
#[must_use]
pub const fn kind(&self) -> EnvLineKind {
self.kind
}
#[must_use]
pub const fn variable(&self) -> Option<&EnvVar> {
self.variable.as_ref()
}
#[must_use]
pub fn key(&self) -> Option<&str> {
self.variable.as_ref().map(EnvVar::key)
}
#[must_use]
pub fn value(&self) -> Option<&str> {
self.variable.as_ref().map(EnvVar::value)
}
}
fn validate_key(value: &str) -> Result<(), EnvParseError> {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return Err(EnvParseError::InvalidKey);
};
if !(first == '_' || first.is_ascii_alphabetic()) {
return Err(EnvParseError::InvalidKey);
}
if chars.any(|character| !(character == '_' || character.is_ascii_alphanumeric())) {
return Err(EnvParseError::InvalidKey);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{EnvLine, EnvLineKind, EnvParseError, EnvVar};
#[test]
fn parses_env_lines() -> Result<(), Box<dyn std::error::Error>> {
let line = EnvLine::parse("RUST_LOG=info")?;
let comment = EnvLine::parse("# local")?;
assert_eq!(line.kind(), EnvLineKind::Variable);
assert_eq!(line.key(), Some("RUST_LOG"));
assert_eq!(line.value(), Some("info"));
assert_eq!(comment.kind(), EnvLineKind::Comment);
assert_eq!(EnvVar::new("1BAD", "value"), Err(EnvParseError::InvalidKey));
Ok(())
}
}