Skip to main content

use_docker_env/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned when Docker environment text is invalid.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum EnvParseError {
10    /// The line had no `=` separator.
11    MissingEquals,
12    /// The environment variable key was empty or invalid.
13    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/// Environment file line classification.
28#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29pub enum EnvLineKind {
30    /// A blank line.
31    Blank,
32    /// A comment line beginning with `#` after trimming leading whitespace.
33    Comment,
34    /// A `KEY=value` variable line.
35    Variable,
36}
37
38/// A validated environment variable key and value.
39#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct EnvVar {
41    key: String,
42    value: String,
43}
44
45impl EnvVar {
46    /// Creates an environment variable pair.
47    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    /// Returns the variable key.
57    #[must_use]
58    pub fn key(&self) -> &str {
59        &self.key
60    }
61
62    /// Returns the variable value.
63    #[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/// A classified Docker env-file line.
87#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct EnvLine {
89    original: String,
90    kind: EnvLineKind,
91    variable: Option<EnvVar>,
92}
93
94impl EnvLine {
95    /// Parses an env-file line.
96    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    /// Returns the original line text.
122    #[must_use]
123    pub fn original(&self) -> &str {
124        &self.original
125    }
126
127    /// Returns the line kind.
128    #[must_use]
129    pub const fn kind(&self) -> EnvLineKind {
130        self.kind
131    }
132
133    /// Returns the parsed variable pair when this is a variable line.
134    #[must_use]
135    pub const fn variable(&self) -> Option<&EnvVar> {
136        self.variable.as_ref()
137    }
138
139    /// Returns the parsed key when this is a variable line.
140    #[must_use]
141    pub fn key(&self) -> Option<&str> {
142        self.variable.as_ref().map(EnvVar::key)
143    }
144
145    /// Returns the parsed value when this is a variable line.
146    #[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}