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