1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum EnvParseError {
10 MissingEquals,
12 InvalidKey,
14}
15
16impl fmt::Display for EnvParseError {
17 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::MissingEquals => formatter.write_str("environment variable line needs `=`"),
20 Self::InvalidKey => formatter.write_str("invalid environment variable key"),
21 }
22 }
23}
24
25impl Error for EnvParseError {}
26
27#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub enum EnvLineKind {
30 Blank,
32 Comment,
34 Variable,
36}
37
38#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct EnvVar {
41 key: String,
42 value: String,
43}
44
45impl EnvVar {
46 pub fn new(key: impl AsRef<str>, value: impl Into<String>) -> Result<Self, EnvParseError> {
48 let key = key.as_ref().trim();
49 validate_key(key)?;
50 Ok(Self {
51 key: key.to_string(),
52 value: value.into(),
53 })
54 }
55
56 #[must_use]
58 pub fn key(&self) -> &str {
59 &self.key
60 }
61
62 #[must_use]
64 pub fn value(&self) -> &str {
65 &self.value
66 }
67}
68
69impl fmt::Display for EnvVar {
70 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(formatter, "{}={}", self.key, self.value)
72 }
73}
74
75impl FromStr for EnvVar {
76 type Err = EnvParseError;
77
78 fn from_str(value: &str) -> Result<Self, Self::Err> {
79 let Some((key, env_value)) = value.split_once('=') else {
80 return Err(EnvParseError::MissingEquals);
81 };
82 Self::new(key, env_value)
83 }
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct EnvLine {
89 original: String,
90 kind: EnvLineKind,
91 variable: Option<EnvVar>,
92}
93
94impl EnvLine {
95 pub fn parse(value: impl AsRef<str>) -> Result<Self, EnvParseError> {
97 let original = value.as_ref().to_string();
98 let trimmed = value.as_ref().trim();
99 if trimmed.is_empty() {
100 return Ok(Self {
101 original,
102 kind: EnvLineKind::Blank,
103 variable: None,
104 });
105 }
106 if trimmed.starts_with('#') {
107 return Ok(Self {
108 original,
109 kind: EnvLineKind::Comment,
110 variable: None,
111 });
112 }
113 let variable = trimmed.parse()?;
114 Ok(Self {
115 original,
116 kind: EnvLineKind::Variable,
117 variable: Some(variable),
118 })
119 }
120
121 #[must_use]
123 pub fn original(&self) -> &str {
124 &self.original
125 }
126
127 #[must_use]
129 pub const fn kind(&self) -> EnvLineKind {
130 self.kind
131 }
132
133 #[must_use]
135 pub const fn variable(&self) -> Option<&EnvVar> {
136 self.variable.as_ref()
137 }
138
139 #[must_use]
141 pub fn key(&self) -> Option<&str> {
142 self.variable.as_ref().map(EnvVar::key)
143 }
144
145 #[must_use]
147 pub fn value(&self) -> Option<&str> {
148 self.variable.as_ref().map(EnvVar::value)
149 }
150}
151
152fn validate_key(value: &str) -> Result<(), EnvParseError> {
153 let mut chars = value.chars();
154 let Some(first) = chars.next() else {
155 return Err(EnvParseError::InvalidKey);
156 };
157 if !(first == '_' || first.is_ascii_alphabetic()) {
158 return Err(EnvParseError::InvalidKey);
159 }
160 if chars.any(|character| !(character == '_' || character.is_ascii_alphanumeric())) {
161 return Err(EnvParseError::InvalidKey);
162 }
163 Ok(())
164}
165
166#[cfg(test)]
167mod tests {
168 use super::{EnvLine, EnvLineKind, EnvParseError, EnvVar};
169
170 #[test]
171 fn parses_env_lines() -> Result<(), Box<dyn std::error::Error>> {
172 let line = EnvLine::parse("RUST_LOG=info")?;
173 let comment = EnvLine::parse("# local")?;
174
175 assert_eq!(line.kind(), EnvLineKind::Variable);
176 assert_eq!(line.key(), Some("RUST_LOG"));
177 assert_eq!(line.value(), Some("info"));
178 assert_eq!(comment.kind(), EnvLineKind::Comment);
179 assert_eq!(EnvVar::new("1BAD", "value"), Err(EnvParseError::InvalidKey));
180 Ok(())
181 }
182}