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