use crate::config::Config;
pub fn interpolate_env_vars(raw: &str) -> anyhow::Result<String> {
let mut result = String::with_capacity(raw.len());
let mut chars = raw.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '$' && chars.peek() == Some(&'{') {
chars.next(); let var_name: String = chars.by_ref().take_while(|&c| c != '}').collect();
if var_name.is_empty() {
anyhow::bail!("config error: empty placeholder '${{}}' in gateway.yml");
}
let value = std::env::var(&var_name).map_err(|_| {
anyhow::anyhow!(
"config error: env var '{}' is not set (referenced in gateway.yml)",
var_name
)
})?;
result.push_str(&value);
} else {
result.push(ch);
}
}
Ok(result)
}
pub fn apply_env_overrides(config: &mut Config) {
if let Ok(token) = std::env::var("ARBIT_ADMIN_TOKEN") {
config.admin_token = Some(token);
}
if let Ok(url) = std::env::var("ARBIT_UPSTREAM_URL") {
config.set_upstream_url(url);
}
if let Ok(addr) = std::env::var("ARBIT_LISTEN_ADDR") {
config.set_listen_addr(addr);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_single_placeholder() {
unsafe { std::env::set_var("_ARBIT_TEST_TOKEN", "supersecret") };
let raw = "admin_token: \"${_ARBIT_TEST_TOKEN}\"";
let out = interpolate_env_vars(raw).unwrap();
assert_eq!(out, "admin_token: \"supersecret\"");
unsafe { std::env::remove_var("_ARBIT_TEST_TOKEN") };
}
#[test]
fn resolves_multiple_placeholders() {
unsafe {
std::env::set_var("_ARBIT_A", "val_a");
std::env::set_var("_ARBIT_B", "val_b");
}
let raw = "x: \"${_ARBIT_A}\"\ny: \"${_ARBIT_B}\"";
let out = interpolate_env_vars(raw).unwrap();
assert_eq!(out, "x: \"val_a\"\ny: \"val_b\"");
unsafe {
std::env::remove_var("_ARBIT_A");
std::env::remove_var("_ARBIT_B");
}
}
#[test]
fn passthrough_when_no_placeholders() {
let raw = "admin_token: hardcoded";
let out = interpolate_env_vars(raw).unwrap();
assert_eq!(out, "admin_token: hardcoded");
}
#[test]
fn error_on_unset_variable() {
unsafe { std::env::remove_var("_ARBIT_DEFINITELY_NOT_SET") };
let raw = "admin_token: \"${_ARBIT_DEFINITELY_NOT_SET}\"";
let err = interpolate_env_vars(raw).unwrap_err();
assert!(
err.to_string().contains("_ARBIT_DEFINITELY_NOT_SET"),
"error should name the missing variable"
);
assert!(
err.to_string().contains("not set"),
"error should say 'not set'"
);
}
#[test]
fn error_on_empty_placeholder() {
let raw = "admin_token: \"${}\"";
let err = interpolate_env_vars(raw).unwrap_err();
assert!(err.to_string().contains("empty placeholder"));
}
#[test]
fn dollar_without_brace_is_literal() {
let raw = "cost: $100";
let out = interpolate_env_vars(raw).unwrap();
assert_eq!(out, "cost: $100");
}
#[test]
fn partial_placeholder_not_consumed() {
unsafe { std::env::remove_var("_ARBIT_UNCLOSED") };
let raw = "${_ARBIT_UNCLOSED";
let err = interpolate_env_vars(raw).unwrap_err();
assert!(err.to_string().contains("_ARBIT_UNCLOSED"));
}
}