Skip to main content

shine_core/
env.rs

1//! Frontend-neutral environment declaration grammar used by Presets.
2
3use anyhow::{Result, bail};
4use std::collections::BTreeSet;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct EnvVarSpec {
8    pub source: String,
9    pub target: String,
10}
11
12impl EnvVarSpec {
13    pub fn to_with_arg(&self) -> String {
14        if self.source == self.target {
15            self.source.clone()
16        } else {
17            format!("{}={}", self.source, self.target)
18        }
19    }
20}
21
22pub fn parse_env_specs(specs: &[String]) -> Result<Vec<EnvVarSpec>> {
23    let mut parsed = Vec::with_capacity(specs.len());
24    let mut targets = BTreeSet::new();
25    for spec in specs {
26        let (source, target) = spec.split_once('=').unwrap_or((spec, spec));
27        validate_env_key(source)?;
28        validate_env_key(target)?;
29        if !targets.insert(target.to_string()) {
30            bail!("duplicate target variable: {target}");
31        }
32        parsed.push(EnvVarSpec {
33            source: source.to_string(),
34            target: target.to_string(),
35        });
36    }
37    Ok(parsed)
38}
39
40pub fn validate_env_key(key: &str) -> Result<()> {
41    let mut chars = key.chars();
42    let Some(first) = chars.next() else {
43        bail!("environment variable name must not be empty");
44    };
45    if !(first == '_' || first.is_ascii_alphabetic())
46        || !chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
47    {
48        bail!("invalid environment variable name: {key}");
49    }
50    Ok(())
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn ordered_specs_reject_duplicate_targets() {
59        let error = parse_env_specs(&["A=X".into(), "B=X".into()]).unwrap_err();
60        assert!(error.to_string().contains("duplicate target"));
61    }
62}