foundry_config/
resolve.rs

1//! Helper for resolving env vars
2
3use regex::Regex;
4use std::{env, env::VarError, fmt, sync::LazyLock};
5
6/// A regex that matches `${val}` placeholders
7pub static RE_PLACEHOLDER: LazyLock<Regex> =
8    LazyLock::new(|| Regex::new(r"(?m)(?P<outer>\$\{\s*(?P<inner>.*?)\s*})").unwrap());
9
10/// Error when we failed to resolve an env var
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct UnresolvedEnvVarError {
13    /// The unresolved input string
14    pub unresolved: String,
15    /// Var that couldn't be resolved
16    pub var: String,
17    /// the `env::var` error
18    pub source: VarError,
19}
20
21impl UnresolvedEnvVarError {
22    /// Tries to resolve a value
23    pub fn try_resolve(&self) -> Result<String, Self> {
24        interpolate(&self.unresolved)
25    }
26
27    fn is_simple(&self) -> bool {
28        RE_PLACEHOLDER.captures_iter(&self.unresolved).count() <= 1
29    }
30}
31
32impl fmt::Display for UnresolvedEnvVarError {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(f, "environment variable `{}` ", self.var)?;
35        f.write_str(match self.source {
36            VarError::NotPresent => "not found",
37            VarError::NotUnicode(_) => "is not valid unicode",
38        })?;
39        if !self.is_simple() {
40            write!(f, " in `{}`", self.unresolved)?;
41        }
42        Ok(())
43    }
44}
45
46impl std::error::Error for UnresolvedEnvVarError {
47    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
48        Some(&self.source)
49    }
50}
51
52/// Replaces all Env var placeholders in the input string with the values they hold
53pub fn interpolate(input: &str) -> Result<String, UnresolvedEnvVarError> {
54    let mut res = input.to_string();
55
56    // loop over all placeholders in the input and replace them one by one
57    for caps in RE_PLACEHOLDER.captures_iter(input) {
58        let var = &caps["inner"];
59        let value = env::var(var).map_err(|source| UnresolvedEnvVarError {
60            unresolved: input.to_string(),
61            var: var.to_string(),
62            source,
63        })?;
64
65        res = res.replacen(&caps["outer"], &value, 1);
66    }
67    Ok(res)
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn can_find_placeholder() {
76        let val = "https://eth-mainnet.alchemyapi.io/v2/346273846238426342";
77        assert!(!RE_PLACEHOLDER.is_match(val));
78
79        let val = "${RPC_ENV}";
80        assert!(RE_PLACEHOLDER.is_match(val));
81
82        let val = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}";
83        assert!(RE_PLACEHOLDER.is_match(val));
84
85        let cap = RE_PLACEHOLDER.captures(val).unwrap();
86        assert_eq!(cap.name("outer").unwrap().as_str(), "${API_KEY}");
87        assert_eq!(cap.name("inner").unwrap().as_str(), "API_KEY");
88    }
89}