109 lines
3.4 KiB
Rust
109 lines
3.4 KiB
Rust
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<Self, ConfigError> {
|
|
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<dyn std::error::Error + Send + Sync>,
|
|
},
|
|
}
|
|
|
|
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<T>(name: &'static str, default: T) -> Result<T, ConfigError>
|
|
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);
|
|
}
|
|
}
|