sms_client/
lib.rs

1//! A client library for SMS-API, via HTTP and an optional websocket connection.
2//! <https://github.com/morgverd/sms-api>
3
4#![deny(missing_docs)]
5#![deny(unsafe_code)]
6#![warn(clippy::all, clippy::pedantic)]
7
8use crate::error::*;
9
10pub mod config;
11pub mod error;
12pub mod types;
13
14#[cfg(feature = "http")]
15pub mod http;
16
17#[cfg(feature = "websocket")]
18pub mod ws;
19
20/// SMS Client with HTTP and optional WebSocket support.
21#[derive(Clone, Debug)]
22pub struct Client {
23
24    #[cfg(feature = "http")]
25    http: Option<std::sync::Arc<http::HttpClient>>,
26
27    #[cfg(feature = "websocket")]
28    ws_client: std::sync::Arc<tokio::sync::RwLock<Option<ws::WebsocketClient>>>,
29
30    #[cfg(feature = "websocket")]
31    ws_config: Option<config::WebsocketConfig>
32}
33impl Client {
34
35    /// Create an SMS client with a connection config.
36    pub fn new(config: config::ClientConfig) -> ClientResult<Self> {
37        Ok(Self {
38
39            #[cfg(feature = "http")]
40            http: config.http.map(|config| http::HttpClient::new(config).map(std::sync::Arc::new)).transpose()?,
41
42            #[cfg(feature = "websocket")]
43            ws_client: std::sync::Arc::new(tokio::sync::RwLock::new(None)),
44
45            #[cfg(feature = "websocket")]
46            ws_config: config.websocket
47        })
48    }
49
50    /// Borrow the optional inner HTTP client.
51    #[cfg(feature = "http")]
52    pub fn http(&self) -> ClientResult<&http::HttpClient> {
53        match &self.http {
54            Some(http) => Ok(http),
55            None => Err(ClientError::MissingConfiguration("HttpClient"))
56        }
57    }
58
59    /// Get a cloned Arc to the optional HTTP client for use in async contexts.
60    #[cfg(feature = "http")]
61    pub fn http_arc(&self) -> ClientResult<std::sync::Arc<http::HttpClient>> {
62        match &self.http {
63            Some(http) => Ok(http.clone()),
64            None => Err(ClientError::MissingConfiguration("HttpClient"))
65        }
66    }
67
68    /// Set the callback for incoming WebSocket messages. The callback will include the WebSocket
69    /// message and an Arc to the current Client allowing for easy use within the callback!
70    /// This must be called before starting the WebSocket connection.
71    ///
72    /// # Example
73    /// ```
74    /// use sms_client::http::types::HttpOutgoingSmsMessage;
75    /// use sms_client::ws::types::WebsocketMessage;
76    /// use sms_client::Client;
77    /// use log::info;
78    ///
79    /// #[tokio::main]
80    /// async fn main() {
81    ///     let client: Client = unimplemented!("See other examples");
82    ///
83    ///     client.on_message(move |message, client| {
84    ///         match message {
85    ///             WebsocketMessage::IncomingMessage(sms) => {
86    ///                 // Can access client.http() here!
87    ///             },
88    ///             _ => { }
89    ///         }
90    ///     }).await?
91    /// }
92    /// ```
93    #[cfg(feature = "websocket")]
94    pub async fn on_message<F>(&self, callback: F) -> ClientResult<()>
95    where
96        F: Fn(ws::types::WebsocketMessage, std::sync::Arc<Self>) + Send + Sync + 'static,
97    {
98        let mut ws_guard = self.create_or_get_ws_client().await?;
99        if let Some(ws_client) = ws_guard.as_mut() {
100            let client = std::sync::Arc::new(self.clone());
101            ws_client.on_message(move |msg| {
102                callback(msg, std::sync::Arc::clone(&client));
103            });
104        }
105        Ok(())
106    }
107
108    /// Set the callback for incoming WebSocket messages (simple version without client copy).
109    /// This must be called before starting the WebSocket connection.
110    ///
111    /// # Example
112    /// ```
113    /// use sms_client::Client;
114    /// use sms_client::ws::types::WebsocketMessage;
115    /// use log::info;
116    ///
117    /// #[tokio::main]
118    /// async fn main() {
119    ///     let client: Client = unimplemented!("See other examples");
120    ///
121    ///     client.on_message_simple(move |message| {
122    ///         match message {
123    ///             WebsocketMessage::OutgoingMessage(sms) => info!("Outgoing message: {:?}", sms),
124    ///             _ => { }
125    ///         }
126    ///     }).await?
127    /// }
128    /// ```
129    #[cfg(feature = "websocket")]
130    pub async fn on_message_simple<F>(&self, callback: F) -> ClientResult<()>
131    where
132        F: Fn(ws::types::WebsocketMessage) + Send + Sync + 'static,
133    {
134        let mut ws_guard = self.create_or_get_ws_client().await?;
135        if let Some(ws_client) = ws_guard.as_mut() {
136            ws_client.on_message(callback);
137        }
138        Ok(())
139    }
140
141    /// Start the WebSocket connection.
142    #[cfg(feature = "websocket")]
143    pub async fn start_background_websocket(&self) -> ClientResult<()> {
144        let mut ws_guard = self.create_or_get_ws_client().await?;
145        if let Some(ws_client) = ws_guard.as_mut() {
146            ws_client.start_background().await?;
147        }
148        Ok(())
149    }
150
151    /// Start the WebSocket connection and block until closed.
152    #[cfg(feature = "websocket")]
153    pub async fn start_blocking_websocket(&self) -> ClientResult<()> {
154        let mut ws_guard = self.create_or_get_ws_client().await?;
155        if let Some(ws_client) = ws_guard.as_mut() {
156            ws_client.start_blocking().await?;
157        }
158        Ok(())
159    }
160
161    /// Stop the WebSocket connection.
162    #[cfg(feature = "websocket")]
163    pub async fn stop_background_websocket(&self) -> ClientResult<()> {
164        let mut ws_guard = self.ws_client.write().await;
165
166        if let Some(ws_client) = ws_guard.as_mut() {
167            ws_client.stop_background().await?;
168        }
169
170        Ok(())
171    }
172
173    /// Check if the WebSocket is currently connected.
174    #[cfg(feature = "websocket")]
175    pub async fn is_websocket_connected(&self) -> bool {
176        let ws_guard = self.ws_client.read().await;
177
178        if let Some(ws_client) = ws_guard.as_ref() {
179            ws_client.is_connected().await
180        } else {
181            false
182        }
183    }
184
185    /// Force a WebSocket reconnection.
186    #[cfg(feature = "websocket")]
187    pub async fn reconnect_websocket(&self) -> ClientResult<()> {
188        let ws_guard = self.ws_client.read().await;
189
190        if let Some(ws_client) = ws_guard.as_ref() {
191            ws_client.reconnect().await?;
192            Ok(())
193        } else {
194            Err(ClientError::NoWebsocketClient)
195        }
196    }
197
198    /// Create or return existing websocket client guard.
199    #[cfg(feature = "websocket")]
200    async fn create_or_get_ws_client(&self) -> ClientResult<tokio::sync::RwLockWriteGuard<'_, Option<ws::WebsocketClient>>> {
201        let mut ws_guard = self.ws_client.write().await;
202        if ws_guard.is_none() {
203            let config = match self.ws_config.clone() {
204                Some(config) => config,
205                None => return Err(ClientError::MissingConfiguration("WebsocketConfig"))
206            };
207
208            let ws_client = ws::WebsocketClient::new(config);
209            *ws_guard = Some(ws_client);
210        }
211
212        Ok(ws_guard)
213    }
214}
215impl Drop for Client {
216    fn drop(&mut self) {
217        // The WebSocket client will handle its own cleanup in its Drop impl
218        // This is just here to ensure proper cleanup ordering.
219    }
220}