nntp-proxy 0.5.0

High-performance NNTP proxy server with connection pooling and authentication
Documentation
//! Type-safe domain values for TUI

use std::fmt;
use std::num::NonZeroUsize;
use std::time::Instant;

/// Type-safe history size (non-zero)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HistorySize(NonZeroUsize);

impl HistorySize {
    /// Default history size: 60 points = 1 minute at 1 update/sec
    pub const DEFAULT: Self = Self(NonZeroUsize::new(60).unwrap());

    /// Create a new history size
    ///
    /// # Panics
    /// Panics if size is zero
    #[must_use]
    pub const fn new(size: usize) -> Self {
        match NonZeroUsize::new(size) {
            Some(non_zero) => Self(non_zero),
            None => panic!("HistorySize must be non-zero"),
        }
    }

    /// Get the raw value
    #[must_use]
    #[inline]
    pub const fn get(&self) -> usize {
        self.0.get()
    }
}

impl Default for HistorySize {
    fn default() -> Self {
        Self::DEFAULT
    }
}

/// Type-safe throughput in bytes per second (f64 for display formatting)
///
/// This is distinct from `metrics::BytesPerSecond` (u64) which is used for rate calculations.
/// This type includes display formatting methods for the TUI.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
pub struct Throughput(f64);

impl Throughput {
    /// Create from raw value
    #[must_use]
    #[inline]
    pub const fn new(bps: f64) -> Self {
        Self(bps)
    }

    /// Zero throughput
    #[must_use]
    #[inline]
    pub const fn zero() -> Self {
        Self(0.0)
    }

    /// Get raw value
    #[must_use]
    #[inline]
    pub const fn get(&self) -> f64 {
        self.0
    }

    /// Format as human-readable string
    #[must_use]
    pub fn format(&self) -> String {
        const KIB: f64 = 1_024.0;
        const MIB: f64 = KIB * 1_024.0;
        const GIB: f64 = MIB * 1_024.0;
        const TIB: f64 = GIB * 1_024.0;
        const PIB: f64 = TIB * 1_024.0;

        if self.0 >= PIB {
            format!("{:.2} PiB/s", self.0 / PIB)
        } else if self.0 >= TIB {
            format!("{:.2} TiB/s", self.0 / TIB)
        } else if self.0 >= GIB {
            format!("{:.2} GiB/s", self.0 / GIB)
        } else if self.0 >= MIB {
            format!("{:.2} MiB/s", self.0 / MIB)
        } else if self.0 >= KIB {
            format!("{:.2} KiB/s", self.0 / KIB)
        } else {
            format!("{:.0} B/s", self.0)
        }
    }
}

impl Default for Throughput {
    fn default() -> Self {
        Self::zero()
    }
}

impl fmt::Display for Throughput {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.format())
    }
}

impl From<f64> for Throughput {
    fn from(bps: f64) -> Self {
        Self::new(bps)
    }
}

/// Type-safe command rate in commands per second
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
pub struct CommandsPerSecond(f64);

impl CommandsPerSecond {
    /// Create from raw value
    #[must_use]
    #[inline]
    pub const fn new(cps: f64) -> Self {
        Self(cps)
    }

    /// Zero command rate
    #[must_use]
    #[inline]
    pub const fn zero() -> Self {
        Self(0.0)
    }

    /// Get raw value
    #[must_use]
    #[inline]
    pub const fn get(&self) -> f64 {
        self.0
    }
}

impl Default for CommandsPerSecond {
    fn default() -> Self {
        Self::zero()
    }
}

impl fmt::Display for CommandsPerSecond {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}", self.0)
    }
}

impl From<f64> for CommandsPerSecond {
    fn from(cps: f64) -> Self {
        Self::new(cps)
    }
}

/// Type-safe timestamp wrapper
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Timestamp(Instant);

impl Timestamp {
    /// Create from Instant
    #[must_use]
    #[inline]
    pub const fn new(instant: Instant) -> Self {
        Self(instant)
    }

    /// Current timestamp
    #[must_use]
    #[inline]
    pub fn now() -> Self {
        Self(Instant::now())
    }

    /// Get the inner Instant
    #[must_use]
    #[inline]
    pub const fn into_inner(self) -> Instant {
        self.0
    }

    /// Duration since this timestamp
    #[must_use]
    #[inline]
    pub fn elapsed(&self) -> std::time::Duration {
        self.0.elapsed()
    }

    /// Duration between two timestamps
    #[must_use]
    #[inline]
    pub fn duration_since(&self, earlier: Self) -> std::time::Duration {
        self.0.duration_since(earlier.0)
    }
}

impl From<Instant> for Timestamp {
    fn from(instant: Instant) -> Self {
        Self::new(instant)
    }
}

