veza/veza-common/src/config_rust.rs
2026-03-05 19:22:31 +01:00

463 lines
16 KiB
Rust

//! Configuration management for Veza Rust services
//!
//! This module provides centralized configuration management with validation
//! and environment variable support.
use serde::{Deserialize, Serialize};
use std::env;
use tracing::info;
/// Main configuration trait for Veza services
pub trait VezaConfig: Default + Clone + Serialize {
/// Load configuration from environment variables
fn from_env() -> crate::VezaResult<Self> {
let mut config = Self::default();
config.load_from_env()?;
config.validate()?;
Ok(config)
}
/// Load configuration from environment variables
fn load_from_env(&mut self) -> crate::VezaResult<()>;
/// Validate configuration
fn validate(&self) -> crate::VezaResult<()>;
/// Get configuration as JSON for debugging
fn to_json(&self) -> crate::VezaResult<String> {
Ok(serde_json::to_string_pretty(self)?)
}
}
/// Database configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
pub url: String,
pub max_connections: u32,
pub min_connections: u32,
pub connect_timeout: u64,
pub idle_timeout: u64,
pub acquire_timeout: u64,
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
url: "postgresql://veza:password@localhost:5432/veza_db".to_string(),
max_connections: 20,
min_connections: 5,
connect_timeout: 5,
idle_timeout: 600,
acquire_timeout: 10,
}
}
}
impl VezaConfig for DatabaseConfig {
fn load_from_env(&mut self) -> crate::VezaResult<()> {
if let Ok(url) = env::var("DATABASE_URL") {
self.url = url;
}
if let Ok(max_conn) = env::var("DATABASE_MAX_CONNECTIONS") {
self.max_connections = max_conn.parse()
.map_err(|_| crate::VezaError::Config("Invalid DATABASE_MAX_CONNECTIONS".to_string()))?;
}
if let Ok(min_conn) = env::var("DATABASE_MIN_CONNECTIONS") {
self.min_connections = min_conn.parse()
.map_err(|_| crate::VezaError::Config("Invalid DATABASE_MIN_CONNECTIONS".to_string()))?;
}
if let Ok(timeout) = env::var("DATABASE_CONNECT_TIMEOUT") {
self.connect_timeout = timeout.parse()
.map_err(|_| crate::VezaError::Config("Invalid DATABASE_CONNECT_TIMEOUT".to_string()))?;
}
if let Ok(timeout) = env::var("DATABASE_IDLE_TIMEOUT") {
self.idle_timeout = timeout.parse()
.map_err(|_| crate::VezaError::Config("Invalid DATABASE_IDLE_TIMEOUT".to_string()))?;
}
if let Ok(timeout) = env::var("DATABASE_ACQUIRE_TIMEOUT") {
self.acquire_timeout = timeout.parse()
.map_err(|_| crate::VezaError::Config("Invalid DATABASE_ACQUIRE_TIMEOUT".to_string()))?;
}
Ok(())
}
fn validate(&self) -> crate::VezaResult<()> {
if self.url.is_empty() {
return Err(crate::VezaError::Config("Database URL cannot be empty".to_string()));
}
if self.max_connections < self.min_connections {
return Err(crate::VezaError::Config("Max connections must be >= min connections".to_string()));
}
if self.max_connections == 0 {
return Err(crate::VezaError::Config("Max connections must be > 0".to_string()));
}
Ok(())
}
}
/// Redis configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedisConfig {
pub url: String,
pub max_connections: u32,
pub connection_timeout: u64,
pub command_timeout: u64,
pub retry_attempts: u32,
}
impl Default for RedisConfig {
fn default() -> Self {
Self {
url: "redis://localhost:6379".to_string(),
max_connections: 10,
connection_timeout: 5,
command_timeout: 3,
retry_attempts: 3,
}
}
}
impl VezaConfig for RedisConfig {
fn load_from_env(&mut self) -> crate::VezaResult<()> {
if let Ok(url) = env::var("REDIS_URL") {
self.url = url;
}
if let Ok(max_conn) = env::var("REDIS_MAX_CONNECTIONS") {
self.max_connections = max_conn.parse()
.map_err(|_| crate::VezaError::Config("Invalid REDIS_MAX_CONNECTIONS".to_string()))?;
}
if let Ok(timeout) = env::var("REDIS_CONNECTION_TIMEOUT") {
self.connection_timeout = timeout.parse()
.map_err(|_| crate::VezaError::Config("Invalid REDIS_CONNECTION_TIMEOUT".to_string()))?;
}
if let Ok(timeout) = env::var("REDIS_COMMAND_TIMEOUT") {
self.command_timeout = timeout.parse()
.map_err(|_| crate::VezaError::Config("Invalid REDIS_COMMAND_TIMEOUT".to_string()))?;
}
if let Ok(retries) = env::var("REDIS_RETRY_ATTEMPTS") {
self.retry_attempts = retries.parse()
.map_err(|_| crate::VezaError::Config("Invalid REDIS_RETRY_ATTEMPTS".to_string()))?;
}
Ok(())
}
fn validate(&self) -> crate::VezaResult<()> {
if self.url.is_empty() {
return Err(crate::VezaError::Config("Redis URL cannot be empty".to_string()));
}
if self.max_connections == 0 {
return Err(crate::VezaError::Config("Redis max connections must be > 0".to_string()));
}
Ok(())
}
}
/// Server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub workers: Option<usize>,
pub keep_alive: u64,
pub max_connections: u32,
pub timeout: u64,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "0.0.0.0".to_string(),
port: 8080,
workers: None,
keep_alive: 30,
max_connections: 1000,
timeout: 30,
}
}
}
impl VezaConfig for ServerConfig {
fn load_from_env(&mut self) -> crate::VezaResult<()> {
if let Ok(host) = env::var("SERVER_HOST") {
self.host = host;
}
if let Ok(port) = env::var("SERVER_PORT") {
self.port = port.parse()
.map_err(|_| crate::VezaError::Config("Invalid SERVER_PORT".to_string()))?;
}
if let Ok(workers) = env::var("SERVER_WORKERS") {
self.workers = Some(workers.parse()
.map_err(|_| crate::VezaError::Config("Invalid SERVER_WORKERS".to_string()))?);
}
if let Ok(keep_alive) = env::var("SERVER_KEEP_ALIVE") {
self.keep_alive = keep_alive.parse()
.map_err(|_| crate::VezaError::Config("Invalid SERVER_KEEP_ALIVE".to_string()))?;
}
if let Ok(max_conn) = env::var("SERVER_MAX_CONNECTIONS") {
self.max_connections = max_conn.parse()
.map_err(|_| crate::VezaError::Config("Invalid SERVER_MAX_CONNECTIONS".to_string()))?;
}
if let Ok(timeout) = env::var("SERVER_TIMEOUT") {
self.timeout = timeout.parse()
.map_err(|_| crate::VezaError::Config("Invalid SERVER_TIMEOUT".to_string()))?;
}
Ok(())
}
fn validate(&self) -> crate::VezaResult<()> {
if self.host.is_empty() {
return Err(crate::VezaError::Config("Server host cannot be empty".to_string()));
}
if self.port == 0 {
return Err(crate::VezaError::Config("Server port must be > 0".to_string()));
}
if self.max_connections == 0 {
return Err(crate::VezaError::Config("Server max connections must be > 0".to_string()));
}
Ok(())
}
}
/// JWT configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtConfig {
pub secret: String,
pub access_token_ttl: u64,
pub refresh_token_ttl: u64,
pub issuer: String,
pub audience: String,
}
impl Default for JwtConfig {
fn default() -> Self {
Self {
// VEZA-SEC-001: No default secret — JWT_SECRET must be set via load_from_env
secret: String::new(),
access_token_ttl: 3600, // 1 hour
refresh_token_ttl: 604800, // 7 days
issuer: "veza-api".to_string(),
audience: "veza-platform".to_string(),
}
}
}
impl VezaConfig for JwtConfig {
fn load_from_env(&mut self) -> crate::VezaResult<()> {
// VEZA-SEC-001: JWT_SECRET is REQUIRED — refusing to start with no secret
self.secret = env::var("JWT_SECRET")
.map_err(|_| crate::VezaError::Config("JWT_SECRET must be set — refusing to start with no secret".to_string()))?;
if let Ok(ttl) = env::var("JWT_ACCESS_TOKEN_TTL") {
self.access_token_ttl = ttl.parse()
.map_err(|_| crate::VezaError::Config("Invalid JWT_ACCESS_TOKEN_TTL".to_string()))?;
}
if let Ok(ttl) = env::var("JWT_REFRESH_TOKEN_TTL") {
self.refresh_token_ttl = ttl.parse()
.map_err(|_| crate::VezaError::Config("Invalid JWT_REFRESH_TOKEN_TTL".to_string()))?;
}
if let Ok(issuer) = env::var("JWT_ISSUER") {
self.issuer = issuer;
}
if let Ok(audience) = env::var("JWT_AUDIENCE") {
self.audience = audience;
}
Ok(())
}
fn validate(&self) -> crate::VezaResult<()> {
if self.secret.is_empty() {
return Err(crate::VezaError::Config("JWT secret cannot be empty".to_string()));
}
if self.secret.len() < 32 {
return Err(crate::VezaError::Config(
"JWT secret must be at least 32 characters".to_string(),
));
}
if self.access_token_ttl == 0 {
return Err(crate::VezaError::Config("JWT access token TTL must be > 0".to_string()));
}
if self.refresh_token_ttl == 0 {
return Err(crate::VezaError::Config("JWT refresh token TTL must be > 0".to_string()));
}
if self.issuer.is_empty() {
return Err(crate::VezaError::Config("JWT issuer cannot be empty".to_string()));
}
if self.audience.is_empty() {
return Err(crate::VezaError::Config("JWT audience cannot be empty".to_string()));
}
Ok(())
}
}
/// Rate limiting configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
pub enabled: bool,
pub global_limit: u32,
pub global_window: u64,
pub per_user_limit: u32,
pub per_user_window: u64,
pub per_ip_limit: u32,
pub per_ip_window: u64,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
enabled: true,
global_limit: 1000,
global_window: 60,
per_user_limit: 100,
per_user_window: 60,
per_ip_limit: 60,
per_ip_window: 60,
}
}
}
impl VezaConfig for RateLimitConfig {
fn load_from_env(&mut self) -> crate::VezaResult<()> {
if let Ok(enabled) = env::var("RATE_LIMIT_ENABLED") {
self.enabled = enabled.parse()
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_ENABLED".to_string()))?;
}
if let Ok(limit) = env::var("RATE_LIMIT_GLOBAL_LIMIT") {
self.global_limit = limit.parse()
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_GLOBAL_LIMIT".to_string()))?;
}
if let Ok(window) = env::var("RATE_LIMIT_GLOBAL_WINDOW") {
self.global_window = window.parse()
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_GLOBAL_WINDOW".to_string()))?;
}
if let Ok(limit) = env::var("RATE_LIMIT_PER_USER_LIMIT") {
self.per_user_limit = limit.parse()
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_USER_LIMIT".to_string()))?;
}
if let Ok(window) = env::var("RATE_LIMIT_PER_USER_WINDOW") {
self.per_user_window = window.parse()
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_USER_WINDOW".to_string()))?;
}
if let Ok(limit) = env::var("RATE_LIMIT_PER_IP_LIMIT") {
self.per_ip_limit = limit.parse()
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_IP_LIMIT".to_string()))?;
}
if let Ok(window) = env::var("RATE_LIMIT_PER_IP_WINDOW") {
self.per_ip_window = window.parse()
.map_err(|_| crate::VezaError::Config("Invalid RATE_LIMIT_PER_IP_WINDOW".to_string()))?;
}
Ok(())
}
fn validate(&self) -> crate::VezaResult<()> {
if self.global_limit == 0 {
return Err(crate::VezaError::Config("Global rate limit must be > 0".to_string()));
}
if self.global_window == 0 {
return Err(crate::VezaError::Config("Global rate limit window must be > 0".to_string()));
}
if self.per_user_limit == 0 {
return Err(crate::VezaError::Config("Per-user rate limit must be > 0".to_string()));
}
if self.per_user_window == 0 {
return Err(crate::VezaError::Config("Per-user rate limit window must be > 0".to_string()));
}
if self.per_ip_limit == 0 {
return Err(crate::VezaError::Config("Per-IP rate limit must be > 0".to_string()));
}
if self.per_ip_window == 0 {
return Err(crate::VezaError::Config("Per-IP rate limit window must be > 0".to_string()));
}
Ok(())
}
}
/// Logging configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
pub format: String,
pub file: Option<String>,
pub max_size: u64,
pub max_files: u32,
pub compress: bool,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
format: "json".to_string(),
file: None,
max_size: 100 * 1024 * 1024, // 100MB
max_files: 5,
compress: true,
}
}
}
impl VezaConfig for LoggingConfig {
fn load_from_env(&mut self) -> crate::VezaResult<()> {
if let Ok(level) = env::var("LOG_LEVEL") {
self.level = level;
}
if let Ok(format) = env::var("LOG_FORMAT") {
self.format = format;
}
if let Ok(file) = env::var("LOG_FILE") {
self.file = Some(file);
}
if let Ok(size) = env::var("LOG_MAX_SIZE") {
self.max_size = size.parse()
.map_err(|_| crate::VezaError::Config("Invalid LOG_MAX_SIZE".to_string()))?;
}
if let Ok(files) = env::var("LOG_MAX_FILES") {
self.max_files = files.parse()
.map_err(|_| crate::VezaError::Config("Invalid LOG_MAX_FILES".to_string()))?;
}
if let Ok(compress) = env::var("LOG_COMPRESS") {
self.compress = compress.parse()
.map_err(|_| crate::VezaError::Config("Invalid LOG_COMPRESS".to_string()))?;
}
Ok(())
}
fn validate(&self) -> crate::VezaResult<()> {
let valid_levels = ["trace", "debug", "info", "warn", "error"];
if !valid_levels.contains(&self.level.as_str()) {
return Err(crate::VezaError::Config(format!("Invalid log level: {}", self.level)));
}
let valid_formats = ["json", "text"];
if !valid_formats.contains(&self.format.as_str()) {
return Err(crate::VezaError::Config(format!("Invalid log format: {}", self.format)));
}
if self.max_size == 0 {
return Err(crate::VezaError::Config("Log max size must be > 0".to_string()));
}
if self.max_files == 0 {
return Err(crate::VezaError::Config("Log max files must be > 0".to_string()));
}
Ok(())
}
}
/// Load configuration from environment with validation
pub fn load_config<T: VezaConfig>() -> crate::VezaResult<T> {
let config = T::from_env()?;
info!("Configuration loaded successfully");
Ok(config)
}
/// Load configuration from file
pub fn load_config_from_file<T: VezaConfig>(path: &str) -> crate::VezaResult<T>
where
T: for<'de> Deserialize<'de>,
{
let content = std::fs::read_to_string(path)
.map_err(|e| crate::VezaError::Config(format!("Failed to read config file {}: {}", path, e)))?;
let config: T = serde_json::from_str(&content)
.map_err(|e| crate::VezaError::Config(format!("Failed to parse config file {}: {}", path, e)))?;
config.validate()?;
info!("Configuration loaded from file: {}", path);
Ok(config)
}