use std::{fmt, sync::Arc, time::Duration};
use super::{Heartbeat, StreamQuote};
use serde::Deserialize;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WebSocketCluster {
Stocks,
Crypto,
Forex,
}
impl WebSocketCluster {
pub const fn endpoint(self) -> &'static str {
match self {
Self::Stocks => "wss://websockets.financialmodelingprep.com",
Self::Crypto => "wss://crypto.financialmodelingprep.com",
Self::Forex => "wss://forex.financialmodelingprep.com",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ControlEvent {
pub event: String,
pub status: Option<u16>,
pub message: Option<String>,
pub json: String,
}
pub type LoginConfirmation =
Arc<dyn Fn(&ControlEvent) -> Result<bool, WebSocketError> + Send + Sync>;
#[derive(Clone)]
pub struct WebSocketConfig {
pub(super) handshake_timeout: Duration,
pub(super) receive_timeout: Duration,
pub(super) connect_attempts: u32,
pub(super) retry_delay: Duration,
pub(super) confirm_login: LoginConfirmation,
}
impl fmt::Debug for WebSocketConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WebSocketConfig")
.field("handshake_timeout", &self.handshake_timeout)
.field("receive_timeout", &self.receive_timeout)
.field("connect_attempts", &self.connect_attempts)
.field("retry_delay", &self.retry_delay)
.finish_non_exhaustive()
}
}
impl WebSocketConfig {
pub fn new(
handshake_timeout: Duration,
receive_timeout: Duration,
connect_attempts: u32,
retry_delay: Duration,
confirm_login: impl Fn(&ControlEvent) -> Result<bool, WebSocketError> + Send + Sync + 'static,
) -> Result<Self, WebSocketError> {
if handshake_timeout.is_zero() || receive_timeout.is_zero() || connect_attempts == 0 {
return Err(WebSocketError::Configuration {
reason: "handshake_timeout, receive_timeout, and connect_attempts must be positive"
.to_owned(),
});
}
Ok(Self {
handshake_timeout,
receive_timeout,
connect_attempts,
retry_delay,
confirm_login: Arc::new(confirm_login),
})
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct TradeUpdate {
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "t")]
pub timestamp: u64,
#[serde(rename = "lp")]
pub price: f64,
#[serde(rename = "ls")]
pub size: f64,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct QuoteUpdate {
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "t")]
pub timestamp: u64,
#[serde(rename = "ap")]
pub ask_price: f64,
#[serde(rename = "as")]
pub ask_size: f64,
#[serde(rename = "bp")]
pub bid_price: f64,
#[serde(rename = "bs")]
pub bid_size: f64,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(tag = "type")]
pub enum MarketUpdate {
#[serde(rename = "T")]
Trade(TradeUpdate),
#[serde(rename = "Q")]
Quote(QuoteUpdate),
#[serde(rename = "B")]
Break(TradeUpdate),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloseInfo {
pub code: Option<u16>,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WebSocketEvent {
Market(MarketUpdate),
StreamQuote(Box<StreamQuote>),
Heartbeat(Heartbeat),
Control(ControlEvent),
Pong(Vec<u8>),
Closed(CloseInfo),
}
#[derive(Debug, Error)]
pub enum WebSocketError {
#[error("invalid WebSocket configuration: {reason}")]
Configuration {
reason: String,
},
#[error(
"no explicit WebSocket entitlement configured for {cluster:?}; confirm the cluster grant with FMP"
)]
Entitlement {
cluster: WebSocketCluster,
},
#[error(
"no explicit WebSocket entitlement configured for stream {stream}; confirm the stream grant with FMP"
)]
StreamEntitlement {
stream: String,
},
#[error("WebSocket {operation} timed out for {endpoint}")]
Timeout {
operation: &'static str,
endpoint: String,
},
#[error("WebSocket {operation} failed for {endpoint}: {reason}")]
Transport {
operation: &'static str,
endpoint: String,
reason: String,
},
#[error("FMP WebSocket rejected {event} at {endpoint} (status {status:?}): {body}")]
Server {
event: String,
status: Option<u16>,
endpoint: String,
body: String,
},
#[error("invalid WebSocket response at {endpoint}: {reason}; body: {body}")]
Protocol {
endpoint: String,
reason: String,
body: String,
},
#[error(
"WebSocket connection is closed at {endpoint}; reconnect explicitly before issuing commands"
)]
Closed {
endpoint: String,
},
}