foundry_config/
resolve.rsuse regex::Regex;
use std::{env, env::VarError, fmt, sync::LazyLock};
pub static RE_PLACEHOLDER: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)(?P<outer>\$\{\s*(?P<inner>.*?)\s*})").unwrap());
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnresolvedEnvVarError {
pub unresolved: String,
pub var: String,
pub source: VarError,
}
impl UnresolvedEnvVarError {
pub fn try_resolve(&self) -> Result<String, Self> {
interpolate(&self.unresolved)
}
fn is_simple(&self) -> bool {
RE_PLACEHOLDER.captures_iter(&self.unresolved).count() <= 1
}
}
impl fmt::Display for UnresolvedEnvVarError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "environment variable `{}` ", self.var)?;
f.write_str(match self.source {
VarError::NotPresent => "not found",
VarError::NotUnicode(_) => "is not valid unicode",
})?;
if !self.is_simple() {
write!(f, " in `{}`", self.unresolved)?;
}
Ok(())
}
}
impl std::error::Error for UnresolvedEnvVarError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
pub fn interpolate(input: &str) -> Result<String, UnresolvedEnvVarError> {
let mut res = input.to_string();
for caps in RE_PLACEHOLDER.captures_iter(input) {
let var = &caps["inner"];
let value = env::var(var).map_err(|source| UnresolvedEnvVarError {
unresolved: input.to_string(),
var: var.to_string(),
source,
})?;
res = res.replacen(&caps["outer"], &value, 1);
}
Ok(res)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_find_placeholder() {
let val = "https://eth-mainnet.alchemyapi.io/v2/346273846238426342";
assert!(!RE_PLACEHOLDER.is_match(val));
let val = "${RPC_ENV}";
assert!(RE_PLACEHOLDER.is_match(val));
let val = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}";
assert!(RE_PLACEHOLDER.is_match(val));
let cap = RE_PLACEHOLDER.captures(val).unwrap();
assert_eq!(cap.name("outer").unwrap().as_str(), "${API_KEY}");
assert_eq!(cap.name("inner").unwrap().as_str(), "API_KEY");
}
}