pub mod builder;
pub mod models;
pub mod validation;
pub use validation::Validate;
use crate::config::models::auth::AuthConfig;
use crate::config::models::gateway::GatewayConfig;
use crate::config::models::monitoring::MonitoringConfig;
use crate::config::models::provider::ProviderConfig;
use crate::config::models::router::GatewayRouterConfig;
use crate::config::models::server::ServerConfig;
use crate::config::models::storage::StorageConfig;
use crate::utils::error::gateway_error::{GatewayError, Result};
use regex::Regex;
use std::path::Path;
use tracing::{debug, info, warn};
pub type GatewayServerConfig = crate::config::models::server::ServerConfig;
pub type GatewayProviderConfig = crate::config::models::provider::ProviderConfig;
fn substitute_env_vars(input: &str) -> String {
let re = Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)")
.expect("static regex is valid");
re.replace_all(input, |caps: ®ex::Captures<'_>| {
let var_name = caps
.get(1)
.or_else(|| caps.get(2))
.map(|m| m.as_str())
.unwrap_or("");
match std::env::var(var_name) {
Ok(val) => val,
Err(_) => {
warn!(
"Config env var '{}' is not set; leaving placeholder as-is",
var_name
);
caps[0].to_string()
}
}
})
.into_owned()
}
#[derive(Debug, Clone, Default)]
pub struct Config {
pub gateway: GatewayConfig,
}
impl Config {
pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
info!("Loading configuration from: {:?}", path);
let content = tokio::fs::read_to_string(path)
.await
.map_err(|e| GatewayError::Config(format!("Failed to read config file: {}", e)))?;
let content = substitute_env_vars(&content);
let gateway: GatewayConfig = serde_yml::from_str(&content)
.map_err(|e| GatewayError::Config(format!("Failed to parse config: {}", e)))?;
let config = Self { gateway };
config.validate()?;
debug!("Configuration loaded successfully");
Ok(config)
}
pub fn from_env() -> Result<Self> {
info!("Loading configuration from environment variables");
let gateway = GatewayConfig::from_env()?;
let config = Self { gateway };
config.validate()?;
Ok(config)
}
pub fn server(&self) -> &ServerConfig {
&self.gateway.server
}
pub fn providers(&self) -> &[ProviderConfig] {
&self.gateway.providers
}
pub fn router(&self) -> &GatewayRouterConfig {
&self.gateway.router
}
pub fn storage(&self) -> &StorageConfig {
&self.gateway.storage
}
pub fn auth(&self) -> &AuthConfig {
&self.gateway.auth
}
pub fn monitoring(&self) -> &MonitoringConfig {
&self.gateway.monitoring
}
pub fn validate(&self) -> Result<()> {
debug!("Validating configuration");
validation::Validate::validate(&self.gateway)
.map_err(|e| GatewayError::Config(format!("Gateway config error: {}", e)))?;
crate::config::models::auth::warn_insecure_config(&self.gateway.auth);
debug!("Configuration validation completed");
Ok(())
}
pub fn merge(mut self, other: Self) -> Self {
self.gateway = self.gateway.merge(other.gateway);
self
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(&self.gateway)
.map_err(|e| GatewayError::Config(format!("Failed to serialize config to JSON: {}", e)))
}
pub fn to_yaml(&self) -> Result<String> {
serde_yml::to_string(&self.gateway)
.map_err(|e| GatewayError::Config(format!("Failed to serialize config to YAML: {}", e)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_substitute_env_vars_braces() {
unsafe { std::env::set_var("TEST_HOST", "example.com") };
let result = substitute_env_vars("host: ${TEST_HOST}");
assert_eq!(result, "host: example.com");
unsafe { std::env::remove_var("TEST_HOST") };
}
#[test]
fn test_substitute_env_vars_bare() {
unsafe { std::env::set_var("TEST_PORT", "9000") };
let result = substitute_env_vars("port: $TEST_PORT");
assert_eq!(result, "port: 9000");
unsafe { std::env::remove_var("TEST_PORT") };
}
#[test]
fn test_substitute_env_vars_missing_leaves_placeholder() {
unsafe { std::env::remove_var("DEFINITELY_NOT_SET_XYZ") };
let result = substitute_env_vars("key: ${DEFINITELY_NOT_SET_XYZ}");
assert_eq!(result, "key: ${DEFINITELY_NOT_SET_XYZ}");
}
#[tokio::test]
async fn test_config_from_file() {
let config_content = r#"
server:
host: "127.0.0.1"
port: 8080
workers: 4
providers:
- name: "openai"
provider_type: "openai"
api_key: "test-key"
api_base: "https://api.openai.com/v1"
router:
strategy: "round_robin"
circuit_breaker:
failure_threshold: 5
recovery_timeout: 30
storage:
database:
url: "postgresql://localhost/gateway"
redis:
url: "redis://localhost:6379"
auth:
jwt_secret: "test-secret-that-is-at-least-32-characters-long-for-security"
api_key_header: "Authorization"
monitoring:
metrics:
enabled: true
port: 9090
"#;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(config_content.as_bytes()).unwrap();
let config = Config::from_file(temp_file.path()).await.unwrap();
assert_eq!(config.server().host, "127.0.0.1");
assert_eq!(config.server().port, 8080);
assert_eq!(config.providers().len(), 1);
assert_eq!(config.providers()[0].name, "openai");
}
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.validate().is_err());
}
#[test]
fn test_config_serialization() {
let config = Config::default();
let json = config.to_json().unwrap();
assert!(!json.is_empty());
let yaml = config.to_yaml().unwrap();
assert!(!yaml.is_empty());
}
}