Skip to main content

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
28impl fmt::Display for UnresolvedEnvVarError {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        // The unresolved input is omitted because it can contain literal credentials.
31        write!(f, "environment variable `{}` ", self.var)?;
32        f.write_str(match self.source {
33            VarError::NotPresent => "not found",
34            VarError::NotUnicode(_) => "is not valid unicode",
35        })
36    }
37}
38
39impl std::error::Error for UnresolvedEnvVarError {
40    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
41        Some(&self.source)
42    }
43}
44
45/// Replaces all Env var placeholders in the input string with the values they hold
46pub fn interpolate(input: &str) -> Result<String, UnresolvedEnvVarError> {
47    let mut res = input.to_string();
48
49    // loop over all placeholders in the input and replace them one by one
50    for caps in RE_PLACEHOLDER.captures_iter(input) {
51        let var = &caps["inner"];
52        let value = env::var(var).map_err(|source| UnresolvedEnvVarError {
53            unresolved: input.to_string(),
54            var: var.to_string(),
55            source,
56        })?;
57
58        res = res.replacen(&caps["outer"], &value, 1);
59    }
60    Ok(res)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn can_find_placeholder() {
69        let val = "https://eth-mainnet.alchemyapi.io/v2/346273846238426342";
70        assert!(!RE_PLACEHOLDER.is_match(val));
71
72        let val = "${RPC_ENV}";
73        assert!(RE_PLACEHOLDER.is_match(val));
74
75        let val = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}";
76        assert!(RE_PLACEHOLDER.is_match(val));
77
78        let cap = RE_PLACEHOLDER.captures(val).unwrap();
79        assert_eq!(cap.name("outer").unwrap().as_str(), "${API_KEY}");
80        assert_eq!(cap.name("inner").unwrap().as_str(), "API_KEY");
81    }
82
83    #[test]
84    fn unresolved_error_omits_input() {
85        let err = interpolate(
86            "https://user:synthetic-password@rpc.invalid/${FOUNDRY_TEST_UNSET_A}/${FOUNDRY_TEST_UNSET_B}",
87        )
88        .unwrap_err();
89        assert_eq!(err.to_string(), "environment variable `FOUNDRY_TEST_UNSET_A` not found");
90    }
91}