Skip to main content

netspeed_cli/
services.rs

1//! Service abstractions for dependency injection.
2//!
3//! All traits use `async_trait` for dyn-compatibility,
4//! enabling `Arc<dyn Trait>` in `ServiceContainer`.
5//!
6//! Services own their HTTP client internally — callers never
7//! pass `reqwest::Client` to service methods (full DIP).
8
9use crate::error::Error;
10use crate::types::{ClientLocation, Server};
11use async_trait::async_trait;
12
13// ============================================================================
14// Server service traits (no client parameter — services own their client)
15// ============================================================================
16
17#[async_trait]
18pub trait ServerFetcher: Send + Sync {
19    async fn fetch_servers(&self) -> Result<(Vec<Server>, Option<ClientLocation>), Error>;
20}
21
22#[async_trait]
23pub trait ServerPinger: Send + Sync {
24    async fn ping_server(&self, server: &Server) -> Result<(f64, f64, f64, Vec<f64>), Error>;
25}
26
27pub trait ServerSelector: Send + Sync {
28    fn select_best(&self, servers: &[Server]) -> Result<Server, Error>;
29}
30
31pub trait ServerService: ServerFetcher + ServerPinger + ServerSelector + Send + Sync {}
32
33// ============================================================================
34// IP discovery trait
35// ============================================================================
36
37#[async_trait]
38pub trait IpDiscoverer: Send + Sync {
39    async fn discover_ip(&self) -> Result<String, Error>;
40}
41
42// ============================================================================
43// Latency monitor trait
44// ============================================================================
45
46#[async_trait]
47pub trait LatencyMonitor: Send + Sync {
48    async fn measure_latency_under_load(
49        &self,
50        server_url: String,
51        samples: std::sync::Arc<std::sync::Mutex<Vec<f64>>>,
52        stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
53    );
54}
55
56// ============================================================================
57// Default implementations (own reqwest::Client internally)
58// ============================================================================
59
60#[derive(Clone, Debug)]
61pub struct DefaultServerService {
62    client: reqwest::Client,
63}
64
65impl DefaultServerService {
66    pub fn new(client: reqwest::Client) -> Self {
67        Self { client }
68    }
69}
70
71#[async_trait]
72impl ServerFetcher for DefaultServerService {
73    async fn fetch_servers(&self) -> Result<(Vec<Server>, Option<ClientLocation>), Error> {
74        crate::servers::fetch(&self.client).await
75    }
76}
77
78#[async_trait]
79impl ServerPinger for DefaultServerService {
80    async fn ping_server(&self, server: &Server) -> Result<(f64, f64, f64, Vec<f64>), Error> {
81        crate::servers::ping_test(&self.client, server).await
82    }
83}
84
85impl ServerSelector for DefaultServerService {
86    fn select_best(&self, servers: &[Server]) -> Result<Server, Error> {
87        crate::servers::select_best_server(servers)
88    }
89}
90
91impl ServerService for DefaultServerService {}
92
93#[derive(Clone, Debug)]
94pub struct DefaultIpService {
95    client: reqwest::Client,
96}
97
98impl DefaultIpService {
99    pub fn new(client: reqwest::Client) -> Self {
100        Self { client }
101    }
102}
103
104#[async_trait]
105impl IpDiscoverer for DefaultIpService {
106    async fn discover_ip(&self) -> Result<String, Error> {
107        crate::http::discover_client_ip(&self.client).await
108    }
109}
110
111#[derive(Clone, Debug)]
112pub struct DefaultLatencyMonitor {
113    client: reqwest::Client,
114}
115
116impl DefaultLatencyMonitor {
117    pub fn new(client: reqwest::Client) -> Self {
118        Self { client }
119    }
120}
121
122#[async_trait]
123impl LatencyMonitor for DefaultLatencyMonitor {
124    async fn measure_latency_under_load(
125        &self,
126        server_url: String,
127        samples: std::sync::Arc<std::sync::Mutex<Vec<f64>>>,
128        stop: std::sync::Arc<std::sync::atomic::AtomicBool>,
129    ) {
130        crate::servers::measure_latency_under_load(self.client.clone(), server_url, samples, stop)
131            .await;
132    }
133}
134
135// ============================================================================
136// Services trait + ServiceContainer
137// ============================================================================
138
139/// Trait for accessing all services — enables mocking PhaseContext in tests.
140pub trait Services: Send + Sync {
141    fn server_service(&self) -> &dyn ServerService;
142    fn ip_service(&self) -> &dyn IpDiscoverer;
143}
144
145/// Service container holding all injectable services.
146/// Uses `Arc<dyn Trait>` for true dependency injection.
147#[derive(Clone)]
148pub struct ServiceContainer {
149    server: std::sync::Arc<dyn ServerService>,
150    ip: std::sync::Arc<dyn IpDiscoverer>,
151}
152
153impl Services for ServiceContainer {
154    fn server_service(&self) -> &dyn ServerService {
155        self.server.as_ref()
156    }
157
158    fn ip_service(&self) -> &dyn IpDiscoverer {
159        self.ip.as_ref()
160    }
161}
162
163impl std::fmt::Debug for ServiceContainer {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        f.debug_struct("ServiceContainer")
166            .field("server", &"dyn ServerService")
167            .field("ip", &"dyn IpDiscoverer")
168            .finish()
169    }
170}
171
172impl ServiceContainer {
173    /// Create a container with default services using the given HTTP client.
174    pub fn new(client: reqwest::Client) -> Self {
175        Self {
176            server: std::sync::Arc::new(DefaultServerService::new(client.clone())),
177            ip: std::sync::Arc::new(DefaultIpService::new(client)),
178        }
179    }
180
181    pub fn with_server(mut self, server: impl ServerService + 'static) -> Self {
182        self.server = std::sync::Arc::new(server);
183        self
184    }
185
186    pub fn with_ip(mut self, ip: impl IpDiscoverer + 'static) -> Self {
187        self.ip = std::sync::Arc::new(ip);
188        self
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    fn make_client() -> reqwest::Client {
197        reqwest::Client::new()
198    }
199
200    #[test]
201    fn test_default_service_container_creation() {
202        let container = ServiceContainer::new(make_client());
203        let _server = container.server_service();
204        let _ip = container.ip_service();
205    }
206
207    #[test]
208    fn test_service_container_with_custom() {
209        let client = make_client();
210        let container = ServiceContainer::new(client)
211            .with_server(DefaultServerService::new(make_client()))
212            .with_ip(DefaultIpService::new(make_client()));
213        let _server = container.server_service();
214        let _ip = container.ip_service();
215    }
216}