Skip to main content

commit_bridge/
config.rs

1use config::{Config as ConfigCrate, Environment};
2use serde::Deserialize;
3use std::net::SocketAddr;
4use std::path::PathBuf;
5use std::time::Duration;
6use url::Url;
7use validator::Validate;
8
9use crate::domain::{AcceptHeader, ApiVersion, NonEmptyString};
10use crate::error::FatalError;
11
12/// Holds all application-wide configuration settings.
13#[derive(Clone, Debug, Deserialize, Validate)]
14pub struct Config {
15    /// Server-related configuration settings.
16    #[validate(nested)]
17    pub server: ServerConfig,
18
19    /// Database-related configuration settings.
20    #[validate(nested)]
21    pub database: DatabaseConfig,
22
23    /// GitHub API communication configuration settings.
24    #[validate(nested)]
25    pub github_api: GitHubApiConfig,
26
27    /// Asynchronous engine configuration settings.
28    #[validate(nested)]
29    pub engine: EngineConfig,
30
31    /// Authentication configuration settings.
32    #[validate(nested)]
33    pub auth: AuthConfig,
34}
35
36impl Config {
37    /// Bootstraps the application configuration from the environment.
38    pub fn load() -> Result<Self, FatalError> {
39        if dotenvy::dotenv().is_ok() {
40            #[cfg(debug_assertions)]
41            tracing::info!("Successfully loaded local `.env` file.");
42
43            #[cfg(not(debug_assertions))]
44            tracing::warn!(
45                "Successfully loaded local `.env` file. \
46                If this is a production build, \
47                environment variables should be set prior to execution."
48            );
49        }
50
51        let environment = Environment::with_prefix("CBRIDGE")
52            .separator("__")
53            .try_parsing(true);
54
55        let config: Config = ConfigCrate::builder()
56            .add_source(environment)
57            .build()?
58            .try_deserialize()?;
59
60        config.validate()?;
61
62        if config.auth.allow_unauthenticated {
63            tracing::warn!(
64                "API AUTHENTICATION DISABLED. \
65                Please do not set `CBRIDGE__AUTH__ALLOW_UNAUTHENTICATED=true` \
66                on production environments."
67            );
68        }
69
70        Ok(config)
71    }
72}
73
74/// Validates that a duration is positive.
75fn validate_duration_positive(duration: &Duration) -> Result<(), validator::ValidationError> {
76    if duration.is_zero() {
77        return Err(validator::ValidationError::new("duration_must_be_positive"));
78    }
79    Ok(())
80}
81
82/// Configuration for the HTTP server.
83#[derive(Clone, Debug, Deserialize, Validate)]
84pub struct ServerConfig {
85    /// The bind address for the server.
86    pub address: SocketAddr,
87
88    /// The User-Agent string for HTTP requests.
89    pub user_agent: NonEmptyString,
90
91    /// Timeout duration for incoming HTTP requests.
92    #[serde(with = "humantime_serde")]
93    #[validate(custom(function = "validate_duration_positive"))]
94    pub in_request_timeout: Duration,
95
96    /// Timeout duration for outgoing HTTP requests.
97    #[serde(with = "humantime_serde")]
98    #[validate(custom(function = "validate_duration_positive"))]
99    pub out_request_timeout: Duration,
100}
101
102/// Configuration for the database connection.
103#[derive(Clone, Debug, Deserialize, Validate)]
104#[validate(schema(function = "validate_pagination_limits"))]
105pub struct DatabaseConfig {
106    /// The connection URL for the database.
107    pub url: Url,
108
109    /// The database connection timeout duration.
110    #[serde(with = "humantime_serde")]
111    pub timeout: Duration,
112
113    /// The maximum size of the polling database buffer.
114    #[validate(range(min = 1))]
115    pub polling_db_buffer_size: usize,
116
117    /// The cooldown duration for polling database errors.
118    #[serde(with = "humantime_serde")]
119    pub polling_db_error_cooldown: Duration,
120
121    /// The default limit for subscriptions list pagination.
122    #[validate(range(min = 1))]
123    pub subscriptions_list_limit: usize,
124
125    /// The maximum allowed limit for subscriptions list pagination.
126    #[validate(range(min = 1))]
127    pub subscriptions_list_limit_cap: usize,
128}
129
130/// Validates that the default pagination limit (`subscriptions_list_limit`)
131/// does not exceed the maximum allowed cap (`subscriptions_list_limit_cap`).
132fn validate_pagination_limits(config: &DatabaseConfig) -> Result<(), validator::ValidationError> {
133    if config.subscriptions_list_limit > config.subscriptions_list_limit_cap {
134        return Err(validator::ValidationError::new("limit_exceeds_cap"));
135    }
136    Ok(())
137}
138
139/// Configuration for GitHub API interactions.
140#[derive(Clone, Debug, Deserialize, Validate)]
141pub struct GitHubApiConfig {
142    /// The base URL for the GitHub API.
143    pub base_url: Url,
144
145    /// The specific GitHub API version to use.
146    pub version: ApiVersion,
147
148    /// The value of the Accept header.
149    pub accept_header: AcceptHeader,
150}
151
152/// Validates that the stuck task threshold is greater than the polling interval.
153fn validate_engine_config(config: &EngineConfig) -> Result<(), validator::ValidationError> {
154    if config.stuck_task_threshold <= config.trigger_queue_polling_interval {
155        return Err(validator::ValidationError::new(
156            "stuck_task_threshold_too_low",
157        ));
158    }
159    Ok(())
160}
161
162/// Configuration for internal engine processes.
163#[derive(Clone, Debug, Deserialize, Validate)]
164#[validate(schema(function = "validate_engine_config"))]
165pub struct EngineConfig {
166    /// Duration to sleep between polling cycles.
167    #[serde(with = "humantime_serde")]
168    pub polling_sleep: Duration,
169
170    /// The interval for polling the trigger queue.
171    #[serde(with = "humantime_serde")]
172    pub trigger_queue_polling_interval: Duration,
173
174    /// The maximum number of trigger retry attempts.
175    #[validate(range(min = 1))]
176    pub trigger_retry_max_attempts: u32,
177
178    /// The base duration for retry backoff.
179    #[serde(with = "humantime_serde")]
180    pub trigger_retry_backoff_base: Duration,
181
182    /// The duration before a task is considered stuck.
183    #[serde(with = "humantime_serde")]
184    pub stuck_task_threshold: Duration,
185}
186
187/// Validates that an API key is provided if unauthenticated access is disabled,
188/// and that token validity exceeds the clock drift buffer.
189fn validate_auth_config(config: &AuthConfig) -> Result<(), validator::ValidationError> {
190    if !config.allow_unauthenticated && config.api_key.is_none() {
191        return Err(validator::ValidationError::new(
192            "api_key_required_when_auth_enabled",
193        ));
194    }
195
196    if config.token_validity <= config.clock_drift_buffer {
197        return Err(validator::ValidationError::new(
198            "token_validity_too_short_for_drift_buffer",
199        ));
200    }
201
202    Ok(())
203}
204
205/// Configuration for authentication mechanisms.
206#[derive(Clone, Debug, Deserialize, Validate)]
207#[validate(schema(function = "validate_auth_config"))]
208pub struct AuthConfig {
209    /// Buffer time allowed for clock drift.
210    #[serde(with = "humantime_serde")]
211    pub clock_drift_buffer: Duration,
212
213    /// Duration for which an authentication token is valid.
214    #[serde(with = "humantime_serde")]
215    pub token_validity: Duration,
216
217    /// Optional API key for authentication.
218    ///
219    /// If set, the `X-API-KEY` header must be present
220    /// and match this value on sensible requests.
221    pub api_key: Option<NonEmptyString>,
222
223    /// Allow unauthenticated access to the API.
224    #[serde(default)]
225    pub allow_unauthenticated: bool,
226
227    /// GitHub App's Client ID.
228    pub client_id: NonEmptyString,
229
230    /// Path to the GitHub App's private key.
231    pub pem_path: PathBuf,
232}