use std::{env, net::SocketAddr, time::Duration}; #[derive(Clone, Debug, PartialEq)] pub struct AppConfig { pub bind_addr: SocketAddr, pub pokeapi_base_url: String, pub translations_base_url: String, pub request_timeout: Duration, pub rate_limit_per_second: f64, pub rate_limit_burst: u32, pub service_name: String, } impl AppConfig { #[must_use] pub fn default_local() -> Self { Self { bind_addr: default_bind_addr(), pokeapi_base_url: "https://pokeapi.co/api/v2".to_owned(), translations_base_url: "https://api.funtranslations.mercxry.me/v1".to_owned(), request_timeout: Duration::from_secs(5), rate_limit_per_second: 20.0, rate_limit_burst: 40, service_name: env!("CARGO_PKG_NAME").to_owned(), } } pub fn from_env() -> Result { let defaults = Self::default_local(); let rate_limit_per_second = env_or_parse("RATE_LIMIT_PER_SECOND", defaults.rate_limit_per_second)?; let rate_limit_burst = env_or_parse("RATE_LIMIT_BURST", defaults.rate_limit_burst)?; validate_rate_limit(rate_limit_per_second, rate_limit_burst)?; Ok(Self { bind_addr: env_or_parse("BIND_ADDR", defaults.bind_addr)?, pokeapi_base_url: env_or_string("POKEAPI_BASE_URL", &defaults.pokeapi_base_url), translations_base_url: env_or_string( "FUN_TRANSLATIONS_BASE_URL", &defaults.translations_base_url, ), request_timeout: Duration::from_secs(env_or_parse( "REQUEST_TIMEOUT_SECONDS", defaults.request_timeout.as_secs(), )?), rate_limit_per_second, rate_limit_burst, service_name: env_or_string("OTEL_SERVICE_NAME", &defaults.service_name), }) } } #[derive(Debug, thiserror::Error)] pub enum ConfigError { #[error("rate limit values must be positive")] InvalidRateLimit, #[error("invalid value for {name}: {source}")] InvalidValue { name: &'static str, source: Box, }, } fn env_or_string(name: &'static str, default: &str) -> String { env::var(name).unwrap_or_else(|_| default.to_owned()) } fn default_bind_addr() -> SocketAddr { SocketAddr::from(([0, 0, 0, 0], 8000)) } const fn validate_rate_limit(per_second: f64, burst: u32) -> Result<(), ConfigError> { if per_second.is_finite() && per_second > 0.0 && burst > 0 { Ok(()) } else { Err(ConfigError::InvalidRateLimit) } } fn env_or_parse(name: &'static str, default: T) -> Result where T: std::str::FromStr, T::Err: std::error::Error + Send + Sync + 'static, { env::var(name).map_or_else( |_| Ok(default), |value| { value.parse().map_err(|source| ConfigError::InvalidValue { name, source: Box::new(source), }) }, ) } #[cfg(test)] mod tests { use super::AppConfig; #[test] fn default_config_is_valid_for_local_development() { let config = AppConfig::default_local(); assert_eq!(config.bind_addr.port(), 8000); assert_eq!(config.pokeapi_base_url, "https://pokeapi.co/api/v2"); assert!((config.rate_limit_per_second - 20.0).abs() < f64::EPSILON); } }