Skip to main content

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