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