Skip to main content

wsio_client/
lib.rs

1#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
2
3use std::sync::Arc;
4
5use anyhow::{
6    Result,
7    anyhow,
8    bail,
9};
10use serde::{
11    Serialize,
12    de::DeserializeOwned,
13};
14use tokio_util::sync::CancellationToken;
15use url::Url;
16pub use wsio_core as core;
17
18pub mod builder;
19mod config;
20mod runtime;
21pub mod session;
22
23use crate::{
24    builder::WsIoClientBuilder,
25    core::traits::task::spawner::TaskSpawner,
26    runtime::WsIoClientRuntime,
27    session::WsIoClientSession,
28};
29
30// Structs
31#[derive(Clone, Debug)]
32pub struct WsIoClient(Arc<WsIoClientRuntime>);
33
34impl WsIoClient {
35    // Public methods
36    pub fn builder(url: impl AsRef<str>) -> Result<WsIoClientBuilder> {
37        let url = Url::parse(url.as_ref()).map_err(|err| anyhow!("Invalid URL: {err}"))?;
38
39        if url.scheme() == "wss"
40            && !cfg!(any(
41                feature = "tls-native",
42                feature = "tls-rustls-native",
43                feature = "tls-rustls-webpki",
44            ))
45        {
46            bail!("TLS is required for wss:// URLs, but no TLS feature is enabled");
47        }
48
49        WsIoClientBuilder::new(url)
50    }
51
52    pub fn cancel_token(&self) -> Arc<CancellationToken> {
53        self.0.cancel_token()
54    }
55
56    pub async fn connect(&self) {
57        self.0.connect().await
58    }
59
60    pub async fn disconnect(&self) {
61        self.0.disconnect().await
62    }
63
64    pub async fn emit<D: Serialize>(&self, event: impl AsRef<str>, data: Option<&D>) -> Result<()> {
65        self.0.emit(event.as_ref(), data).await
66    }
67
68    #[inline]
69    pub fn is_session_ready(&self) -> bool {
70        self.0.is_session_ready()
71    }
72
73    #[inline]
74    pub fn off(&self, event: impl AsRef<str>) {
75        self.0.off(event.as_ref());
76    }
77
78    #[inline]
79    pub fn off_by_handler_id(&self, event: impl AsRef<str>, handler_id: u32) {
80        self.0.off_by_handler_id(event.as_ref(), handler_id);
81    }
82
83    #[inline]
84    pub fn on<H, Fut, D>(&self, event: impl AsRef<str>, handler: H) -> u32
85    where
86        H: Fn(Arc<WsIoClientSession>, Arc<D>) -> Fut + Send + Sync + 'static,
87        Fut: Future<Output = Result<()>> + Send + 'static,
88        D: DeserializeOwned + Send + Sync + 'static,
89    {
90        self.0.on(event.as_ref(), handler)
91    }
92
93    #[inline]
94    pub fn spawn_task<F: Future<Output = Result<()>> + Send + 'static>(&self, future: F) {
95        self.0.spawn_task(future);
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use std::borrow::Cow;
102
103    use super::*;
104
105    fn assert_builder_accepts(url: impl AsRef<str>) {
106        assert!(WsIoClient::builder(url).is_ok());
107    }
108
109    #[test]
110    #[allow(clippy::needless_borrows_for_generic_args)] // Explicitly verify borrowed input forms.
111    fn builder_accepts_common_url_input_types() {
112        const URL: &str = "ws://localhost:8080/chat";
113
114        let str_ref = URL;
115        assert_builder_accepts(str_ref);
116        assert_builder_accepts(&str_ref);
117
118        let string = URL.to_owned();
119        assert_builder_accepts(string.clone());
120        assert_builder_accepts(&string);
121
122        let url = Url::parse(URL).unwrap();
123        assert_builder_accepts(url.clone());
124        assert_builder_accepts(&url);
125
126        assert_builder_accepts(Cow::Borrowed(URL));
127        assert_builder_accepts(Cow::Owned(URL.to_owned()));
128    }
129
130    #[test]
131    fn builder_rejects_unparseable_url() {
132        let result = WsIoClient::builder("not a url");
133
134        match result {
135            Ok(_) => panic!("invalid URL should fail"),
136            Err(err) => assert!(err.to_string().contains("Invalid URL")),
137        }
138    }
139
140    #[test]
141    fn builder_rejects_wss_without_tls_features() {
142        let result = WsIoClient::builder("wss://localhost/socket");
143
144        if cfg!(any(
145            feature = "tls-native",
146            feature = "tls-rustls-native",
147            feature = "tls-rustls-webpki",
148        )) {
149            assert!(result.is_ok());
150        } else {
151            match result {
152                Ok(_) => panic!("wss URL should fail when no TLS feature is enabled"),
153                Err(err) => assert!(err.to_string().contains("TLS is required")),
154            }
155        }
156    }
157}