1use std::sync::Arc;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use http::header::{COOKIE, ORIGIN};
6use http::{HeaderMap, HeaderName, HeaderValue};
7use url::Url;
8
9use crate::client::Client;
10use crate::error::Error;
11use crate::protocol::Protocol;
12use crate::transport::Transport;
13use crate::v1_json::V1Json;
14
15#[async_trait]
45pub trait HeaderProvider: Send + Sync {
46 async fn headers(&self) -> Result<HeaderMap, Error>;
49}
50
51#[async_trait]
52impl<F, Fut> HeaderProvider for F
53where
54 F: Fn() -> Fut + Send + Sync,
55 Fut: Future<Output = Result<HeaderMap, Error>> + Send,
56{
57 async fn headers(&self) -> Result<HeaderMap, Error> {
58 self().await
59 }
60}
61
62#[async_trait]
65impl<T: HeaderProvider + ?Sized> HeaderProvider for Arc<T> {
66 async fn headers(&self) -> Result<HeaderMap, Error> {
67 (**self).headers().await
68 }
69}
70
71type Terminal = Arc<dyn Fn(&Error) -> bool + Send + Sync>;
74
75pub struct ClientBuilder {
77 url: String,
78 transport: Option<Arc<dyn Transport>>,
81 protocols: Vec<Arc<dyn Protocol>>,
82 headers: HeaderMap,
83 invalid_header: Option<Error>,
84 header_provider: Option<Arc<dyn HeaderProvider>>,
85 stop_on_error: Option<Terminal>,
86 stale_after: Duration,
87 subscribe_retry: Duration,
88 initial_backoff: Duration,
89 longest_backoff: Duration,
90 max_attempts: u32,
91 message_buffer: usize,
92}
93
94impl ClientBuilder {
95 pub(crate) fn new(url: String) -> ClientBuilder {
96 ClientBuilder {
97 url,
98 transport: None,
99 protocols: vec![Arc::new(V1Json)],
100 headers: HeaderMap::new(),
101 invalid_header: None,
102 header_provider: None,
103 stop_on_error: None,
104 stale_after: Duration::from_secs(6),
105 subscribe_retry: Duration::from_millis(500),
106 initial_backoff: Duration::from_secs(1),
107 longest_backoff: Duration::from_secs(30),
108 max_attempts: 0,
109 message_buffer: 64,
110 }
111 }
112
113 pub fn transport(mut self, transport: impl Transport + 'static) -> ClientBuilder {
119 self.transport = Some(Arc::new(transport));
120 self
121 }
122
123 pub fn protocols(mut self, protocols: Vec<Arc<dyn Protocol>>) -> ClientBuilder {
127 self.protocols = protocols;
128 self
129 }
130
131 pub fn prefer_protocols(mut self, protocols: Vec<Arc<dyn Protocol>>) -> ClientBuilder {
134 let mut preferred = protocols;
135 preferred.append(&mut self.protocols);
136 self.protocols = preferred;
137 self
138 }
139
140 pub fn header(mut self, name: &str, value: &str) -> ClientBuilder {
144 match parse_header(name, value) {
145 Ok((name, value)) => {
146 self.headers.insert(name, value);
147 }
148 Err(error) => {
149 self.invalid_header.get_or_insert(error);
150 }
151 }
152 self
153 }
154
155 pub fn cookie(self, cookie: &str) -> ClientBuilder {
157 self.header(COOKIE.as_str(), cookie)
158 }
159
160 pub fn origin(self, origin: &str) -> ClientBuilder {
166 self.header(ORIGIN.as_str(), origin)
167 }
168
169 pub fn header_provider(mut self, provider: impl HeaderProvider + 'static) -> ClientBuilder {
172 self.header_provider = Some(Arc::new(provider));
173 self
174 }
175
176 pub fn stop_on_error(
193 mut self,
194 terminal: impl Fn(&Error) -> bool + Send + Sync + 'static,
195 ) -> ClientBuilder {
196 self.stop_on_error = Some(Arc::new(terminal));
197 self
198 }
199
200 pub fn stale_after(mut self, after: Duration) -> ClientBuilder {
203 self.stale_after = after;
204 self
205 }
206
207 pub fn backoff(mut self, initial: Duration, longest: Duration) -> ClientBuilder {
210 self.initial_backoff = initial;
211 self.longest_backoff = longest;
212 self
213 }
214
215 pub fn max_attempts(mut self, attempts: u32) -> ClientBuilder {
220 self.max_attempts = attempts;
221 self
222 }
223
224 pub fn subscribe_retry(mut self, retry: Duration) -> ClientBuilder {
227 self.subscribe_retry = retry;
228 self
229 }
230
231 pub fn message_buffer(mut self, messages: usize) -> ClientBuilder {
234 self.message_buffer = messages;
235 self
236 }
237
238 pub fn build(mut self) -> Result<Client, Error> {
241 if let Some(error) = self.invalid_header {
242 return Err(error);
243 }
244
245 let transport = match self.transport.take() {
246 Some(transport) => transport,
247 None => default_transport()?,
248 };
249 self.assume_origin();
250 Ok(Client::new(Config {
251 url: self.url,
252 transport,
253 protocols: self.protocols,
254 headers: self.headers,
255 header_provider: self.header_provider,
256 stop_on_error: self.stop_on_error,
257 stale_after: self.stale_after,
258 subscribe_retry: self.subscribe_retry,
259 initial_backoff: self.initial_backoff,
260 longest_backoff: self.longest_backoff,
261 max_attempts: self.max_attempts,
262 message_buffer: self.message_buffer,
263 }))
264 }
265
266 fn assume_origin(&mut self) {
267 if !self.headers.contains_key(ORIGIN)
268 && let Some(origin) = origin_of(&self.url)
269 && let Ok(origin) = HeaderValue::from_str(&origin)
270 {
271 self.headers.insert(ORIGIN, origin);
272 }
273 }
274}
275
276#[cfg(feature = "websocket")]
277#[allow(
278 clippy::unnecessary_wraps,
279 reason = "the other half of this pair, without the feature, has nothing to hand back"
280)]
281fn default_transport() -> Result<Arc<dyn Transport>, Error> {
282 Ok(Arc::new(crate::websocket::WebSocketTransport::default()))
283}
284
285#[cfg(not(feature = "websocket"))]
286fn default_transport() -> Result<Arc<dyn Transport>, Error> {
287 Err(Error::NoTransport)
288}
289
290fn parse_header(name: &str, value: &str) -> Result<(HeaderName, HeaderValue), Error> {
291 let invalid = |source: http::Error| Error::InvalidHeader {
292 name: name.to_string(),
293 source: Arc::new(source),
294 };
295 let name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| invalid(error.into()))?;
296 let value = HeaderValue::from_str(value).map_err(|error| invalid(error.into()))?;
297 Ok((name, value))
298}
299
300fn origin_of(url: &str) -> Option<String> {
301 let endpoint = Url::parse(url).ok()?;
302 let host = endpoint.host_str()?;
303 let authority = match endpoint.port() {
304 Some(port) => format!("{host}:{port}"),
305 None => host.to_string(),
306 };
307 match endpoint.scheme() {
308 "wss" | "https" => Some(format!("https://{authority}")),
309 "ws" | "http" => Some(format!("http://{authority}")),
310 _ => None,
311 }
312}
313
314pub(crate) struct Config {
316 pub(crate) url: String,
317 pub(crate) transport: Arc<dyn Transport>,
318 pub(crate) protocols: Vec<Arc<dyn Protocol>>,
319 pub(crate) headers: HeaderMap,
320 pub(crate) header_provider: Option<Arc<dyn HeaderProvider>>,
321 pub(crate) stop_on_error: Option<Terminal>,
322 pub(crate) stale_after: Duration,
323 pub(crate) subscribe_retry: Duration,
324 pub(crate) initial_backoff: Duration,
325 pub(crate) longest_backoff: Duration,
326 pub(crate) max_attempts: u32,
327 pub(crate) message_buffer: usize,
328}
329
330#[cfg(test)]
331#[allow(
332 clippy::unwrap_used,
333 clippy::expect_used,
334 reason = "a test that can't have what it asked for has nothing left to assert"
335)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn the_origin_follows_the_cable_url() {
341 let origins = [
342 (
343 "wss://cable.example.com/cable",
344 Some("https://cable.example.com"),
345 ),
346 (
347 "ws://cable.example.com:3000/cable",
348 Some("http://cable.example.com:3000"),
349 ),
350 (
351 "wss://cable.example.com:8443/cable",
352 Some("https://cable.example.com:8443"),
353 ),
354 ("ftp://cable.example.com/cable", None),
355 ("not a url", None),
356 ];
357
358 for (url, origin) in origins {
359 assert_eq!(origin.map(str::to_string), origin_of(url), "{url}");
360 }
361 }
362
363 #[cfg(not(feature = "websocket"))]
364 #[test]
365 fn without_the_websocket_feature_a_transport_is_required() {
366 assert!(matches!(
367 Client::builder("wss://cable.example.com/cable").build(),
368 Err(Error::NoTransport)
369 ));
370 }
371
372 #[cfg(feature = "websocket")]
373 #[test]
374 fn the_websocket_transport_is_the_default() {
375 assert!(
376 Client::builder("wss://cable.example.com/cable")
377 .build()
378 .is_ok()
379 );
380 }
381}