Skip to main content

nntp_proxy/
types.rs

1//! Core types for request tracking and identification
2//!
3//! This module provides unique identifiers used throughout the proxy.
4
5pub mod config;
6pub mod metrics;
7pub mod metrics_recording;
8pub mod network;
9pub mod pool;
10pub mod protocol;
11pub mod tui;
12pub mod validated;
13
14pub use config::{
15    BackendReadTimeout, BufferSize, CacheCapacity, CommandExecutionTimeout, ConnectionTimeout,
16    HealthCheckTimeout, MaxConnections, MaxErrors, Port, ThreadCount, WindowSize, duration_serde,
17    option_duration_serde,
18};
19pub use metrics::{
20    ArticleBytesTotal, BackendToClientBytes, BytesPerSecondRate, BytesReceived, BytesSent,
21    ClientBytes, ClientToBackendBytes, TimingMeasurementCount, TotalConnections, TransferMetrics,
22};
23pub use metrics_recording::{
24    DirectionalBytes, MetricsBytes, Recorded, RecordingState, TransferDirection, Unrecorded,
25};
26pub use network::ClientAddress;
27pub use pool::{
28    AvailableConnections, CreatedConnections, InUseConnections, MaxPoolSize, PoolUtilization,
29};
30pub use protocol::MessageId;
31pub use validated::{ConfigPath, HostName, Password, ServerName, Username, ValidationError};
32
33use serde::{Deserialize, Deserializer, Serialize};
34use std::fmt;
35use uuid::Uuid;
36
37/// Unique identifier for a client connection
38///
39/// Uses `UUIDv4` for guaranteed uniqueness across sessions.
40/// Useful for request tracing and debugging.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
42pub struct ClientId(Uuid);
43
44impl ClientId {
45    /// Generate a new random client ID
46    #[inline]
47    #[must_use]
48    pub fn new() -> Self {
49        Self(Uuid::new_v4())
50    }
51
52    /// Get the underlying UUID
53    #[inline]
54    #[must_use]
55    pub const fn as_uuid(&self) -> &Uuid {
56        &self.0
57    }
58}
59
60impl Default for ClientId {
61    #[inline]
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl fmt::Display for ClientId {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "{}", self.0)
70    }
71}
72
73/// Unique identifier for a backend server.
74///
75/// Backend IDs are bounded by the availability bitmap width. Constructing a
76/// backend outside that range is not a valid internal state.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
78pub struct BackendId(usize);
79
80impl BackendId {
81    pub const MAX_COUNT: usize = usize::BITS as usize;
82
83    /// Create a backend ID from an index
84    #[inline]
85    #[must_use]
86    pub fn from_index(index: usize) -> Self {
87        Self::try_from_index(index).unwrap_or_else(|| {
88            panic!(
89                "Backend index {index} exceeds maximum backend count ({})",
90                Self::MAX_COUNT
91            )
92        })
93    }
94
95    #[inline]
96    #[must_use]
97    pub const fn try_from_index(index: usize) -> Option<Self> {
98        if index < Self::MAX_COUNT {
99            Some(Self(index))
100        } else {
101            None
102        }
103    }
104
105    /// Get the backend index
106    #[inline]
107    #[must_use]
108    pub const fn as_index(&self) -> usize {
109        self.0
110    }
111
112    /// Get this backend's bit within the availability bitset.
113    #[inline]
114    #[must_use]
115    pub(crate) fn availability_bit(self) -> usize {
116        1usize << self.as_index()
117    }
118}
119
120impl fmt::Display for BackendId {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        write!(f, "Backend({})", self.0)
123    }
124}
125
126impl<'de> Deserialize<'de> for BackendId {
127    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
128    where
129        D: Deserializer<'de>,
130    {
131        let index = usize::deserialize(deserializer)?;
132        Self::try_from_index(index).ok_or_else(|| {
133            serde::de::Error::custom(format!(
134                "backend index {index} exceeds maximum backend count ({})",
135                Self::MAX_COUNT
136            ))
137        })
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    // ClientId tests
146    #[test]
147    fn test_client_id_unique() {
148        let id1 = ClientId::new();
149        let id2 = ClientId::new();
150        assert_ne!(id1, id2);
151    }
152
153    #[test]
154    fn test_client_id_default() {
155        let id1 = ClientId::default();
156        let id2 = ClientId::default();
157        assert_ne!(id1, id2); // Each default() creates unique ID
158    }
159
160    #[test]
161    fn test_client_id_as_uuid() {
162        let id = ClientId::new();
163        let uuid = id.as_uuid();
164        assert_eq!(uuid.get_version(), Some(uuid::Version::Random));
165    }
166
167    #[test]
168    fn test_client_id_display() {
169        let id = ClientId::new();
170        let display = format!("{id}");
171        assert!(!display.is_empty());
172        // UUID format: 8-4-4-4-12 hex characters
173        assert_eq!(display.len(), 36);
174        assert_eq!(display.chars().filter(|&c| c == '-').count(), 4);
175    }
176
177    #[test]
178    fn test_client_id_debug() {
179        let id = ClientId::new();
180        let debug = format!("{id:?}");
181        assert!(debug.contains("ClientId"));
182    }
183
184    #[test]
185    fn test_client_id_clone() {
186        let id1 = ClientId::new();
187        let id2 = id1;
188        assert_eq!(id1, id2);
189    }
190
191    #[test]
192    fn test_client_id_equality() {
193        let id1 = ClientId::new();
194        let id2 = id1;
195        let id3 = ClientId::new();
196
197        assert_eq!(id1, id2);
198        assert_ne!(id1, id3);
199    }
200
201    #[test]
202    fn test_client_id_hash() {
203        use std::collections::HashSet;
204
205        let id1 = ClientId::new();
206        let id2 = id1;
207        let id3 = ClientId::new();
208
209        let mut set = HashSet::new();
210        set.insert(id1);
211        set.insert(id2); // Duplicate, should not increase size
212        set.insert(id3);
213
214        assert_eq!(set.len(), 2);
215    }
216
217    #[test]
218    fn test_client_id_ordering() {
219        let id1 = ClientId::new();
220        let id2 = ClientId::new();
221
222        // IDs are UUIDs - just verify ordering trait is implemented
223        // Actual ordering doesn't matter, just that comparison works
224        let _ = id1 < id2;
225        let _ = id1 > id2;
226
227        let id3 = id1;
228        assert_eq!(id1, id3);
229    }
230
231    // BackendId tests
232    #[test]
233    fn test_backend_id() {
234        let id1 = BackendId::from_index(0);
235        let id2 = BackendId::from_index(1);
236        assert_ne!(id1, id2);
237        assert_eq!(id1.as_index(), 0);
238        assert_eq!(id2.as_index(), 1);
239    }
240
241    #[test]
242    fn test_backend_id_const_fn() {
243        let id = BackendId::from_index(10);
244        assert_eq!(id.as_index(), 10);
245    }
246
247    #[test]
248    fn test_backend_id_display() {
249        let id = BackendId::from_index(5);
250        assert_eq!(format!("{id}"), "Backend(5)");
251    }
252
253    #[test]
254    fn test_backend_id_availability_bit() {
255        assert_eq!(BackendId::from_index(0).availability_bit(), 0b0000_0001);
256        assert_eq!(BackendId::from_index(7).availability_bit(), 0b1000_0000);
257        assert_eq!(BackendId::from_index(8).availability_bit(), 0b1_0000_0000);
258    }
259
260    #[test]
261    fn test_backend_id_rejects_index_outside_bitset() {
262        assert!(BackendId::try_from_index(usize::BITS as usize).is_none());
263    }
264
265    #[test]
266    fn test_backend_id_debug() {
267        let id = BackendId::from_index(7);
268        let debug = format!("{id:?}");
269        assert!(debug.contains("BackendId"));
270        assert!(debug.contains('7'));
271    }
272
273    #[test]
274    fn test_backend_id_clone() {
275        let id1 = BackendId::from_index(15);
276        let id2 = id1;
277        assert_eq!(id1, id2);
278    }
279
280    #[test]
281    fn test_backend_id_equality() {
282        let id1 = BackendId::from_index(10);
283        let id2 = BackendId::from_index(10);
284        let id3 = BackendId::from_index(20);
285
286        assert_eq!(id1, id2);
287        assert_ne!(id1, id3);
288    }
289
290    #[test]
291    fn test_backend_id_hash() {
292        use std::collections::HashSet;
293
294        let id1 = BackendId::from_index(1);
295        let id2 = BackendId::from_index(1);
296        let id3 = BackendId::from_index(2);
297
298        let mut set = HashSet::new();
299        set.insert(id1);
300        set.insert(id2); // Duplicate
301        set.insert(id3);
302
303        assert_eq!(set.len(), 2);
304    }
305
306    #[test]
307    fn test_backend_id_ordering() {
308        let id1 = BackendId::from_index(1);
309        let id2 = BackendId::from_index(2);
310        let id3 = BackendId::from_index(3);
311
312        assert!(id1 < id2);
313        assert!(id2 < id3);
314        assert!(id1 < id3);
315        assert!(id2 > id1);
316    }
317
318    #[test]
319    fn test_backend_id_zero() {
320        let id = BackendId::from_index(0);
321        assert_eq!(id.as_index(), 0);
322    }
323
324    #[test]
325    #[should_panic(expected = "exceeds maximum backend count")]
326    fn test_backend_id_from_index_panics_for_out_of_range_index() {
327        let _ = BackendId::from_index(usize::MAX);
328    }
329
330    #[test]
331    fn test_display() {
332        let client_id = ClientId::new();
333        let backend_id = BackendId::from_index(5);
334
335        assert!(!format!("{client_id}").is_empty());
336        assert_eq!(format!("{backend_id}"), "Backend(5)");
337    }
338}