use std::env;
use serde::de::DeserializeOwned;
#[derive(Debug)]
pub enum ConfigError {
Io(std::io::Error),
Parse(serde_yaml::Error),
Env(env::VarError),
Missing(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::Io(e) => write!(f, "IO error: {}", e),
ConfigError::Parse(e) => write!(f, "Parse error: {}", e),
ConfigError::Env(e) => write!(f, "Environment error: {}", e),
ConfigError::Missing(key) => write!(f, "Missing required config: {}", key),
}
}
}
impl std::error::Error for ConfigError {}
impl From<std::io::Error> for ConfigError {
fn from(e: std::io::Error) -> Self {
ConfigError::Io(e)
}
}
impl From<serde_yaml::Error> for ConfigError {
fn from(e: serde_yaml::Error) -> Self {
ConfigError::Parse(e)
}
}
impl From<env::VarError> for ConfigError {
fn from(e: env::VarError) -> Self {
ConfigError::Env(e)
}
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct AppConfig {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default)]
pub database: DatabaseConfig,
#[serde(default)]
pub redis: RedisConfig,
#[serde(default)]
pub logging: LoggingConfig,
#[serde(default)]
pub cors: CorsConfig,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
host: default_host(),
port: default_port(),
database: DatabaseConfig::default(),
redis: RedisConfig::default(),
logging: LoggingConfig::default(),
cors: CorsConfig::default(),
}
}
}
fn default_host() -> String {
"0.0.0.0".to_string()
}
fn default_port() -> u16 {
8080
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct DatabaseConfig {
#[serde(default = "default_database_url")]
pub url: String,
#[serde(default = "default_max_connections")]
pub max_connections: u32,
#[serde(default = "default_min_connections")]
pub min_connections: u32,
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
url: std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://localhost:5432/kegani".to_string()),
max_connections: 10,
min_connections: 2,
}
}
}
fn default_database_url() -> String {
"postgres://localhost:5432/kegani".to_string()
}
fn default_max_connections() -> u32 {
10
}
fn default_min_connections() -> u32 {
2
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct RedisConfig {
#[serde(default = "default_redis_url")]
pub url: String,
#[serde(default = "default_pool_size")]
pub pool_size: u32,
}
impl Default for RedisConfig {
fn default() -> Self {
Self {
url: std::env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://localhost:6379".to_string()),
pool_size: 5,
}
}
}
fn default_redis_url() -> String {
"redis://localhost:6379".to_string()
}
fn default_pool_size() -> u32 {
5
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct LoggingConfig {
#[serde(default = "default_log_level")]
pub level: String,
#[serde(default = "default_log_format")]
pub format: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: std::env::var("LOG_LEVEL")
.unwrap_or_else(|_| "info".to_string()),
format: "json".to_string(),
}
}
}
fn default_log_level() -> String {
"info".to_string()
}
fn default_log_format() -> String {
"json".to_string()
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct CorsConfig {
#[serde(default = "default_allowed_origins")]
pub allowed_origins: Vec<String>,
#[serde(default = "default_allowed_methods")]
pub allowed_methods: Vec<String>,
#[serde(default = "default_max_age")]
pub max_age: u64,
}
impl Default for CorsConfig {
fn default() -> Self {
Self {
allowed_origins: vec!["*".to_string()],
allowed_methods: vec![
"GET".to_string(),
"POST".to_string(),
"PUT".to_string(),
"DELETE".to_string(),
"PATCH".to_string(),
],
max_age: 3600,
}
}
}
fn default_allowed_origins() -> Vec<String> {
vec!["*".to_string()]
}
fn default_allowed_methods() -> Vec<String> {
vec![
"GET".to_string(),
"POST".to_string(),
"PUT".to_string(),
"DELETE".to_string(),
]
}
fn default_max_age() -> u64 {
3600
}
pub trait Settings: DeserializeOwned + Default {
fn load() -> Result<Self, ConfigError>
where
Self: Sized;
fn load_from_files(paths: Vec<&str>) -> Result<Self, ConfigError>
where
Self: Sized;
}
impl Settings for AppConfig {
fn load() -> Result<Self, ConfigError> {
Self::load_from_files(vec!["config.yaml", "config.yml"])
}
fn load_from_files(paths: Vec<&str>) -> Result<Self, ConfigError> {
let mut config = Self::default();
for path in paths {
if let Ok(contents) = std::fs::read_to_string(path) {
config = serde_yaml::from_str(&contents)?;
break;
}
}
if let Ok(host) = env::var("APP_HOST") {
config.host = host;
}
if let Ok(port) = env::var("APP_PORT") {
if let Ok(p) = port.parse() {
config.port = p;
}
}
if let Ok(url) = env::var("DATABASE_URL") {
config.database.url = url;
}
if let Ok(url) = env::var("REDIS_URL") {
config.redis.url = url;
}
if let Ok(level) = env::var("LOG_LEVEL") {
config.logging.level = level;
}
Ok(config)
}
}
pub trait SettingsExt: Settings + Sized {
fn reload(&mut self) -> Result<(), ConfigError>;
fn load_file(path: &str) -> Result<Self, ConfigError>;
}
impl SettingsExt for AppConfig {
fn reload(&mut self) -> Result<(), ConfigError> {
let new_config = Self::load()?;
*self = new_config;
Ok(())
}
fn load_file(path: &str) -> Result<Self, ConfigError> {
let mut config = Self::default();
if let Ok(contents) = std::fs::read_to_string(path) {
config = serde_yaml::from_str(&contents)?;
}
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = AppConfig::default();
assert_eq!(config.host, "0.0.0.0");
assert_eq!(config.port, 8080);
}
}