Skip to main content

wsio_client/
lib.rs

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