Skip to main content

netspeed_cli/
types.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Deserialize, Serialize)]
4pub struct Server {
5    #[serde(rename = "@id")]
6    pub id: String,
7    #[serde(rename = "@url")]
8    pub url: String,
9    #[serde(rename = "@name")]
10    pub name: String,
11    #[serde(rename = "@sponsor")]
12    pub sponsor: String,
13    #[serde(rename = "@country")]
14    pub country: String,
15    #[serde(rename = "@lat")]
16    pub lat: f64,
17    #[serde(rename = "@lon")]
18    pub lon: f64,
19    #[serde(skip)]
20    pub distance: f64,
21}
22
23#[derive(Debug, Clone, Serialize)]
24pub struct TestResult {
25    pub server: ServerInfo,
26    pub ping: Option<f64>,
27    pub jitter: Option<f64>,
28    pub packet_loss: Option<f64>,
29    pub download: Option<f64>,
30    pub download_peak: Option<f64>,
31    pub upload: Option<f64>,
32    pub upload_peak: Option<f64>,
33    pub latency_download: Option<f64>,
34    pub latency_upload: Option<f64>,
35    pub download_samples: Option<Vec<f64>>,
36    pub upload_samples: Option<Vec<f64>>,
37    pub ping_samples: Option<Vec<f64>>,
38    pub timestamp: String,
39    pub client_ip: Option<String>,
40}
41
42impl TestResult {
43    /// Build a `TestResult` from ping test output and download/upload test runs.
44    #[allow(clippy::too_many_arguments)]
45    #[must_use]
46    pub fn from_test_runs(
47        server: ServerInfo,
48        ping: Option<f64>,
49        jitter: Option<f64>,
50        packet_loss: Option<f64>,
51        ping_samples: Vec<f64>,
52        dl: &crate::test_runner::TestRunResult,
53        ul: &crate::test_runner::TestRunResult,
54        client_ip: Option<String>,
55    ) -> Self {
56        fn opt_samples(v: &[f64]) -> Option<Vec<f64>> {
57            if v.is_empty() { None } else { Some(v.to_vec()) }
58        }
59        fn opt_positive(v: f64) -> Option<f64> {
60            if v > 0.0 { Some(v) } else { None }
61        }
62
63        Self {
64            server,
65            ping,
66            jitter,
67            packet_loss,
68            download: opt_positive(dl.avg_bps),
69            download_peak: opt_positive(dl.peak_bps),
70            upload: opt_positive(ul.avg_bps),
71            upload_peak: opt_positive(ul.peak_bps),
72            latency_download: dl.latency_under_load,
73            latency_upload: ul.latency_under_load,
74            download_samples: opt_samples(&dl.speed_samples),
75            upload_samples: opt_samples(&ul.speed_samples),
76            ping_samples: opt_samples(&ping_samples),
77            timestamp: chrono::Utc::now().to_rfc3339(),
78            client_ip,
79        }
80    }
81}
82
83#[derive(Debug, Clone, Serialize)]
84pub struct ServerInfo {
85    pub id: String,
86    pub name: String,
87    pub sponsor: String,
88    pub country: String,
89    pub distance: f64,
90}
91
92#[derive(Debug, Clone, Serialize)]
93pub struct CsvOutput {
94    pub server_id: String,
95    pub sponsor: String,
96    pub server_name: String,
97    pub timestamp: String,
98    pub distance: f64,
99    pub ping: f64,
100    pub jitter: f64,
101    pub packet_loss: f64,
102    pub download: f64,
103    pub download_peak: f64,
104    pub upload: f64,
105    pub upload_peak: f64,
106    pub ip_address: String,
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_server_serialization() {
115        let server = Server {
116            id: "1234".to_string(),
117            url: "http://example.com".to_string(),
118            name: "Test Server".to_string(),
119            sponsor: "Test ISP".to_string(),
120            country: "US".to_string(),
121            lat: 40.7128,
122            lon: -74.0060,
123            distance: 100.5,
124        };
125
126        let json = serde_json::to_string(&server).unwrap();
127        // With @ prefix for XML attributes, serde serializes them as normal fields in JSON
128        assert!(json.contains("\"1234\""));
129        assert!(json.contains("\"Test Server\""));
130    }
131
132    #[test]
133    fn test_test_result_serialization() {
134        let result = TestResult {
135            server: ServerInfo {
136                id: "1234".to_string(),
137                name: "Test Server".to_string(),
138                sponsor: "Test ISP".to_string(),
139                country: "US".to_string(),
140                distance: 100.5,
141            },
142            ping: Some(15.234),
143            jitter: Some(1.2),
144            packet_loss: Some(0.0),
145            download: Some(150_000_000.0),
146            download_peak: Some(180_000_000.0),
147            upload: Some(50_000_000.0),
148            upload_peak: Some(60_000_000.0),
149            latency_download: Some(25.0),
150            latency_upload: Some(30.0),
151            download_samples: None,
152            upload_samples: None,
153            ping_samples: None,
154            timestamp: "2026-04-04T12:00:00Z".to_string(),
155            client_ip: Some("192.168.1.1".to_string()),
156        };
157
158        let json = serde_json::to_string(&result).unwrap();
159        assert!(json.contains("\"ping\":15.234"));
160        assert!(json.contains("\"jitter\":1.2"));
161        assert!(json.contains("\"download\":150000000.0"));
162    }
163
164    #[test]
165    fn test_csv_output_serialization() {
166        let csv = CsvOutput {
167            server_id: "1234".to_string(),
168            sponsor: "Test ISP".to_string(),
169            server_name: "Test Server".to_string(),
170            timestamp: "2026-04-04T12:00:00Z".to_string(),
171            distance: 100.5,
172            ping: 15.234,
173            jitter: 1.2,
174            packet_loss: 0.0,
175            download: 150_000_000.0,
176            download_peak: 180_000_000.0,
177            upload: 50_000_000.0,
178            upload_peak: 60_000_000.0,
179            ip_address: "192.168.1.1".to_string(),
180        };
181
182        let json = serde_json::to_string(&csv).unwrap();
183        assert!(json.contains("\"server_id\":\"1234\""));
184        assert!(json.contains("\"ping\":15.234"));
185        assert!(json.contains("\"jitter\":1.2"));
186    }
187
188    #[test]
189    fn test_server_clone() {
190        let server = Server {
191            id: "1234".to_string(),
192            url: "http://example.com".to_string(),
193            name: "Test".to_string(),
194            sponsor: "ISP".to_string(),
195            country: "US".to_string(),
196            lat: 40.0,
197            lon: -74.0,
198            distance: 0.0,
199        };
200
201        let cloned = server.clone();
202        assert_eq!(cloned.id, server.id);
203        assert_eq!(cloned.name, server.name);
204    }
205}