use config::{Config as ConfigCrate, Environment};
use serde::Deserialize;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
use url::Url;
use validator::Validate;
use crate::domain::{AcceptHeader, ApiVersion, NonEmptyString};
use crate::error::FatalError;
#[derive(Clone, Debug, Deserialize, Validate)]
pub struct Config {
#[validate(nested)]
pub server: ServerConfig,
#[validate(nested)]
pub database: DatabaseConfig,
#[validate(nested)]
pub github_api: GitHubApiConfig,
#[validate(nested)]
pub engine: EngineConfig,
#[validate(nested)]
pub auth: AuthConfig,
}
impl Config {
pub fn load() -> Result<Self, FatalError> {
if dotenvy::dotenv().is_ok() {
#[cfg(debug_assertions)]
tracing::info!("Successfully loaded local `.env` file.");
#[cfg(not(debug_assertions))]
tracing::warn!(
"Successfully loaded local `.env` file. \
If this is a production build, \
environment variables should be set prior to execution."
);
}
let environment = Environment::with_prefix("CBRIDGE")
.separator("__")
.try_parsing(true);
let config: Config = ConfigCrate::builder()
.add_source(environment)
.build()?
.try_deserialize()?;
config.validate()?;
if config.auth.allow_unauthenticated {
tracing::warn!(
"API AUTHENTICATION DISABLED. \
Please do not set `CBRIDGE__AUTH__ALLOW_UNAUTHENTICATED=true` \
on production environments."
);
}
Ok(config)
}
}
fn validate_duration_positive(duration: &Duration) -> Result<(), validator::ValidationError> {
if duration.is_zero() {
return Err(validator::ValidationError::new("duration_must_be_positive"));
}
Ok(())
}
#[derive(Clone, Debug, Deserialize, Validate)]
pub struct ServerConfig {
pub address: SocketAddr,
pub user_agent: NonEmptyString,
#[serde(with = "humantime_serde")]
#[validate(custom(function = "validate_duration_positive"))]
pub in_request_timeout: Duration,
#[serde(with = "humantime_serde")]
#[validate(custom(function = "validate_duration_positive"))]
pub out_request_timeout: Duration,
}
#[derive(Clone, Debug, Deserialize, Validate)]
#[validate(schema(function = "validate_pagination_limits"))]
pub struct DatabaseConfig {
pub url: Url,
#[serde(with = "humantime_serde")]
pub timeout: Duration,
#[validate(range(min = 1))]
pub polling_db_buffer_size: usize,
#[serde(with = "humantime_serde")]
pub polling_db_error_cooldown: Duration,
#[validate(range(min = 1))]
pub subscriptions_list_limit: usize,
#[validate(range(min = 1))]
pub subscriptions_list_limit_cap: usize,
}
fn validate_pagination_limits(config: &DatabaseConfig) -> Result<(), validator::ValidationError> {
if config.subscriptions_list_limit > config.subscriptions_list_limit_cap {
return Err(validator::ValidationError::new("limit_exceeds_cap"));
}
Ok(())
}
#[derive(Clone, Debug, Deserialize, Validate)]
pub struct GitHubApiConfig {
pub base_url: Url,
pub version: ApiVersion,
pub accept_header: AcceptHeader,
}
fn validate_engine_config(config: &EngineConfig) -> Result<(), validator::ValidationError> {
if config.stuck_task_threshold <= config.trigger_queue_polling_interval {
return Err(validator::ValidationError::new(
"stuck_task_threshold_too_low",
));
}
Ok(())
}
#[derive(Clone, Debug, Deserialize, Validate)]
#[validate(schema(function = "validate_engine_config"))]
pub struct EngineConfig {
#[serde(with = "humantime_serde")]
pub polling_sleep: Duration,
#[serde(with = "humantime_serde")]
pub trigger_queue_polling_interval: Duration,
#[validate(range(min = 1))]
pub trigger_retry_max_attempts: u32,
#[serde(with = "humantime_serde")]
pub trigger_retry_backoff_base: Duration,
#[serde(with = "humantime_serde")]
pub stuck_task_threshold: Duration,
}
fn validate_auth_config(config: &AuthConfig) -> Result<(), validator::ValidationError> {
if !config.allow_unauthenticated && config.api_key.is_none() {
return Err(validator::ValidationError::new(
"api_key_required_when_auth_enabled",
));
}
if config.token_validity <= config.clock_drift_buffer {
return Err(validator::ValidationError::new(
"token_validity_too_short_for_drift_buffer",
));
}
Ok(())
}
#[derive(Clone, Debug, Deserialize, Validate)]
#[validate(schema(function = "validate_auth_config"))]
pub struct AuthConfig {
#[serde(with = "humantime_serde")]
pub clock_drift_buffer: Duration,
#[serde(with = "humantime_serde")]
pub token_validity: Duration,
pub api_key: Option<NonEmptyString>,
#[serde(default)]
pub allow_unauthenticated: bool,
pub client_id: NonEmptyString,
pub pem_path: PathBuf,
}