netpulse-cli 0.1.1

A zero-config, single-binary network quality monitor with percentile stats, jitter, and MTR-style traceroute
Documentation
// src/config.rs — Configuration management
//
// NetPulseConfig can be loaded from an optional `netpulse.toml` file.
// CLI flags ALWAYS take precedence over config file values.
// If no config file is present, sensible defaults are used.

use crate::error::NetPulseError;
use serde::{Deserialize, Serialize};
use std::path::Path;

/// Top-level configuration for netpulse.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetPulseConfig {
    /// Targets to monitor (IP addresses or host:port for TCP mode)
    #[serde(default)]
    pub targets: Vec<String>,

    /// How often to send probes per target (milliseconds)
    #[serde(default = "default_interval_ms")]
    pub interval_ms: u64,

    /// Probe timeout (milliseconds)
    #[serde(default = "default_timeout_ms")]
    pub timeout_ms: u64,

    /// Number of probe samples to keep in the ring buffer per target
    #[serde(default = "default_window_size")]
    pub window_size: usize,

    /// How often to emit a stats summary (in probe cycles)
    #[serde(default = "default_report_every")]
    pub report_every: u64,

    /// Probe type: "icmp" or "tcp"
    #[serde(default = "default_probe_type")]
    pub probe_type: ProbeType,

    /// Default TCP port for TCP probing
    #[serde(default = "default_tcp_port")]
    pub tcp_port: u16,
}

/// Which probe type to use.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ProbeType {
    Icmp,
    Tcp,
    Udp,
}

impl std::fmt::Display for ProbeType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ProbeType::Icmp => write!(f, "icmp"),
            ProbeType::Tcp => write!(f, "tcp"),
            ProbeType::Udp => write!(f, "udp"),
        }
    }
}

impl std::str::FromStr for ProbeType {
    type Err = NetPulseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "icmp" => Ok(ProbeType::Icmp),
            "tcp" => Ok(ProbeType::Tcp),
            "udp" => Ok(ProbeType::Udp),
            _ => Err(NetPulseError::ConfigError(format!(
                "unknown probe type '{}', must be 'icmp', 'tcp', or 'udp'",
                s
            ))),
        }
    }
}

fn default_interval_ms() -> u64 {
    1000
}
fn default_timeout_ms() -> u64 {
    3000
}
fn default_window_size() -> usize {
    300
} // 5 minutes at 1Hz
fn default_report_every() -> u64 {
    10
} // emit summary every 10 probes
fn default_probe_type() -> ProbeType {
    ProbeType::Icmp
}
fn default_tcp_port() -> u16 {
    80
}

impl Default for NetPulseConfig {
    fn default() -> Self {
        Self {
            targets: vec![],
            interval_ms: default_interval_ms(),
            timeout_ms: default_timeout_ms(),
            window_size: default_window_size(),
            report_every: default_report_every(),
            probe_type: default_probe_type(),
            tcp_port: default_tcp_port(),
        }
    }
}

impl NetPulseConfig {
    /// Load config from a TOML file, returning defaults if file doesn't exist.
    pub fn from_file(path: &Path) -> Result<Self, NetPulseError> {
        if !path.exists() {
            return Ok(Self::default());
        }

        let content = std::fs::read_to_string(path)
            .map_err(|e| NetPulseError::ConfigError(format!("cannot read config file: {}", e)))?;

        toml::from_str(&content)
            .map_err(|e| NetPulseError::ConfigError(format!("invalid TOML: {}", e)))
    }
}