impl From<Timestamp> for Instant {
    fn from(ts: Timestamp) -> Self {
        ts.into_inner()
    }
}

#[cfg(test)]
#[allow(clippy::float_cmp)] // These newtype tests intentionally compare exact fixture values and zero constructors.
mod tests {
    use super::*;

    // HistorySize tests
    #[test]
    fn test_history_size() {
        let size = HistorySize::new(60);
        assert_eq!(size.get(), 60);
        assert_eq!(HistorySize::DEFAULT.get(), 60);
        assert_eq!(HistorySize::default().get(), 60);
    }

    #[test]
    #[should_panic(expected = "HistorySize must be non-zero")]
    fn test_history_size_zero_panics() {
        let _ = HistorySize::new(0);
    }

    #[test]
    fn test_history_size_custom_values() {
        assert_eq!(HistorySize::new(100).get(), 100);
        assert_eq!(HistorySize::new(1).get(), 1);
        assert_eq!(HistorySize::new(1000).get(), 1000);
    }

    // Throughput tests
    #[test]
    fn test_bytes_per_second() {
        let bps = Throughput::new(1_500_000.0);
        assert_eq!(bps.format(), "1.43 MiB/s");

        let bps2 = Throughput::new(2_500.0);
        assert_eq!(bps2.format(), "2.44 KiB/s");

        let bps3 = Throughput::new(500.0);
        assert_eq!(bps3.format(), "500 B/s");

        assert_eq!(Throughput::zero().get(), 0.0);
    }

    #[test]
    fn test_bytes_per_second_format_boundaries() {
        // Just over 1 MiB/s
        assert_eq!(Throughput::new(1_048_577.0).format(), "1.00 MiB/s");
        // Just under 1 MiB/s
        assert_eq!(Throughput::new(1_048_575.0).format(), "1024.00 KiB/s");
        // Just over 1 KiB/s
        assert_eq!(Throughput::new(1_025.0).format(), "1.00 KiB/s");
        // Just under 1 KiB/s
        assert_eq!(Throughput::new(999.0).format(), "999 B/s");
        // Zero
        assert_eq!(Throughput::zero().format(), "0 B/s");
    }

    #[test]
    fn test_bytes_per_second_default() {
        let bps = Throughput::default();
        assert_eq!(bps.get(), 0.0);
    }

    #[test]
    fn test_bytes_per_second_display() {
        assert_eq!(Throughput::new(1_500_000.0).to_string(), "1.43 MiB/s");
        assert_eq!(Throughput::new(2_500.0).to_string(), "2.44 KiB/s");
        assert_eq!(Throughput::new(500.0).to_string(), "500 B/s");
    }

    #[test]
    fn test_bytes_per_second_from_f64() {
        let bps = Throughput::from(1234.5);
        assert_eq!(bps.get(), 1234.5);
    }

    // CommandsPerSecond tests
    #[test]
    fn test_commands_per_second() {
        let cps = CommandsPerSecond::new(123.456);
        assert_eq!(cps.to_string(), "123.5");

        assert_eq!(CommandsPerSecond::zero().get(), 0.0);
    }

    #[test]
    fn test_commands_per_second_default() {
        let cps = CommandsPerSecond::default();
        assert_eq!(cps.get(), 0.0);
    }

    #[test]
    fn test_commands_per_second_display() {
        assert_eq!(CommandsPerSecond::new(0.0).to_string(), "0.0");
        assert_eq!(CommandsPerSecond::new(1.5).to_string(), "1.5");
        assert_eq!(CommandsPerSecond::new(99.99).to_string(), "100.0");
    }

    #[test]
    fn test_commands_per_second_from_f64() {
        let cps = CommandsPerSecond::from(42.7);
        assert_eq!(cps.get(), 42.7);
    }

    // Timestamp tests
    #[test]
    fn test_timestamp() {
        let ts = Timestamp::now();
        std::thread::sleep(std::time::Duration::from_millis(10));
        assert!(ts.elapsed() >= std::time::Duration::from_millis(10));
    }

    #[test]
    fn test_timestamp_duration_since() {
        let ts1 = Timestamp::now();
        std::thread::sleep(std::time::Duration::from_millis(10));
        let ts2 = Timestamp::now();

        let duration = ts2.duration_since(ts1);
        assert!(duration >= std::time::Duration::from_millis(10));
    }

    #[test]
    fn test_timestamp_from_instant() {
        let instant = Instant::now();
        let ts = Timestamp::from(instant);
        assert_eq!(ts.into_inner(), instant);
    }

    #[test]
    fn test_timestamp_into_instant() {
        let instant = Instant::now();
        let ts = Timestamp::new(instant);
        let instant2: Instant = ts.into();
        assert_eq!(instant, instant2);
    }
}