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, clap::ValueEnum)]
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 const 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 const fn supports_stateful_commands(&self) -> bool {
43        matches!(self, Self::Standard | Self::Hybrid)
44    }
45
46    /// Get a human-readable description of this routing mode
47    #[must_use]
48    pub const fn as_str(&self) -> &'static str {
49        match self {
50            Self::Standard => "standard 1:1 mode",
51            Self::PerCommand => "per-command routing mode",
52            Self::Hybrid => "hybrid routing mode",
53        }
54    }
55}
56
57impl std::fmt::Display for RoutingMode {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        f.write_str(self.as_str())
60    }
61}
62
63/// Main proxy configuration
64#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
65pub struct Config {
66    /// List of backend NNTP servers
67    #[serde(default)]
68    pub servers: Vec<ServerConfig>,
69    /// Health check configuration
70    #[serde(default)]
71    pub health_check: HealthCheckConfig,
72    /// Cache configuration (optional, for caching proxy)
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub cache: Option<CacheConfig>,
75    /// Client authentication configuration
76    #[serde(default)]
77    pub client_auth: ClientAuthConfig,
78}
79
80/// Cache configuration for article caching
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
82pub struct CacheConfig {
83    /// Maximum number of articles to cache
84    #[serde(default = "super::defaults::cache_max_capacity")]
85    pub max_capacity: CacheCapacity,
86    /// Time-to-live for cached articles
87    #[serde(with = "duration_serde", default = "super::defaults::cache_ttl")]
88    pub ttl: Duration,
89}
90
91impl Default for CacheConfig {
92    fn default() -> Self {
93        Self {
94            max_capacity: super::defaults::cache_max_capacity(),
95            ttl: super::defaults::cache_ttl(),
96        }
97    }
98}
99
100/// Health check configuration
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102pub struct HealthCheckConfig {
103    /// Interval between health checks
104    #[serde(
105        with = "duration_serde",
106        default = "super::defaults::health_check_interval"
107    )]
108    pub interval: Duration,
109    /// Timeout for each health check
110    #[serde(
111        with = "duration_serde",
112        default = "super::defaults::health_check_timeout"
113    )]
114    pub timeout: Duration,
115    /// Number of consecutive failures before marking unhealthy
116    #[serde(default = "super::defaults::unhealthy_threshold")]
117    pub unhealthy_threshold: MaxErrors,
118}
119
120impl Default for HealthCheckConfig {
121    fn default() -> Self {
122        Self {
123            interval: super::defaults::health_check_interval(),
124            timeout: super::defaults::health_check_timeout(),
125            unhealthy_threshold: super::defaults::unhealthy_threshold(),
126        }
127    }
128}
129
130/// Client authentication configuration
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
132pub struct ClientAuthConfig {
133    /// Required username for client authentication (if set, auth is enabled)
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub username: Option<String>,
136    /// Required password for client authentication
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub password: Option<String>,
139    /// Optional custom greeting message
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub greeting: Option<String>,
142}
143
144impl ClientAuthConfig {
145    /// Check if authentication is enabled
146    pub fn is_enabled(&self) -> bool {
147        self.username.is_some() && self.password.is_some()
148    }
149}
150
151/// Configuration for a single backend server
152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
153pub struct ServerConfig {
154    pub host: HostName,
155    pub port: Port,
156    pub name: ServerName,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub username: Option<String>,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub password: Option<String>,
161    /// Maximum number of concurrent connections to this server
162    #[serde(default = "super::defaults::max_connections")]
163    pub max_connections: MaxConnections,
164
165    /// Enable TLS/SSL for this backend connection
166    #[serde(default)]
167    pub use_tls: bool,
168    /// Verify TLS certificates (recommended for production)
169    #[serde(default = "super::defaults::tls_verify_cert")]
170    pub tls_verify_cert: bool,
171    /// Optional path to custom CA certificate
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub tls_cert_path: Option<String>,
174    /// Interval to send keep-alive commands (DATE) on idle connections
175    /// None disables keep-alive (default)
176    #[serde(
177        with = "option_duration_serde",
178        default,
179        skip_serializing_if = "Option::is_none"
180    )]
181    pub connection_keepalive: Option<Duration>,
182    /// Maximum number of connections to check per health check cycle
183    /// Lower values reduce pool contention but may take longer to detect all stale connections
184    #[serde(default = "super::defaults::health_check_max_per_cycle")]
185    pub health_check_max_per_cycle: usize,
186    /// Timeout when acquiring a connection for health checking
187    /// Short timeout prevents blocking if pool is busy
188    #[serde(
189        with = "duration_serde",
190        default = "super::defaults::health_check_pool_timeout"
191    )]
192    pub health_check_pool_timeout: Duration,
193}
194
195/// Builder for constructing `ServerConfig` instances
196///
197/// Provides a fluent API for creating server configurations, especially useful in tests
198/// where creating ServerConfig with all 11+ fields is verbose.
199///
200/// # Examples
201///
202/// ```
203/// use nntp_proxy::config::ServerConfig;
204///
205/// // Minimal configuration
206/// let config = ServerConfig::builder("news.example.com", 119)
207///     .build()
208///     .unwrap();
209///
210/// // With authentication and TLS
211/// let config = ServerConfig::builder("secure.example.com", 563)
212///     .name("Secure Server")
213///     .username("user")
214///     .password("pass")
215///     .max_connections(20)
216///     .use_tls(true)
217///     .build()
218///     .unwrap();
219/// ```
220pub struct ServerConfigBuilder {
221    host: String,
222    port: u16,
223    name: Option<String>,
224    username: Option<String>,
225    password: Option<String>,
226    max_connections: Option<usize>,
227    use_tls: bool,
228    tls_verify_cert: bool,
229    tls_cert_path: Option<String>,
230    connection_keepalive: Option<Duration>,
231    health_check_max_per_cycle: Option<usize>,
232    health_check_pool_timeout: Option<Duration>,
233}
234
235impl ServerConfigBuilder {
236    /// Create a new builder with required parameters
237    ///
238    /// # Arguments
239    /// * `host` - Backend server hostname or IP address
240    /// * `port` - Backend server port number
241    #[must_use]
242    pub fn new(host: impl Into<String>, port: u16) -> Self {
243        Self {
244            host: host.into(),
245            port,
246            name: None,
247            username: None,
248            password: None,
249            max_connections: None,
250            use_tls: false,
251            tls_verify_cert: true, // Secure by default
252            tls_cert_path: None,
253            connection_keepalive: None,
254            health_check_max_per_cycle: None,
255            health_check_pool_timeout: None,
256        }
257    }
258
259    /// Set a friendly name for logging (defaults to "host:port")
260    #[must_use]
261    pub fn name(mut self, name: impl Into<String>) -> Self {
262        self.name = Some(name.into());
263        self
264    }
265
266    /// Set authentication username
267    #[must_use]
268    pub fn username(mut self, username: impl Into<String>) -> Self {
269        self.username = Some(username.into());
270        self
271    }
272
273    /// Set authentication password
274    #[must_use]
275    pub fn password(mut self, password: impl Into<String>) -> Self {
276        self.password = Some(password.into());
277        self
278    }
279
280    /// Set maximum number of concurrent connections
281    #[must_use]
282    pub fn max_connections(mut self, max: usize) -> Self {
283        self.max_connections = Some(max);
284        self
285    }
286
287    /// Enable TLS/SSL for this backend connection
288    #[must_use]
289    pub fn use_tls(mut self, enabled: bool) -> Self {
290        self.use_tls = enabled;
291        self
292    }
293
294    /// Set whether to verify TLS certificates
295    #[must_use]
296    pub fn tls_verify_cert(mut self, verify: bool) -> Self {
297        self.tls_verify_cert = verify;
298        self
299    }
300
301    /// Set path to custom CA certificate
302    #[must_use]
303    pub fn tls_cert_path(mut self, path: impl Into<String>) -> Self {
304        self.tls_cert_path = Some(path.into());
305        self
306    }
307
308    /// Set keep-alive interval for idle connections
309    #[must_use]
310    pub fn connection_keepalive(mut self, interval: Duration) -> Self {
311        self.connection_keepalive = Some(interval);
312        self
313    }
314
315    /// Set maximum connections to check per health check cycle
316    #[must_use]
317    pub fn health_check_max_per_cycle(mut self, max: usize) -> Self {
318        self.health_check_max_per_cycle = Some(max);
319        self
320    }
321
322    /// Set timeout for acquiring connections during health checks
323    #[must_use]
324    pub fn health_check_pool_timeout(mut self, timeout: Duration) -> Self {
325        self.health_check_pool_timeout = Some(timeout);
326        self
327    }
328
329    /// Build the ServerConfig
330    ///
331    /// # Errors
332    ///
333    /// Returns an error if:
334    /// - Host is empty or invalid
335    /// - Port is 0
336    /// - Name is empty (when explicitly set)
337    /// - Max connections is 0 (when explicitly set)
338    pub fn build(self) -> Result<ServerConfig, anyhow::Error> {
339        use crate::types::{HostName, MaxConnections, Port, ServerName};
340
341        let host = HostName::new(self.host.clone())?;
342
343        let port = Port::new(self.port)
344            .ok_or_else(|| anyhow::anyhow!("Invalid port: {} (must be 1-65535)", self.port))?;
345
346        let name_str = self
347            .name
348            .unwrap_or_else(|| format!("{}:{}", self.host, self.port));
349        let name = ServerName::new(name_str)?;
350
351        let max_connections = if let Some(max) = self.max_connections {
352            MaxConnections::new(max)
353                .ok_or_else(|| anyhow::anyhow!("Invalid max_connections: {} (must be > 0)", max))?
354        } else {
355            super::defaults::max_connections()
356        };
357
358        let health_check_max_per_cycle = self
359            .health_check_max_per_cycle
360            .unwrap_or_else(super::defaults::health_check_max_per_cycle);
361
362        let health_check_pool_timeout = self
363            .health_check_pool_timeout
364            .unwrap_or_else(super::defaults::health_check_pool_timeout);
365
366        Ok(ServerConfig {
367            host,
368            port,
369            name,
370            username: self.username,
371            password: self.password,
372            max_connections,
373            use_tls: self.use_tls,
374            tls_verify_cert: self.tls_verify_cert,
375            tls_cert_path: self.tls_cert_path,
376            connection_keepalive: self.connection_keepalive,
377            health_check_max_per_cycle,
378            health_check_pool_timeout,
379        })
380    }
381}
382
383impl ServerConfig {
384    /// Create a builder for constructing a ServerConfig
385    ///
386    /// # Examples
387    ///
388    /// ```
389    /// use nntp_proxy::config::ServerConfig;
390    ///
391    /// let config = ServerConfig::builder("news.example.com", 119)
392    ///     .name("Example Server")
393    ///     .max_connections(15)
394    ///     .build()
395    ///     .unwrap();
396    /// ```
397    #[must_use]
398    pub fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
399        ServerConfigBuilder::new(host, port)
400    }
401}