foundry_config/
resolve.rs1use regex::Regex;
4use std::{env, env::VarError, fmt, sync::LazyLock};
5
6pub static RE_PLACEHOLDER: LazyLock<Regex> =
8 LazyLock::new(|| Regex::new(r"(?m)(?P<outer>\$\{\s*(?P<inner>.*?)\s*})").unwrap());
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct UnresolvedEnvVarError {
13 pub unresolved: String,
15 pub var: String,
17 pub source: VarError,
19}
20
21impl UnresolvedEnvVarError {
22 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 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
45pub fn interpolate(input: &str) -> Result<String, UnresolvedEnvVarError> {
47 let mut res = input.to_string();
48
49 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}