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#[derive(Clone, Debug)]
34pub struct WsIoClient(Arc<WsIoClientRuntime>);
35
36impl WsIoClient {
37 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}