sms_client/
config.rs

1//! SMS-Client connection configuration.
2
3/// HTTP-specific configuration.
4#[cfg(feature = "http")]
5#[derive(Clone, Debug)]
6pub struct HttpConfig {
7
8    /// HTTP base URL. eg: http://192.168.1.2:3000
9    pub url: String,
10
11    /// Optional HTTP authorization header token.
12    pub authorization: Option<String>,
13
14    /// A default timeout to apply to all requests that do not have
15    /// their own timeout (this applies to all if modem_timeout is None,
16    /// otherwise only database and sys queries).
17    pub base_timeout: std::time::Duration,
18
19    /// An optional timeout to use specifically for modem requests
20    /// (requests that must send and receive modem data). This should
21    /// be higher than the default timeout as they can take longer.
22    pub modem_timeout: Option<std::time::Duration>,
23}
24#[cfg(feature = "http")]
25impl HttpConfig {
26
27    /// The default amount of seconds before an HTTP request should time out.
28    /// If there is no modem_timeout, this is applied to all requests.
29    pub const HTTP_DEFAULT_BASE_TIMEOUT: u64 = 5;
30
31    /// The default amount of seconds before an HTTP request that interacts directly
32    /// with the modem should time out. This should be longer to allow for carrier response.
33    pub const HTTP_DEFAULT_MODEM_TIMEOUT: u64 = 20;
34
35    /// Create a new HTTP configuration with default settings.
36    pub fn new(url: impl Into<String>) -> Self {
37        Self {
38            url: url.into(),
39            authorization: None,
40            base_timeout: std::time::Duration::from_secs(Self::HTTP_DEFAULT_BASE_TIMEOUT),
41            modem_timeout: Some(std::time::Duration::from_secs(Self::HTTP_DEFAULT_MODEM_TIMEOUT))
42        }
43    }
44
45    /// Set the authorization token.
46    pub fn with_auth(mut self, token: impl Into<String>) -> Self {
47        self.authorization = Some(token.into());
48        self
49    }
50
51    /// Set the base request timeout.
52    pub fn with_base_timeout(mut self, timeout: std::time::Duration) -> Self {
53        self.base_timeout = timeout;
54        self
55    }
56
57    /// Set the modem request timeout.
58    pub fn with_modem_timeout(mut self, timeout: Option<std::time::Duration>) -> Self {
59        self.modem_timeout = timeout;
60        self
61    }
62}
63#[cfg(feature = "http")]
64impl Default for HttpConfig {
65    fn default() -> Self {
66        Self {
67            url: "http://localhost:3000".to_string(),
68            authorization: None,
69            base_timeout: std::time::Duration::from_secs(Self::HTTP_DEFAULT_BASE_TIMEOUT),
70            modem_timeout: Some(std::time::Duration::from_secs(Self::HTTP_DEFAULT_MODEM_TIMEOUT))
71        }
72    }
73}
74
75/// WebSocket-specific configuration.
76#[cfg(feature = "websocket")]
77#[derive(Clone, Debug)]
78pub struct WebSocketConfig {
79
80    /// Websocket event channel URL. eg: ws://192.168.1.2:3000/ws
81    pub url: String,
82
83    /// Optional Websocket authorization header token.
84    pub authorization: Option<String>,
85
86    /// Should the websocket connection automatically reconnect if disconnected.
87    pub auto_reconnect: bool,
88
89    /// Interval to use between reconnection attempts.
90    pub reconnect_interval: std::time::Duration,
91
92    /// The interval between sending websocket pings.
93    pub ping_interval: std::time::Duration,
94
95    /// Timeout duration for missing pings.
96    pub ping_timeout: std::time::Duration,
97
98    /// Maximum reconnection attempts (None = unlimited).
99    pub max_reconnect_attempts: Option<u32>,
100
101    /// Optional set of events that should be listened to. This is added to
102    /// the websocket connection URI, and the server filters out events before
103    /// sending them. By default, all events are sent when none are selected.
104    pub filtered_events: Option<Vec<String>>
105}
106#[cfg(feature = "websocket")]
107impl WebSocketConfig {
108
109    /// The default interval to use between connection attempts.
110    /// Sequential attempts use a backoff up to 60 seconds.
111    pub const WS_DEFAULT_RECONNECT_INTERVAL: u64 = 5;
112
113    /// The interval between sending ping messages.
114    pub const WS_DEFAULT_PING_INTERVAL: u64 = 10;
115
116    /// The duration between the last ping to count as a timeout.
117    pub const WS_DEFAULT_PING_TIMEOUT: u64 = 30;
118
119    /// Create a new WebSocket configuration with default settings.
120    pub fn new(url: impl Into<String>) -> Self {
121        Self {
122            url: url.into(),
123            authorization: None,
124            auto_reconnect: true,
125            reconnect_interval: std::time::Duration::from_secs(Self::WS_DEFAULT_RECONNECT_INTERVAL),
126            ping_interval: std::time::Duration::from_secs(Self::WS_DEFAULT_PING_INTERVAL),
127            ping_timeout: std::time::Duration::from_secs(Self::WS_DEFAULT_PING_TIMEOUT),
128            max_reconnect_attempts: None,
129            filtered_events: None
130        }
131    }
132
133    /// Set the authorization token.
134    pub fn with_auth(mut self, token: impl Into<String>) -> Self {
135        self.authorization = Some(token.into());
136        self
137    }
138
139    /// Enable or disable auto-reconnection.
140    pub fn with_auto_reconnect(mut self, enabled: bool) -> Self {
141        self.auto_reconnect = enabled;
142        self
143    }
144
145    /// Set the reconnection interval.
146    pub fn with_reconnect_interval(mut self, interval: std::time::Duration) -> Self {
147        self.reconnect_interval = interval;
148        self
149    }
150
151    /// Set the ping interval.
152    pub fn with_ping_interval(mut self, interval: std::time::Duration) -> Self {
153        self.ping_interval = interval;
154        self
155    }
156
157    /// Set the ping timeout.
158    pub fn with_ping_timeout(mut self, timeout: std::time::Duration) -> Self {
159        self.ping_timeout = timeout;
160        self
161    }
162
163    /// Set maximum reconnection attempts (None = unlimited).
164    pub fn with_max_reconnect_attempts(mut self, max_attempts: Option<u32>) -> Self {
165        self.max_reconnect_attempts = max_attempts;
166        self
167    }
168
169    /// Set filtered listen events, this is included in the connection query-string.
170    /// The provided Vec should contain every event name that should be sent by the server.
171    /// If None, filtering is disabled so all events are sent.
172    pub fn with_filtered_events(mut self, events: Option<Vec<impl Into<String>>>) -> Self {
173        self.filtered_events = events.map(|events| events.into_iter().map(Into::into).collect());
174        self
175    }
176}
177#[cfg(feature = "websocket")]
178impl Default for WebSocketConfig {
179    fn default() -> Self {
180        Self {
181            url: "ws://localhost:3000/ws".to_string(),
182            authorization: None,
183            auto_reconnect: true,
184            reconnect_interval: std::time::Duration::from_secs(Self::WS_DEFAULT_RECONNECT_INTERVAL),
185            ping_interval: std::time::Duration::from_secs(Self::WS_DEFAULT_PING_INTERVAL),
186            ping_timeout: std::time::Duration::from_secs(Self::WS_DEFAULT_PING_TIMEOUT),
187            max_reconnect_attempts: None,
188            filtered_events: None
189        }
190    }
191}
192
193/// WebSocket and HTTP TLS configuration.
194#[derive(Clone, Debug)]
195pub struct TLSConfig {
196
197    /// TLS certificate filepath.
198    pub certificate: std::path::PathBuf
199}
200impl TLSConfig {
201
202    /// Set a certificate filepath to use for TLS connections.
203    pub fn new(certificate: impl Into<std::path::PathBuf>) -> crate::error::ClientResult<Self> {
204        Ok(Self {
205            certificate: Self::verify_path(certificate.into())?
206        })
207    }
208
209    /// Verify certificate filepath, that it's a valid filepath and it has an appropriate extension.
210    fn verify_path(path: std::path::PathBuf) -> crate::error::ClientResult<std::path::PathBuf> {
211        if !path.exists() {
212            return Err(crate::error::ClientError::ConfigError("Certificate filepath does not exist"));
213        }
214        if !path.is_file() {
215            return Err(crate::error::ClientError::ConfigError("Certificate filepath is not a file"));
216        }
217        let canonical_path = path.canonicalize()
218            .map_err(|_| { crate::error::ClientError::ConfigError("Invalid certificate path") })?;
219
220        // Check file extension.
221        match path.extension().and_then(|s| s.to_str()) {
222            Some("pem") | Some("crt") | Some("der") => Ok(canonical_path),
223            _ => Err(crate::error::ClientError::ConfigError("Invalid certificate file extension")),
224        }
225    }
226}
227
228/// Complete client configuration.
229#[derive(Clone, Debug)]
230pub struct ClientConfig {
231
232    /// TLS configuration, used for both HTTP and WebSocket connections.
233    pub tls: Option<TLSConfig>,
234
235    /// HTTP configuration.
236    #[cfg(feature = "http")]
237    pub http: Option<HttpConfig>,
238
239    /// Optional WebSocket configuration.
240    #[cfg(feature = "websocket")]
241    pub websocket: Option<WebSocketConfig>
242}
243impl ClientConfig {
244
245    /// Create a new configuration with only HTTP support.
246    ///
247    /// # Example
248    /// ```
249    /// use sms_client::config::ClientConfig;
250    ///
251    /// let config = ClientConfig::http_only("http://192.168.1.2:3000");
252    /// ```
253    #[cfg(feature = "http")]
254    pub fn http_only(url: impl Into<String>) -> Self {
255        Self {
256            tls: None,
257            http: Some(HttpConfig::new(url)),
258
259            #[cfg(feature = "websocket")]
260            websocket: None
261        }
262    }
263
264    /// Create a new configuration with only WebSocket support.
265    ///
266    /// # Example
267    /// ```
268    /// use sms_client::config::ClientConfig;
269    ///
270    /// let config = ClientConfig::websocket_only("ws://192.168.1.2:3000/ws");
271    /// ```
272    #[cfg(feature = "websocket")]
273    pub fn websocket_only(ws_url: impl Into<String>) -> Self {
274        Self {
275            tls: None,
276
277            #[cfg(feature = "http")]
278            http: None,
279
280            websocket: Some(WebSocketConfig::new(ws_url))
281        }
282    }
283
284    /// Create a new configuration with both HTTP and WebSocket support.
285    ///
286    /// # Example
287    /// ```
288    /// use sms_client::config::ClientConfig;
289    ///
290    /// let config = ClientConfig::both(
291    ///     "http://192.168.1.2:3000",
292    ///     "ws://192.168.1.2:3000/ws"
293    /// );
294    /// ```
295    #[cfg(feature = "http")]
296    #[cfg(feature = "websocket")]
297    pub fn both(http_url: impl Into<String>, ws_url: impl Into<String>) -> Self {
298        Self {
299            tls: None,
300            http: Some(HttpConfig::new(http_url)),
301            websocket: Some(WebSocketConfig::new(ws_url))
302        }
303    }
304
305    /// Create a configuration from individual HTTP and WebSocket configs.
306    ///
307    /// # Example
308    /// ```
309    /// use std::time::Duration;
310    /// use sms_client::config::{ClientConfig, HttpConfig, WebSocketConfig};
311    ///
312    /// let http = HttpConfig::new("http://192.168.1.2:3000")
313    ///     .with_auth("token123")
314    ///     .with_base_timeout(Duration::from_secs(30));
315    ///
316    /// let ws = WebSocketConfig::new("ws://192.168.1.2:3000/ws")
317    ///     .with_auth("token123")
318    ///     .with_auto_reconnect(true)
319    ///     .with_max_reconnect_attempts(Some(10));
320    ///
321    /// let config = ClientConfig::from_parts(Some(http), Some(ws));
322    /// ```
323    #[cfg(feature = "http")]
324    #[cfg(feature = "websocket")]
325    pub fn from_parts(http: Option<HttpConfig>, websocket: Option<WebSocketConfig>) -> Self {
326        Self { tls: None, http, websocket }
327    }
328
329    /// Add TLS configuration.
330    pub fn add_tls(mut self, tls: TLSConfig) -> Self {
331        self.tls = Some(tls);
332        self
333    }
334
335    /// Set authorization for both HTTP and WebSocket.
336    /// This only sets the authorization value for components that already exist.
337    ///
338    /// # Example
339    /// ```rust
340    /// use sms_client::config::ClientConfig;
341    ///
342    /// let config = ClientConfig::both(
343    ///     "http://192.168.1.2:3000",
344    ///     "ws://192.168.1.2:3000/ws"
345    /// ).with_auth("my-token");
346    /// ```
347    pub fn with_auth(mut self, token: impl Into<String>) -> Self {
348        let token = token.into();
349
350        #[cfg(feature = "http")]
351        if let Some(http) = &mut self.http {
352            http.authorization = Some(token.clone());
353        }
354
355        #[cfg(feature = "websocket")]
356        if let Some(ws) = &mut self.websocket {
357            ws.authorization = Some(token);
358        }
359        self
360    }
361
362    /// Modify/Set a TLSConfig with certificate filepath.
363    ///
364    /// # Example
365    /// ```rust
366    /// use sms_client::config::ClientConfig;
367    ///
368    /// let config = ClientConfig::http_only("https://192.168.1.2:3000")
369    ///     .with_certificate("./certificate.crt")?;
370    pub fn with_certificate(mut self, certificate: impl Into<std::path::PathBuf>) -> crate::error::ClientResult<Self> {
371        if let Some(tls) = &mut self.tls {
372            tls.certificate = TLSConfig::verify_path(certificate.into())?;
373        } else {
374            self.tls = Some(TLSConfig::new(certificate)?);
375        }
376        Ok(self)
377    }
378
379    /// Configure the HTTP component if present.
380    ///
381    /// # Example
382    /// ```
383    /// use sms_client::config::ClientConfig;
384    /// use std::time::Duration;
385    ///
386    /// let config = ClientConfig::http_only("http://192.168.1.2:3000")
387    ///     .configure_http(|http| {
388    ///         http.with_base_timeout(Duration::from_secs(30))
389    ///             .with_auth("token")
390    ///     });
391    /// ```
392    #[cfg(feature = "http")]
393    pub fn configure_http<F>(mut self, f: F) -> Self
394    where
395        F: FnOnce(HttpConfig) -> HttpConfig,
396    {
397        if let Some(http) = self.http {
398            self.http = Some(f(http));
399        }
400        self
401    }
402
403    /// Configure the WebSocket component if present.
404    ///
405    /// # Example
406    /// ```
407    /// use sms_client::config::ClientConfig;
408    /// use std::time::Duration;
409    ///
410    /// let config = ClientConfig::both(
411    ///     "http://192.168.1.2:3000",
412    ///     "ws://192.168.1.2:3000/ws"
413    /// ).configure_websocket(|ws| {
414    ///     ws.with_ping_interval(Duration::from_secs(60))
415    ///       .with_max_reconnect_attempts(Some(5))
416    /// });
417    /// ```
418    #[cfg(feature = "websocket")]
419    pub fn configure_websocket<F>(mut self, f: F) -> Self
420    where
421        F: FnOnce(WebSocketConfig) -> WebSocketConfig,
422    {
423        if let Some(ws) = self.websocket {
424            self.websocket = Some(f(ws));
425        }
426        self
427    }
428
429    /// Add WebSocket configuration.
430    ///
431    /// # Example
432    /// ```
433    /// use sms_client::config::{ClientConfig, WebSocketConfig};
434    ///
435    /// let config = ClientConfig::http_only("http://192.168.1.2:3000")
436    ///     .add_websocket(WebSocketConfig::new("ws://192.168.1.2:3000/ws"));
437    /// ```
438    #[cfg(feature = "websocket")]
439    pub fn add_websocket(mut self, websocket: WebSocketConfig) -> Self {
440        self.websocket = Some(websocket);
441        self
442    }
443}
444impl Default for ClientConfig {
445    fn default() -> Self {
446        Self {
447            tls: None,
448
449            #[cfg(feature = "http")]
450            http: Some(HttpConfig::default()),
451
452            #[cfg(feature = "websocket")]
453            websocket: Some(WebSocketConfig::default())
454        }
455    }
456}
457
458#[cfg(feature = "http")]
459impl From<HttpConfig> for ClientConfig {
460    fn from(http: HttpConfig) -> Self {
461        ClientConfig { tls: None, http: Some(http), ..Default::default() }
462    }
463}
464
465#[cfg(feature = "websocket")]
466impl From<WebSocketConfig> for ClientConfig {
467    fn from(ws: WebSocketConfig) -> Self {
468        ClientConfig { tls: None, websocket: Some(ws), ..Default::default() }
469    }
470}