use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use http::header::{COOKIE, ORIGIN};
use http::{HeaderMap, HeaderName, HeaderValue};
use url::Url;
use crate::client::Client;
use crate::error::Error;
use crate::protocol::Protocol;
use crate::transport::Transport;
use crate::v1_json::V1Json;
#[async_trait]
pub trait HeaderProvider: Send + Sync {
async fn headers(&self) -> Result<HeaderMap, Error>;
}
#[async_trait]
impl<F, Fut> HeaderProvider for F
where
F: Fn() -> Fut + Send + Sync,
Fut: Future<Output = Result<HeaderMap, Error>> + Send,
{
async fn headers(&self) -> Result<HeaderMap, Error> {
self().await
}
}
#[async_trait]
impl<T: HeaderProvider + ?Sized> HeaderProvider for Arc<T> {
async fn headers(&self) -> Result<HeaderMap, Error> {
(**self).headers().await
}
}
type Terminal = Arc<dyn Fn(&Error) -> bool + Send + Sync>;
pub struct ClientBuilder {
url: String,
transport: Option<Arc<dyn Transport>>,
protocols: Vec<Arc<dyn Protocol>>,
headers: HeaderMap,
invalid_header: Option<Error>,
header_provider: Option<Arc<dyn HeaderProvider>>,
stop_on_error: Option<Terminal>,
stale_after: Duration,
subscribe_retry: Duration,
initial_backoff: Duration,
longest_backoff: Duration,
max_attempts: u32,
message_buffer: usize,
}
impl ClientBuilder {
pub(crate) fn new(url: String) -> ClientBuilder {
ClientBuilder {
url,
transport: None,
protocols: vec![Arc::new(V1Json)],
headers: HeaderMap::new(),
invalid_header: None,
header_provider: None,
stop_on_error: None,
stale_after: Duration::from_secs(6),
subscribe_retry: Duration::from_millis(500),
initial_backoff: Duration::from_secs(1),
longest_backoff: Duration::from_secs(30),
max_attempts: 0,
message_buffer: 64,
}
}
pub fn transport(mut self, transport: impl Transport + 'static) -> ClientBuilder {
self.transport = Some(Arc::new(transport));
self
}
pub fn protocols(mut self, protocols: Vec<Arc<dyn Protocol>>) -> ClientBuilder {
self.protocols = protocols;
self
}
pub fn prefer_protocols(mut self, protocols: Vec<Arc<dyn Protocol>>) -> ClientBuilder {
let mut preferred = protocols;
preferred.append(&mut self.protocols);
self.protocols = preferred;
self
}
pub fn header(mut self, name: &str, value: &str) -> ClientBuilder {
match parse_header(name, value) {
Ok((name, value)) => {
self.headers.insert(name, value);
}
Err(error) => {
self.invalid_header.get_or_insert(error);
}
}
self
}
pub fn cookie(self, cookie: &str) -> ClientBuilder {
self.header(COOKIE.as_str(), cookie)
}
pub fn origin(self, origin: &str) -> ClientBuilder {
self.header(ORIGIN.as_str(), origin)
}
pub fn header_provider(mut self, provider: impl HeaderProvider + 'static) -> ClientBuilder {
self.header_provider = Some(Arc::new(provider));
self
}
pub fn stop_on_error(
mut self,
terminal: impl Fn(&Error) -> bool + Send + Sync + 'static,
) -> ClientBuilder {
self.stop_on_error = Some(Arc::new(terminal));
self
}
pub fn stale_after(mut self, after: Duration) -> ClientBuilder {
self.stale_after = after;
self
}
pub fn backoff(mut self, initial: Duration, longest: Duration) -> ClientBuilder {
self.initial_backoff = initial;
self.longest_backoff = longest;
self
}
pub fn max_attempts(mut self, attempts: u32) -> ClientBuilder {
self.max_attempts = attempts;
self
}
pub fn subscribe_retry(mut self, retry: Duration) -> ClientBuilder {
self.subscribe_retry = retry;
self
}
pub fn message_buffer(mut self, messages: usize) -> ClientBuilder {
self.message_buffer = messages;
self
}
pub fn build(mut self) -> Result<Client, Error> {
if let Some(error) = self.invalid_header {
return Err(error);
}
let transport = match self.transport.take() {
Some(transport) => transport,
None => default_transport()?,
};
self.assume_origin();
Ok(Client::new(Config {
url: self.url,
transport,
protocols: self.protocols,
headers: self.headers,
header_provider: self.header_provider,
stop_on_error: self.stop_on_error,
stale_after: self.stale_after,
subscribe_retry: self.subscribe_retry,
initial_backoff: self.initial_backoff,
longest_backoff: self.longest_backoff,
max_attempts: self.max_attempts,
message_buffer: self.message_buffer,
}))
}
fn assume_origin(&mut self) {
if !self.headers.contains_key(ORIGIN)
&& let Some(origin) = origin_of(&self.url)
&& let Ok(origin) = HeaderValue::from_str(&origin)
{
self.headers.insert(ORIGIN, origin);
}
}
}
#[cfg(feature = "websocket")]
#[allow(
clippy::unnecessary_wraps,
reason = "the other half of this pair, without the feature, has nothing to hand back"
)]
fn default_transport() -> Result<Arc<dyn Transport>, Error> {
Ok(Arc::new(crate::websocket::WebSocketTransport::default()))
}
#[cfg(not(feature = "websocket"))]
fn default_transport() -> Result<Arc<dyn Transport>, Error> {
Err(Error::NoTransport)
}
fn parse_header(name: &str, value: &str) -> Result<(HeaderName, HeaderValue), Error> {
let invalid = |source: http::Error| Error::InvalidHeader {
name: name.to_string(),
source: Arc::new(source),
};
let name = HeaderName::from_bytes(name.as_bytes()).map_err(|error| invalid(error.into()))?;
let value = HeaderValue::from_str(value).map_err(|error| invalid(error.into()))?;
Ok((name, value))
}
fn origin_of(url: &str) -> Option<String> {
let endpoint = Url::parse(url).ok()?;
let host = endpoint.host_str()?;
let authority = match endpoint.port() {
Some(port) => format!("{host}:{port}"),
None => host.to_string(),
};
match endpoint.scheme() {
"wss" | "https" => Some(format!("https://{authority}")),
"ws" | "http" => Some(format!("http://{authority}")),
_ => None,
}
}
pub(crate) struct Config {
pub(crate) url: String,
pub(crate) transport: Arc<dyn Transport>,
pub(crate) protocols: Vec<Arc<dyn Protocol>>,
pub(crate) headers: HeaderMap,
pub(crate) header_provider: Option<Arc<dyn HeaderProvider>>,
pub(crate) stop_on_error: Option<Terminal>,
pub(crate) stale_after: Duration,
pub(crate) subscribe_retry: Duration,
pub(crate) initial_backoff: Duration,
pub(crate) longest_backoff: Duration,
pub(crate) max_attempts: u32,
pub(crate) message_buffer: usize,
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "a test that can't have what it asked for has nothing left to assert"
)]
mod tests {
use super::*;
#[test]
fn the_origin_follows_the_cable_url() {
let origins = [
(
"wss://cable.example.com/cable",
Some("https://cable.example.com"),
),
(
"ws://cable.example.com:3000/cable",
Some("http://cable.example.com:3000"),
),
(
"wss://cable.example.com:8443/cable",
Some("https://cable.example.com:8443"),
),
("ftp://cable.example.com/cable", None),
("not a url", None),
];
for (url, origin) in origins {
assert_eq!(origin.map(str::to_string), origin_of(url), "{url}");
}
}
#[cfg(not(feature = "websocket"))]
#[test]
fn without_the_websocket_feature_a_transport_is_required() {
assert!(matches!(
Client::builder("wss://cable.example.com/cable").build(),
Err(Error::NoTransport)
));
}
#[cfg(feature = "websocket")]
#[test]
fn the_websocket_transport_is_the_default() {
assert!(
Client::builder("wss://cable.example.com/cable")
.build()
.is_ok()
);
}
}