use barter_integration::{Validator, error::SocketError};
use serde::{Deserialize, Serialize};
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
#[serde(tag = "event", rename_all = "lowercase")]
pub enum BitfinexPlatformEvent {
#[serde(rename = "info")]
PlatformStatus(BitfinexPlatformStatus),
Subscribed(BitfinexSubResponse),
Error(BitfinexError),
}
impl Validator for BitfinexPlatformEvent {
type Error = SocketError;
fn validate(self) -> Result<Self, SocketError>
where
Self: Sized,
{
match &self {
BitfinexPlatformEvent::PlatformStatus(status) => match status.status {
Status::Operative => Ok(self),
Status::Maintenance => Err(SocketError::Subscribe(format!(
"exchange version: {} with server_id: {} is in maintenance mode",
status.api_version, status.server_id,
))),
},
BitfinexPlatformEvent::Subscribed(_) => Ok(self),
BitfinexPlatformEvent::Error(error) => Err(SocketError::Subscribe(format!(
"received failure subscription response code: {} with message: {}",
error.code, error.msg,
))),
}
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
pub struct BitfinexPlatformStatus {
#[serde(rename = "version")]
api_version: u8,
#[serde(rename = "serverId")]
server_id: String,
#[serde(rename = "platform")]
status: Status,
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)]
pub enum Status {
Maintenance,
Operative,
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
pub struct BitfinexSubResponse {
pub channel: String,
#[serde(rename = "symbol")]
pub market: String,
#[serde(rename = "chanId")]
pub channel_id: BitfinexChannelId,
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
pub struct BitfinexChannelId(pub u32);
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
pub struct BitfinexError {
msg: String,
code: u32,
}
impl<'de> Deserialize<'de> for Status {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Outer {
#[serde(deserialize_with = "de_status_from_u8")]
status: Status,
}
let Outer { status } = Outer::deserialize(deserializer)?;
Ok(status)
}
}
fn de_status_from_u8<'de, D>(deserializer: D) -> Result<Status, D::Error>
where
D: serde::de::Deserializer<'de>,
{
match Deserialize::deserialize(deserializer)? {
0 => Ok(Status::Maintenance),
1 => Ok(Status::Operative),
other => Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Unsigned(other as u64),
&"0 or 1",
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_de_bitfinex_platform_event() {
struct TestCase {
input: &'static str,
expected: Result<BitfinexPlatformEvent, SocketError>,
}
let cases = vec![
TestCase {
input: r#"{"event": "info", "version": 2, "serverId": "5b73a436-19ca-4a15-8160-9069bdd7f181", "platform": { "status": 1 }}"#,
expected: Ok(BitfinexPlatformEvent::PlatformStatus(
BitfinexPlatformStatus {
api_version: 2,
server_id: "5b73a436-19ca-4a15-8160-9069bdd7f181".to_string(),
status: Status::Operative,
},
)),
},
TestCase {
input: r#"{"event": "info", "version": 2, "serverId": "5b73a436-19ca-4a15-8160-9069bdd7f181", "platform": { "status": 0 }}"#,
expected: Ok(BitfinexPlatformEvent::PlatformStatus(
BitfinexPlatformStatus {
api_version: 2,
server_id: "5b73a436-19ca-4a15-8160-9069bdd7f181".to_string(),
status: Status::Maintenance,
},
)),
},
TestCase {
input: r#"{"event": "subscribed", "channel": "trades", "chanId": 2203, "symbol": "tBTCUSD", "pair": "BTCUSD"}"#,
expected: Ok(BitfinexPlatformEvent::Subscribed(BitfinexSubResponse {
channel: "trades".to_string(),
channel_id: BitfinexChannelId(2203),
market: "tBTCUSD".to_owned(),
})),
},
TestCase {
input: r#"{"event": "error", "msg": "Already subscribed", "code": 10202}"#,
expected: Ok(BitfinexPlatformEvent::Error(BitfinexError {
msg: "Already subscribed".to_owned(),
code: 10202,
})),
},
];
for (index, test) in cases.into_iter().enumerate() {
let actual = serde_json::from_str::<BitfinexPlatformEvent>(test.input);
match (actual, test.expected) {
(Ok(actual), Ok(expected)) => {
assert_eq!(actual, expected, "TC{} failed", index)
}
(Err(_), Err(_)) => {
}
(actual, expected) => {
panic!(
"TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
);
}
}
}
}
#[test]
fn test_bitfinex_platform_sub_response_validate() {
struct TestCase {
input: BitfinexPlatformEvent,
expected: Result<BitfinexPlatformEvent, SocketError>,
}
let tests = vec![
TestCase {
input: BitfinexPlatformEvent::PlatformStatus(BitfinexPlatformStatus {
api_version: 2,
server_id: "server_id".to_string(),
status: Status::Maintenance,
}),
expected: Err(SocketError::Subscribe(format!(
"exchange version: {} with server_id: {} is in maintenance mode",
2, "server_id",
))),
},
TestCase {
input: BitfinexPlatformEvent::PlatformStatus(BitfinexPlatformStatus {
api_version: 2,
server_id: "server_id".to_string(),
status: Status::Operative,
}),
expected: Ok(BitfinexPlatformEvent::PlatformStatus(
BitfinexPlatformStatus {
api_version: 2,
server_id: "server_id".to_string(),
status: Status::Operative,
},
)),
},
TestCase {
input: BitfinexPlatformEvent::Subscribed(BitfinexSubResponse {
channel: "channel".to_string(),
market: "market".to_string(),
channel_id: BitfinexChannelId(1),
}),
expected: Ok(BitfinexPlatformEvent::Subscribed(BitfinexSubResponse {
channel: "channel".to_string(),
market: "market".to_string(),
channel_id: BitfinexChannelId(1),
})),
},
TestCase {
input: BitfinexPlatformEvent::Error(BitfinexError {
msg: "error message".to_string(),
code: 0,
}),
expected: Err(SocketError::Subscribe(format!(
"received failure subscription response code: {} with message: {}",
0, "error message",
))),
},
];
for (index, test) in tests.into_iter().enumerate() {
let actual = test.input.validate();
match (actual, test.expected) {
(Ok(actual), Ok(expected)) => {
assert_eq!(actual, expected, "TC{} failed", index)
}
(Err(_), Err(_)) => {
}
(actual, expected) => {
panic!(
"TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
);
}
}
}
}
}