use std::env;
use std::net::SocketAddr;
#[derive(Debug, Clone)]
pub struct Config {
pub listen_addr: SocketAddr,
pub token_secret: String,
pub claude_code_home: String,
pub upstream_base_url: String,
}
impl Config {
pub fn from_env() -> Result<Self, ConfigError> {
let port = env::var("ROUTER_PORT").unwrap_or_else(|_| "8080".to_string());
let host = env::var("ROUTER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
let token_secret = env::var("TOKEN_SECRET").ok();
let claude_code_home = env::var("CLAUDE_CODE_HOME").unwrap_or_else(|_| {
let home = env::var("HOME").unwrap_or_else(|_| "/root".to_string());
format!("{home}/.claude")
});
let upstream_base_url = env::var("UPSTREAM_BASE_URL")
.unwrap_or_else(|_| "https://api.anthropic.com".to_string());
Self::build(
&host,
&port,
token_secret.as_deref(),
&claude_code_home,
&upstream_base_url,
)
}
pub fn build(
host: &str,
port: &str,
token_secret: Option<&str>,
claude_code_home: &str,
upstream_base_url: &str,
) -> Result<Self, ConfigError> {
let port: u16 = port.parse().map_err(|_| ConfigError::InvalidPort)?;
let listen_addr: SocketAddr = format!("{host}:{port}")
.parse()
.map_err(|_| ConfigError::InvalidAddress)?;
let token_secret = token_secret
.filter(|s| !s.is_empty())
.ok_or(ConfigError::MissingTokenSecret)?
.to_string();
Ok(Self {
listen_addr,
token_secret,
claude_code_home: claude_code_home.to_string(),
upstream_base_url: upstream_base_url.to_string(),
})
}
}
#[derive(Debug)]
pub enum ConfigError {
InvalidPort,
InvalidAddress,
MissingTokenSecret,
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidPort => write!(f, "ROUTER_PORT must be a valid port number (0-65535)"),
Self::InvalidAddress => write!(f, "Could not parse listen address"),
Self::MissingTokenSecret => {
write!(f, "TOKEN_SECRET environment variable is required")
}
}
}
}
impl std::error::Error for ConfigError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_missing_token_secret() {
let result = Config::build(
"0.0.0.0",
"8080",
None,
"/tmp/claude",
"https://api.anthropic.com",
);
assert!(result.is_err());
}
#[test]
fn test_config_empty_token_secret() {
let result = Config::build(
"0.0.0.0",
"8080",
Some(""),
"/tmp/claude",
"https://api.anthropic.com",
);
assert!(result.is_err());
}
#[test]
fn test_config_with_valid_values() {
let config = Config::build(
"127.0.0.1",
"9090",
Some("test-secret-key"),
"/tmp/test-claude",
"https://example.com",
)
.expect("Config should build");
assert_eq!(config.listen_addr.port(), 9090);
assert_eq!(config.token_secret, "test-secret-key");
assert_eq!(config.claude_code_home, "/tmp/test-claude");
assert_eq!(config.upstream_base_url, "https://example.com");
}
#[test]
fn test_config_invalid_port() {
let result = Config::build(
"0.0.0.0",
"not-a-number",
Some("secret"),
"/tmp/claude",
"https://api.anthropic.com",
);
assert!(result.is_err());
}
#[test]
fn test_config_default_port() {
let config = Config::build(
"0.0.0.0",
"8080",
Some("secret"),
"/tmp/claude",
"https://api.anthropic.com",
)
.expect("should build");
assert_eq!(config.listen_addr.port(), 8080);
}
}