nntp_proxy/config/
types.rs

1//! Configuration type definitions
2//!
3//! This module contains all the core configuration structures used by the proxy.
4
5use crate::types::{
6    CacheCapacity, HostName, MaxConnections, MaxErrors, Port, ServerName, duration_serde,
7    option_duration_serde,
8};
9use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12/// Routing mode for the proxy
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum RoutingMode {
16    /// Standard 1:1 mode - each client gets a dedicated backend connection
17    Standard,
18    /// Per-command routing - each command can use a different backend (stateless only)
19    PerCommand,
20    /// Hybrid mode - starts in per-command routing, auto-switches to stateful on first stateful command
21    Hybrid,
22}
23
24impl Default for RoutingMode {
25    /// Default routing mode is Hybrid, which provides optimal performance and full protocol support.
26    /// This mode automatically starts in per-command routing for efficiency and seamlessly switches
27    /// to stateful mode when commands requiring group context are detected.
28    fn default() -> Self {
29        Self::Hybrid
30    }
31}
32
33impl RoutingMode {
34    /// Check if this mode supports per-command routing
35    #[must_use]
36    pub fn supports_per_command_routing(&self) -> bool {
37        matches!(self, Self::PerCommand | Self::Hybrid)
38    }
39
40    /// Check if this mode can handle stateful commands
41    #[must_use]
42    pub fn supports_stateful_commands(&self) -> bool {
43        matches!(self, Self::Standard | Self::Hybrid)
44    }
45}
46
47/// Main proxy configuration
48#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
49pub struct Config {
50    /// List of backend NNTP servers
51    #[serde(default)]
52    pub servers: Vec<ServerConfig>,
53    /// Health check configuration
54    #[serde(default)]
55    pub health_check: HealthCheckConfig,
56    /// Cache configuration (optional, for caching proxy)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub cache: Option<CacheConfig>,
59}
60
61/// Cache configuration for article caching
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub struct CacheConfig {
64    /// Maximum number of articles to cache
65    #[serde(default = "super::defaults::cache_max_capacity")]
66    pub max_capacity: CacheCapacity,
67    /// Time-to-live for cached articles
68    #[serde(with = "duration_serde", default = "super::defaults::cache_ttl")]
69    pub ttl: Duration,
70}
71
72impl Default for CacheConfig {
73    fn default() -> Self {
74        Self {
75            max_capacity: super::defaults::cache_max_capacity(),
76            ttl: super::defaults::cache_ttl(),
77        }
78    }
79}
80
81/// Health check configuration
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct HealthCheckConfig {
84    /// Interval between health checks
85    #[serde(
86        with = "duration_serde",
87        default = "super::defaults::health_check_interval"
88    )]
89    pub interval: Duration,
90    /// Timeout for each health check
91    #[serde(
92        with = "duration_serde",
93        default = "super::defaults::health_check_timeout"
94    )]
95    pub timeout: Duration,
96    /// Number of consecutive failures before marking unhealthy
97    #[serde(default = "super::defaults::unhealthy_threshold")]
98    pub unhealthy_threshold: MaxErrors,
99}
100
101impl Default for HealthCheckConfig {
102    fn default() -> Self {
103        Self {
104            interval: super::defaults::health_check_interval(),
105            timeout: super::defaults::health_check_timeout(),
106            unhealthy_threshold: super::defaults::unhealthy_threshold(),
107        }
108    }
109}
110
111/// Configuration for a single backend server
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
113pub struct ServerConfig {
114    pub host: HostName,
115    pub port: Port,
116    pub name: ServerName,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub username: Option<String>,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub password: Option<String>,
121    /// Maximum number of concurrent connections to this server
122    #[serde(default = "super::defaults::max_connections")]
123    pub max_connections: MaxConnections,
124
125    /// Enable TLS/SSL for this backend connection
126    #[serde(default)]
127    pub use_tls: bool,
128    /// Verify TLS certificates (recommended for production)
129    #[serde(default = "super::defaults::tls_verify_cert")]
130    pub tls_verify_cert: bool,
131    /// Optional path to custom CA certificate
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub tls_cert_path: Option<String>,
134    /// Interval to send keep-alive commands (DATE) on idle connections
135    /// None disables keep-alive (default)
136    #[serde(
137        with = "option_duration_serde",
138        default,
139        skip_serializing_if = "Option::is_none"
140    )]
141    pub connection_keepalive: Option<Duration>,
142    /// Maximum number of connections to check per health check cycle
143    /// Lower values reduce pool contention but may take longer to detect all stale connections
144    #[serde(default = "super::defaults::health_check_max_per_cycle")]
145    pub health_check_max_per_cycle: usize,
146    /// Timeout when acquiring a connection for health checking
147    /// Short timeout prevents blocking if pool is busy
148    #[serde(
149        with = "duration_serde",
150        default = "super::defaults::health_check_pool_timeout"
151    )]
152    pub health_check_pool_timeout: Duration,
153}
154
155/// Builder for constructing `ServerConfig` instances
156///
157/// Provides a fluent API for creating server configurations, especially useful in tests
158/// where creating ServerConfig with all 11+ fields is verbose.
159///
160/// # Examples
161///
162/// ```
163/// use nntp_proxy::config::ServerConfig;
164///
165/// // Minimal configuration
166/// let config = ServerConfig::builder("news.example.com", 119)
167///     .build()
168///     .unwrap();
169///
170/// // With authentication and TLS
171/// let config = ServerConfig::builder("secure.example.com", 563)
172///     .name("Secure Server")
173///     .username("user")
174///     .password("pass")
175///     .max_connections(20)
176///     .use_tls(true)
177///     .build()
178///     .unwrap();
179/// ```
180pub struct ServerConfigBuilder {
181    host: String,
182    port: u16,
183    name: Option<String>,
184    username: Option<String>,
185    password: Option<String>,
186    max_connections: Option<usize>,
187    use_tls: bool,
188    tls_verify_cert: bool,
189    tls_cert_path: Option<String>,
190    connection_keepalive: Option<Duration>,
191    health_check_max_per_cycle: Option<usize>,
192    health_check_pool_timeout: Option<Duration>,
193}
194
195impl ServerConfigBuilder {
196    /// Create a new builder with required parameters
197    ///
198    /// # Arguments
199    /// * `host` - Backend server hostname or IP address
200    /// * `port` - Backend server port number
201    #[must_use]
202    pub fn new(host: impl Into<String>, port: u16) -> Self {
203        Self {
204            host: host.into(),
205            port,
206            name: None,
207            username: None,
208            password: None,
209            max_connections: None,
210            use_tls: false,
211            tls_verify_cert: true, // Secure by default
212            tls_cert_path: None,
213            connection_keepalive: None,
214            health_check_max_per_cycle: None,
215            health_check_pool_timeout: None,
216        }
217    }
218
219    /// Set a friendly name for logging (defaults to "host:port")
220    #[must_use]
221    pub fn name(mut self, name: impl Into<String>) -> Self {
222        self.name = Some(name.into());
223        self
224    }
225
226    /// Set authentication username
227    #[must_use]
228    pub fn username(mut self, username: impl Into<String>) -> Self {
229        self.username = Some(username.into());
230        self
231    }
232
233    /// Set authentication password
234    #[must_use]
235    pub fn password(mut self, password: impl Into<String>) -> Self {
236        self.password = Some(password.into());
237        self
238    }
239
240    /// Set maximum number of concurrent connections
241    #[must_use]
242    pub fn max_connections(mut self, max: usize) -> Self {
243        self.max_connections = Some(max);
244        self
245    }
246
247    /// Enable TLS/SSL for this backend connection
248    #[must_use]
249    pub fn use_tls(mut self, enabled: bool) -> Self {
250        self.use_tls = enabled;
251        self
252    }
253
254    /// Set whether to verify TLS certificates
255    #[must_use]
256    pub fn tls_verify_cert(mut self, verify: bool) -> Self {
257        self.tls_verify_cert = verify;
258        self
259    }
260
261    /// Set path to custom CA certificate
262    #[must_use]
263    pub fn tls_cert_path(mut self, path: impl Into<String>) -> Self {
264        self.tls_cert_path = Some(path.into());
265        self
266    }
267
268    /// Set keep-alive interval for idle connections
269    #[must_use]
270    pub fn connection_keepalive(mut self, interval: Duration) -> Self {
271        self.connection_keepalive = Some(interval);
272        self
273    }
274
275    /// Set maximum connections to check per health check cycle
276    #[must_use]
277    pub fn health_check_max_per_cycle(mut self, max: usize) -> Self {
278        self.health_check_max_per_cycle = Some(max);
279        self
280    }
281
282    /// Set timeout for acquiring connections during health checks
283    #[must_use]
284    pub fn health_check_pool_timeout(mut self, timeout: Duration) -> Self {
285        self.health_check_pool_timeout = Some(timeout);
286        self
287    }
288
289    /// Build the ServerConfig
290    ///
291    /// # Errors
292    ///
293    /// Returns an error if:
294    /// - Host is empty or invalid
295    /// - Port is 0
296    /// - Name is empty (when explicitly set)
297    /// - Max connections is 0 (when explicitly set)
298    pub fn build(self) -> Result<ServerConfig, anyhow::Error> {
299        use crate::types::{HostName, MaxConnections, Port, ServerName};
300
301        let host = HostName::new(self.host.clone())?;
302
303        let port = Port::new(self.port)
304            .ok_or_else(|| anyhow::anyhow!("Invalid port: {} (must be 1-65535)", self.port))?;
305
306        let name_str = self
307            .name
308            .unwrap_or_else(|| format!("{}:{}", self.host, self.port));
309        let name = ServerName::new(name_str)?;
310
311        let max_connections = if let Some(max) = self.max_connections {
312            MaxConnections::new(max)
313                .ok_or_else(|| anyhow::anyhow!("Invalid max_connections: {} (must be > 0)", max))?
314        } else {
315            super::defaults::max_connections()
316        };
317
318        let health_check_max_per_cycle = self
319            .health_check_max_per_cycle
320            .unwrap_or_else(super::defaults::health_check_max_per_cycle);
321
322        let health_check_pool_timeout = self
323            .health_check_pool_timeout
324            .unwrap_or_else(super::defaults::health_check_pool_timeout);
325
326        Ok(ServerConfig {
327            host,
328            port,
329            name,
330            username: self.username,
331            password: self.password,
332            max_connections,
333            use_tls: self.use_tls,
334            tls_verify_cert: self.tls_verify_cert,
335            tls_cert_path: self.tls_cert_path,
336            connection_keepalive: self.connection_keepalive,
337            health_check_max_per_cycle,
338            health_check_pool_timeout,
339        })
340    }
341}
342
343impl ServerConfig {
344    /// Create a builder for constructing a ServerConfig
345    ///
346    /// # Examples
347    ///
348    /// ```
349    /// use nntp_proxy::config::ServerConfig;
350    ///
351    /// let config = ServerConfig::builder("news.example.com", 119)
352    ///     .name("Example Server")
353    ///     .max_connections(15)
354    ///     .build()
355    ///     .unwrap();
356    /// ```
357    #[must_use]
358    pub fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
359        ServerConfigBuilder::new(host, port)
360    }
361}