use std::net::SocketAddr;
#[derive(Debug, Clone)]
pub struct Config {
pub database_url: String,
pub http_addr: SocketAddr,
pub max_concurrent_instances: u32,
}
impl Config {
pub fn from_env() -> Result<Self, ConfigError> {
let database_url = std::env::var("RUNTARA_DATABASE_URL")
.map_err(|_| ConfigError::Missing("RUNTARA_DATABASE_URL"))?;
let http_port: u16 = std::env::var("RUNTARA_HTTP_PORT")
.unwrap_or_else(|_| "8001".to_string())
.parse()
.map_err(|_| {
ConfigError::Invalid("RUNTARA_HTTP_PORT", "must be a valid port number")
})?;
let max_concurrent_instances: u32 = std::env::var("RUNTARA_MAX_CONCURRENT_INSTANCES")
.unwrap_or_else(|_| "32".to_string())
.parse()
.map_err(|_| {
ConfigError::Invalid(
"RUNTARA_MAX_CONCURRENT_INSTANCES",
"must be a positive integer",
)
})?;
Ok(Self {
database_url,
http_addr: SocketAddr::from(([0, 0, 0, 0], http_port)),
max_concurrent_instances,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("missing required environment variable: {0}")]
Missing(&'static str),
#[error("invalid value for {0}: {1}")]
Invalid(&'static str, &'static str),
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::sync::Mutex;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
struct EnvGuard {
vars: Vec<(String, Option<String>)>,
}
impl EnvGuard {
fn new() -> Self {
Self { vars: Vec::new() }
}
fn set(&mut self, key: &str, value: &str) {
let old = env::var(key).ok();
self.vars.push((key.to_string(), old));
unsafe { env::set_var(key, value) };
}
fn remove(&mut self, key: &str) {
let old = env::var(key).ok();
self.vars.push((key.to_string(), old));
unsafe { env::remove_var(key) };
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (key, value) in self.vars.drain(..).rev() {
unsafe {
match value {
Some(v) => env::set_var(&key, v),
None => env::remove_var(&key),
}
}
}
}
}
#[test]
fn test_config_from_env_with_defaults() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.set("RUNTARA_DATABASE_URL", "postgres://localhost/test");
guard.remove("RUNTARA_HTTP_PORT");
guard.remove("RUNTARA_MAX_CONCURRENT_INSTANCES");
let config = Config::from_env().unwrap();
assert_eq!(config.database_url, "postgres://localhost/test");
assert_eq!(config.http_addr.port(), 8001);
assert_eq!(config.max_concurrent_instances, 32);
}
#[test]
fn test_config_from_env_with_custom_port() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.set("RUNTARA_DATABASE_URL", "sqlite:test.db");
guard.set("RUNTARA_HTTP_PORT", "9999");
guard.remove("RUNTARA_MAX_CONCURRENT_INSTANCES");
let config = Config::from_env().unwrap();
assert_eq!(config.database_url, "sqlite:test.db");
assert_eq!(config.http_addr.port(), 9999);
assert_eq!(config.max_concurrent_instances, 32);
}
#[test]
fn test_config_from_env_with_custom_max_instances() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.set("RUNTARA_DATABASE_URL", "postgres://localhost/test");
guard.remove("RUNTARA_HTTP_PORT");
guard.set("RUNTARA_MAX_CONCURRENT_INSTANCES", "100");
let config = Config::from_env().unwrap();
assert_eq!(config.max_concurrent_instances, 100);
}
#[test]
fn test_config_from_env_all_custom() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.set("RUNTARA_DATABASE_URL", "postgres://user:pass@db:5432/prod");
guard.set("RUNTARA_HTTP_PORT", "8080");
guard.set("RUNTARA_MAX_CONCURRENT_INSTANCES", "256");
let config = Config::from_env().unwrap();
assert_eq!(config.database_url, "postgres://user:pass@db:5432/prod");
assert_eq!(config.http_addr.port(), 8080);
assert_eq!(config.max_concurrent_instances, 256);
}
#[test]
fn test_config_missing_database_url() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.remove("RUNTARA_DATABASE_URL");
let result = Config::from_env();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ConfigError::Missing("RUNTARA_DATABASE_URL")));
assert!(err.to_string().contains("RUNTARA_DATABASE_URL"));
}
#[test]
fn test_config_invalid_http_port() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.set("RUNTARA_DATABASE_URL", "postgres://localhost/test");
guard.set("RUNTARA_HTTP_PORT", "not_a_number");
let result = Config::from_env();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ConfigError::Invalid("RUNTARA_HTTP_PORT", _)));
}
#[test]
fn test_config_invalid_http_port_out_of_range() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.set("RUNTARA_DATABASE_URL", "postgres://localhost/test");
guard.set("RUNTARA_HTTP_PORT", "99999");
let result = Config::from_env();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, ConfigError::Invalid("RUNTARA_HTTP_PORT", _)));
}
#[test]
fn test_config_invalid_max_concurrent_instances() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.set("RUNTARA_DATABASE_URL", "postgres://localhost/test");
guard.set("RUNTARA_MAX_CONCURRENT_INSTANCES", "abc");
let result = Config::from_env();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(
err,
ConfigError::Invalid("RUNTARA_MAX_CONCURRENT_INSTANCES", _)
));
}
#[test]
fn test_config_negative_max_concurrent_instances() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.set("RUNTARA_DATABASE_URL", "postgres://localhost/test");
guard.set("RUNTARA_MAX_CONCURRENT_INSTANCES", "-5");
let result = Config::from_env();
assert!(result.is_err());
}
#[test]
fn test_config_error_display() {
let missing = ConfigError::Missing("MY_VAR");
assert_eq!(
missing.to_string(),
"missing required environment variable: MY_VAR"
);
let invalid = ConfigError::Invalid("MY_VAR", "must be a number");
assert_eq!(
invalid.to_string(),
"invalid value for MY_VAR: must be a number"
);
}
#[test]
fn test_config_debug() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.set("RUNTARA_DATABASE_URL", "postgres://localhost/test");
guard.remove("RUNTARA_HTTP_PORT");
guard.remove("RUNTARA_MAX_CONCURRENT_INSTANCES");
let config = Config::from_env().unwrap();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("Config"));
assert!(debug_str.contains("database_url"));
assert!(debug_str.contains("http_addr"));
}
#[test]
fn test_config_clone() {
let _lock = ENV_MUTEX.lock().unwrap();
let mut guard = EnvGuard::new();
guard.set("RUNTARA_DATABASE_URL", "postgres://localhost/test");
guard.remove("RUNTARA_HTTP_PORT");
guard.remove("RUNTARA_MAX_CONCURRENT_INSTANCES");
let config = Config::from_env().unwrap();
let cloned = config.clone();
assert_eq!(config.database_url, cloned.database_url);
assert_eq!(config.http_addr, cloned.http_addr);
assert_eq!(
config.max_concurrent_instances,
cloned.max_concurrent_instances
);
}
}