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#[derive(Clone, Debug, Deserialize, Validate)]
14pub struct Config {
15 #[validate(nested)]
17 pub server: ServerConfig,
18
19 #[validate(nested)]
21 pub database: DatabaseConfig,
22
23 #[validate(nested)]
25 pub github_api: GitHubApiConfig,
26
27 #[validate(nested)]
29 pub engine: EngineConfig,
30
31 #[validate(nested)]
33 pub auth: AuthConfig,
34}
35
36impl Config {
37 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
74fn 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#[derive(Clone, Debug, Deserialize, Validate)]
84pub struct ServerConfig {
85 pub address: SocketAddr,
87
88 pub user_agent: NonEmptyString,
90
91 #[serde(with = "humantime_serde")]
93 #[validate(custom(function = "validate_duration_positive"))]
94 pub in_request_timeout: Duration,
95
96 #[serde(with = "humantime_serde")]
98 #[validate(custom(function = "validate_duration_positive"))]
99 pub out_request_timeout: Duration,
100}
101
102#[derive(Clone, Debug, Deserialize, Validate)]
104#[validate(schema(function = "validate_pagination_limits"))]
105pub struct DatabaseConfig {
106 pub url: Url,
108
109 #[serde(with = "humantime_serde")]
111 pub timeout: Duration,
112
113 #[validate(range(min = 1))]
115 pub polling_db_buffer_size: usize,
116
117 #[serde(with = "humantime_serde")]
119 pub polling_db_error_cooldown: Duration,
120
121 #[validate(range(min = 1))]
123 pub subscriptions_list_limit: usize,
124
125 #[validate(range(min = 1))]
127 pub subscriptions_list_limit_cap: usize,
128}
129
130fn 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#[derive(Clone, Debug, Deserialize, Validate)]
141pub struct GitHubApiConfig {
142 pub base_url: Url,
144
145 pub version: ApiVersion,
147
148 pub accept_header: AcceptHeader,
150}
151
152fn 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#[derive(Clone, Debug, Deserialize, Validate)]
164#[validate(schema(function = "validate_engine_config"))]
165pub struct EngineConfig {
166 #[serde(with = "humantime_serde")]
168 pub polling_sleep: Duration,
169
170 #[serde(with = "humantime_serde")]
172 pub trigger_queue_polling_interval: Duration,
173
174 #[validate(range(min = 1))]
176 pub trigger_retry_max_attempts: u32,
177
178 #[serde(with = "humantime_serde")]
180 pub trigger_retry_backoff_base: Duration,
181
182 #[serde(with = "humantime_serde")]
184 pub stuck_task_threshold: Duration,
185}
186
187fn 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#[derive(Clone, Debug, Deserialize, Validate)]
207#[validate(schema(function = "validate_auth_config"))]
208pub struct AuthConfig {
209 #[serde(with = "humantime_serde")]
211 pub clock_drift_buffer: Duration,
212
213 #[serde(with = "humantime_serde")]
215 pub token_validity: Duration,
216
217 pub api_key: Option<NonEmptyString>,
222
223 #[serde(default)]
225 pub allow_unauthenticated: bool,
226
227 pub client_id: NonEmptyString,
229
230 pub pem_path: PathBuf,
232}