use std::net::{SocketAddr, ToSocketAddrs};
#[derive(Clone, Debug)]
pub struct EdgeConfig {
pub listen_addr: SocketAddr,
pub brain_addr: SocketAddr,
pub pool_size: usize,
pub max_credentials: usize,
pub idle_ttl_secs: u64,
pub request_timeout_secs: u64,
pub max_body_bytes: usize,
pub wire_listen_addr: Option<SocketAddr>,
pub wire_rate_capacity: u32,
pub wire_rate_refill_per_sec: u32,
}
fn parse_env<T>(key: &str, default: T) -> Result<T, String>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
match std::env::var(key) {
Err(_) => Ok(default),
Ok(raw) => raw
.trim()
.parse::<T>()
.map_err(|e| format!("{key} `{raw}`: {e}")),
}
}
fn resolve_addr(key: &str, raw: &str) -> Result<SocketAddr, String> {
if let Ok(addr) = raw.parse::<SocketAddr>() {
return Ok(addr);
}
raw.to_socket_addrs()
.map_err(|e| format!("{key} `{raw}`: {e}"))?
.next()
.ok_or_else(|| format!("{key} `{raw}`: resolved to no addresses"))
}
impl EdgeConfig {
pub fn from_env() -> Result<Self, String> {
let listen = std::env::var("BRAIN_EDGE_LISTEN").unwrap_or_else(|_| "0.0.0.0:8080".into());
let brain = std::env::var("BRAIN_ADDR").unwrap_or_else(|_| "127.0.0.1:7878".into());
let pool_size = parse_env::<usize>("BRAIN_EDGE_POOL_SIZE", 4)?;
let max_credentials = parse_env::<usize>("BRAIN_EDGE_MAX_CREDENTIALS", 256)?;
let idle_ttl_secs = parse_env::<u64>("BRAIN_EDGE_IDLE_TTL_SECS", 900)?;
let request_timeout_secs = parse_env::<u64>("BRAIN_EDGE_REQUEST_TIMEOUT_SECS", 30)?;
let max_body_bytes = parse_env::<usize>("BRAIN_EDGE_MAX_BODY_BYTES", 1024 * 1024)?;
let wire_listen_addr = match std::env::var("BRAIN_EDGE_WIRE_LISTEN") {
Err(_) => None,
Ok(raw) => Some(resolve_addr("BRAIN_EDGE_WIRE_LISTEN", &raw)?),
};
let wire_rate_capacity = parse_env::<u32>("BRAIN_EDGE_WIRE_RATE_CAPACITY", 0)?;
let wire_rate_refill_per_sec = parse_env::<u32>("BRAIN_EDGE_WIRE_RATE_REFILL_PER_SEC", 0)?;
if pool_size == 0 {
return Err("BRAIN_EDGE_POOL_SIZE must be >= 1".into());
}
if max_credentials == 0 {
return Err("BRAIN_EDGE_MAX_CREDENTIALS must be >= 1".into());
}
if request_timeout_secs == 0 {
return Err("BRAIN_EDGE_REQUEST_TIMEOUT_SECS must be >= 1".into());
}
if max_body_bytes == 0 {
return Err("BRAIN_EDGE_MAX_BODY_BYTES must be >= 1".into());
}
Ok(Self {
listen_addr: listen
.parse()
.map_err(|e| format!("BRAIN_EDGE_LISTEN `{listen}`: {e}"))?,
brain_addr: resolve_addr("BRAIN_ADDR", &brain)?,
pool_size,
max_credentials,
idle_ttl_secs,
request_timeout_secs,
max_body_bytes,
wire_listen_addr,
wire_rate_capacity,
wire_rate_refill_per_sec,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_addr_accepts_literal_ip_port() {
let addr = resolve_addr("BRAIN_ADDR", "127.0.0.1:7878").expect("literal ip:port");
assert_eq!(addr.port(), 7878);
assert!(addr.ip().is_loopback());
}
#[test]
fn resolve_addr_resolves_a_hostname() {
let addr = resolve_addr("BRAIN_ADDR", "localhost:9090").expect("hostname:port");
assert_eq!(addr.port(), 9090);
assert!(addr.ip().is_loopback());
}
#[test]
fn resolve_addr_rejects_garbage() {
assert!(resolve_addr("BRAIN_ADDR", "not-an-address").is_err());
assert!(resolve_addr("BRAIN_ADDR", "").is_err());
}
#[test]
fn parse_env_uses_default_when_unset() {
let v = parse_env::<usize>("BRAIN_EDGE_TEST_DEFINITELY_UNSET_KEY", 42).expect("default");
assert_eq!(v, 42);
}
}