use super::channel::KrakenChannel;
use crate::{Identifier, exchange::ExchangeSub};
use rustrade_integration::subscription::SubscriptionId;
use serde::{
Deserialize, Serialize,
de::{self, DeserializeOwned, IgnoredAny, MapAccess, Visitor},
};
use smol_str::SmolStr;
use std::marker::PhantomData;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize)]
#[serde(untagged)]
pub enum KrakenMessage<T> {
Data(T),
Event(KrakenEvent),
}
impl<T> Identifier<Option<SubscriptionId>> for KrakenMessage<T>
where
T: Identifier<Option<SubscriptionId>>,
{
fn id(&self) -> Option<SubscriptionId> {
match self {
Self::Data(data) => data.id(),
Self::Event(_) => None,
}
}
}
impl<'de, T> Deserialize<'de> for KrakenMessage<KrakenData<T>>
where
T: KrakenChannelData,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(KrakenMessageVisitor(PhantomData))
}
}
struct KrakenMessageVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for KrakenMessageVisitor<T>
where
T: KrakenChannelData,
{
type Value = KrakenMessage<KrakenData<T>>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "a Kraken WebSocket v2 message")
}
fn visit_map<Map>(self, mut map: Map) -> Result<Self::Value, Map::Error>
where
Map: MapAccess<'de>,
{
let mut channel: Option<SmolStr> = None;
let mut data: Option<Vec<T>> = None;
let mut data_unread: Option<serde_json::Value> = None;
let mut method: Option<SmolStr> = None;
let mut error: Option<String> = None;
while let Some(key) = map.next_key::<SmolStr>()? {
match key.as_str() {
"channel" => channel = Some(map.next_value()?),
"data" if channel.as_deref() == Some(T::CHANNEL.as_ref()) => {
data = Some(map.next_value()?)
}
"data" if channel.is_none() => data_unread = Some(map.next_value()?),
"method" => method = Some(map.next_value()?),
"error" => error = Some(map.next_value()?),
_ => {
map.next_value::<IgnoredAny>()?;
}
}
}
match (channel, method) {
(Some(channel), _) if channel == T::CHANNEL.as_ref() => {
let data = match (data, data_unread) {
(Some(data), _) => data,
(None, Some(unread)) => {
Vec::<T>::deserialize(unread).map_err(de::Error::custom)?
}
(None, None) => return Err(de::Error::missing_field("data")),
};
KrakenData::new(data)
.map(KrakenMessage::Data)
.map_err(de::Error::custom)
}
(Some(channel), _) => match channel.as_str() {
"heartbeat" => Ok(KrakenMessage::Event(KrakenEvent::Heartbeat)),
"status" => Ok(KrakenMessage::Event(KrakenEvent::Status)),
other => Err(de::Error::custom(format_args!(
"unexpected Kraken channel {other} on a {} stream",
T::CHANNEL.as_ref()
))),
},
(None, Some(method)) => match error {
None => Ok(KrakenMessage::Event(KrakenEvent::Response { method })),
Some(error) => Err(de::Error::custom(format_args!(
"Kraken refused a request ({method}): {error}"
))),
},
(None, None) => Err(de::Error::custom(
"Kraken message names neither a channel nor a method",
)),
}
}
}
pub trait KrakenChannelData: DeserializeOwned {
const CHANNEL: KrakenChannel;
fn symbol(&self) -> &str;
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize)]
pub struct KrakenData<T> {
pub subscription_id: Option<SubscriptionId>,
pub data: Vec<T>,
}
impl<T> KrakenData<T>
where
T: KrakenChannelData,
{
fn new(data: Vec<T>) -> Result<Self, String> {
let subscription_id = match data.split_first() {
None => None,
Some((first, rest)) => {
if let Some(other) = rest.iter().find(|item| item.symbol() != first.symbol()) {
return Err(format!(
"Kraken {} message mixes symbols {} and {}",
T::CHANNEL.as_ref(),
first.symbol(),
other.symbol()
));
}
Some(ExchangeSub::from((T::CHANNEL, first.symbol())).id())
}
};
Ok(Self {
subscription_id,
data,
})
}
}
impl<T> Identifier<Option<SubscriptionId>> for KrakenData<T> {
fn id(&self) -> Option<SubscriptionId> {
self.subscription_id.clone()
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)]
pub enum KrakenEvent {
Heartbeat,
Status,
Response { method: SmolStr },
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
pub struct KrakenError {
#[serde(rename = "error")]
pub message: String,
#[serde(default)]
pub symbol: Option<String>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use crate::exchange::kraken::trade::KrakenTrades;
const TIMES: &str =
r#""time_in":"2018-11-12T21:14:59.545897Z","time_out":"2018-11-12T21:14:59.545901Z""#;
fn trade(symbol: &str) -> String {
format!(
r#"{{"symbol":"{symbol}","side":"buy","price":1.5,"qty":2,"ord_type":"limit","trade_id":1,"timestamp":"2018-11-12T21:14:59.545897Z"}}"#
)
}
fn parse_error(message: &str) -> String {
serde_json::from_str::<KrakenTrades>(message)
.unwrap_err()
.to_string()
}
#[test]
fn heartbeat_and_status_are_events() {
let heartbeat: KrakenTrades = serde_json::from_str(r#"{"channel":"heartbeat"}"#).unwrap();
assert_eq!(heartbeat, KrakenMessage::Event(KrakenEvent::Heartbeat));
let status: KrakenTrades = serde_json::from_str(
r#"{"channel":"status","type":"update","data":[{"api_version":"v2","connection_id":1,"system":"online","version":"2.0.0"}]}"#,
)
.unwrap();
assert_eq!(status, KrakenMessage::Event(KrakenEvent::Status));
}
#[test]
fn a_successful_response_is_an_event() {
let pong: KrakenTrades =
serde_json::from_str(&format!(r#"{{"method":"pong","req_id":1,{TIMES}}}"#)).unwrap();
assert_eq!(
pong,
KrakenMessage::Event(KrakenEvent::Response {
method: "pong".into()
})
);
}
#[test]
fn a_refused_request_is_an_error_quoting_kraken() {
let error = parse_error(&format!(
r#"{{"error":"Malformed request","method":"error","success":false,{TIMES}}}"#
));
assert!(
error.contains("Kraken refused a request (error): Malformed request"),
"{error}"
);
}
#[test]
fn a_message_mixing_symbols_is_refused_naming_them() {
let error = parse_error(&format!(
r#"{{"channel":"trade","type":"update","data":[{},{}]}}"#,
trade("BTC/USD"),
trade("ETH/USD")
));
assert!(
error.contains("Kraken trade message mixes symbols BTC/USD and ETH/USD"),
"{error}"
);
}
#[test]
fn another_data_channel_is_refused_naming_it() {
let error = parse_error(
r#"{"channel":"ticker","type":"update","data":[{"symbol":"BTC/USD","bid":1.0,"bid_qty":1.0,"ask":2.0,"ask_qty":1.0,"timestamp":"2018-11-12T21:14:59.545897Z"}]}"#,
);
assert!(
error.contains("unexpected Kraken channel ticker on a trade stream"),
"{error}"
);
}
#[test]
fn an_item_that_does_not_parse_keeps_its_reason() {
let error = parse_error(
r#"{"channel":"trade","type":"update","data":[{"symbol":"BTC/USD","side":"buy","price":1.5,"qty":2,"ord_type":"limit","timestamp":"2018-11-12T21:14:59.545897Z"}]}"#,
);
assert!(error.contains("missing field `trade_id`"), "{error}");
}
#[test]
fn a_malformed_envelope_is_refused_naming_why() {
let error = parse_error("{}");
assert!(
error.contains("Kraken message names neither a channel nor a method"),
"{error}"
);
let error = parse_error(r#"{"channel":"trade","type":"update"}"#);
assert!(error.contains("missing field `data`"), "{error}");
}
#[test]
fn data_before_another_channel_is_not_read_as_items() {
let status: KrakenTrades =
serde_json::from_str(r#"{"data":[{"system":"online"}],"channel":"status"}"#).unwrap();
assert_eq!(status, KrakenMessage::Event(KrakenEvent::Status));
}
#[test]
fn data_before_its_channel_still_parses() {
let trades: KrakenTrades = serde_json::from_str(&format!(
r#"{{"data":[{}],"type":"update","channel":"trade"}}"#,
trade("BTC/USD")
))
.unwrap();
let KrakenMessage::Data(trades) = trades else {
panic!("expected data, got {trades:?}");
};
assert_eq!(
trades.subscription_id,
Some(SubscriptionId::from("trade|BTC/USD"))
);
assert_eq!(trades.data.len(), 1);
assert_eq!(trades.data[0].symbol, "BTC/USD");
}
}