1use rustrade_integration::{Validator, error::SocketError};
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
41#[serde(tag = "event", rename_all = "lowercase")]
42pub enum BitfinexPlatformEvent {
43 #[serde(rename = "info")]
44 PlatformStatus(BitfinexPlatformStatus),
45 Subscribed(BitfinexSubResponse),
46 Error(BitfinexError),
47}
48
49impl Validator for BitfinexPlatformEvent {
50 type Error = SocketError;
51
52 fn validate(self) -> Result<Self, SocketError>
53 where
54 Self: Sized,
55 {
56 match &self {
57 BitfinexPlatformEvent::PlatformStatus(status) => match status.status {
58 Status::Operative => Ok(self),
59 Status::Maintenance => Err(SocketError::Subscribe(format!(
60 "exchange version: {} with server_id: {} is in maintenance mode",
61 status.api_version, status.server_id,
62 ))),
63 },
64 BitfinexPlatformEvent::Subscribed(_) => Ok(self),
65 BitfinexPlatformEvent::Error(BitfinexError {
66 msg,
67 code,
68 channel: Some(channel),
69 symbol: Some(symbol),
70 }) => Err(SocketError::Subscribe(format!(
71 "Bitfinex refused {channel} {symbol}: {msg} (code {code})"
72 ))),
73 BitfinexPlatformEvent::Error(error) => Err(SocketError::Subscribe(format!(
74 "received failure subscription response code: {} with message: {}",
75 error.code, error.msg,
76 ))),
77 }
78 }
79}
80
81#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
110pub struct BitfinexPlatformStatus {
111 #[serde(rename = "version")]
112 api_version: u8,
113 #[serde(rename = "serverId")]
114 server_id: String,
115 #[serde(rename = "platform")]
116 status: Status,
117}
118
119#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)]
125pub enum Status {
126 Maintenance,
127 Operative,
128}
129
130#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
155pub struct BitfinexSubResponse {
156 pub channel: String,
157 #[serde(rename = "symbol")]
158 pub market: String,
159 #[serde(rename = "chanId")]
160 pub channel_id: BitfinexChannelId,
161}
162
163#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
169pub struct BitfinexChannelId(pub u32);
170
171#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
183pub struct BitfinexError {
184 msg: String,
185 code: u32,
186 #[serde(default)]
188 channel: Option<String>,
189 #[serde(default)]
191 symbol: Option<String>,
192}
193
194impl<'de> Deserialize<'de> for Status {
195 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
196 where
197 D: serde::de::Deserializer<'de>,
198 {
199 #[derive(Deserialize)]
200 struct Outer {
201 #[serde(deserialize_with = "de_status_from_integer")]
202 status: Status,
203 }
204
205 let Outer { status } = Outer::deserialize(deserializer)?;
207
208 Ok(status)
209 }
210}
211
212fn de_status_from_integer<'de, D>(deserializer: D) -> Result<Status, D::Error>
218where
219 D: serde::de::Deserializer<'de>,
220{
221 match Deserialize::deserialize(deserializer)? {
222 0 => Ok(Status::Maintenance),
223 1 => Ok(Status::Operative),
224 other => Err(serde::de::Error::invalid_value(
225 serde::de::Unexpected::Signed(other),
226 &"0 or 1",
227 )),
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_de_bitfinex_platform_event() {
237 struct TestCase {
238 input: &'static str,
239 expected: Result<BitfinexPlatformEvent, SocketError>,
240 }
241
242 let cases = vec![
243 TestCase {
245 input: r#"{"event": "info", "version": 2, "serverId": "5b73a436-19ca-4a15-8160-9069bdd7f181", "platform": { "status": 1 }}"#,
246 expected: Ok(BitfinexPlatformEvent::PlatformStatus(
247 BitfinexPlatformStatus {
248 api_version: 2,
249 server_id: "5b73a436-19ca-4a15-8160-9069bdd7f181".to_string(),
250 status: Status::Operative,
251 },
252 )),
253 },
254 TestCase {
256 input: r#"{"event": "info", "version": 2, "serverId": "5b73a436-19ca-4a15-8160-9069bdd7f181", "platform": { "status": 0 }}"#,
257 expected: Ok(BitfinexPlatformEvent::PlatformStatus(
258 BitfinexPlatformStatus {
259 api_version: 2,
260 server_id: "5b73a436-19ca-4a15-8160-9069bdd7f181".to_string(),
261 status: Status::Maintenance,
262 },
263 )),
264 },
265 TestCase {
267 input: r#"{"event": "subscribed", "channel": "trades", "chanId": 2203, "symbol": "tBTCUSD", "pair": "BTCUSD"}"#,
268 expected: Ok(BitfinexPlatformEvent::Subscribed(BitfinexSubResponse {
269 channel: "trades".to_string(),
270 channel_id: BitfinexChannelId(2203),
271 market: "tBTCUSD".to_owned(),
272 })),
273 },
274 TestCase {
276 input: r#"{"event": "error", "msg": "Already subscribed", "code": 10202}"#,
277 expected: Ok(BitfinexPlatformEvent::Error(BitfinexError {
278 msg: "Already subscribed".to_owned(),
279 code: 10202,
280 channel: None,
281 symbol: None,
282 })),
283 },
284 TestCase {
286 input: r#"{"channel": "trades", "symbol": "tNOSUCH", "event": "error", "msg": "symbol: invalid", "code": 10300, "pair": "NOSUCH"}"#,
287 expected: Ok(BitfinexPlatformEvent::Error(BitfinexError {
288 msg: "symbol: invalid".to_owned(),
289 code: 10300,
290 channel: Some("trades".to_owned()),
291 symbol: Some("tNOSUCH".to_owned()),
292 })),
293 },
294 ];
295
296 for (index, test) in cases.into_iter().enumerate() {
297 let actual = serde_json::from_str::<BitfinexPlatformEvent>(test.input);
298 match (actual, test.expected) {
299 (Ok(actual), Ok(expected)) => {
300 assert_eq!(actual, expected, "TC{} failed", index)
301 }
302 (Err(_), Err(_)) => {
303 }
305 (actual, expected) => {
306 panic!(
308 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
309 );
310 }
311 }
312 }
313 }
314
315 #[test]
316 fn test_bitfinex_platform_sub_response_validate() {
317 struct TestCase {
318 input: BitfinexPlatformEvent,
319 expected: Result<BitfinexPlatformEvent, SocketError>,
320 }
321
322 let tests = vec![
323 TestCase {
324 input: BitfinexPlatformEvent::PlatformStatus(BitfinexPlatformStatus {
326 api_version: 2,
327 server_id: "server_id".to_string(),
328 status: Status::Maintenance,
329 }),
330 expected: Err(SocketError::Subscribe(format!(
331 "exchange version: {} with server_id: {} is in maintenance mode",
332 2, "server_id",
333 ))),
334 },
335 TestCase {
336 input: BitfinexPlatformEvent::PlatformStatus(BitfinexPlatformStatus {
338 api_version: 2,
339 server_id: "server_id".to_string(),
340 status: Status::Operative,
341 }),
342 expected: Ok(BitfinexPlatformEvent::PlatformStatus(
343 BitfinexPlatformStatus {
344 api_version: 2,
345 server_id: "server_id".to_string(),
346 status: Status::Operative,
347 },
348 )),
349 },
350 TestCase {
351 input: BitfinexPlatformEvent::Subscribed(BitfinexSubResponse {
353 channel: "channel".to_string(),
354 market: "market".to_string(),
355 channel_id: BitfinexChannelId(1),
356 }),
357 expected: Ok(BitfinexPlatformEvent::Subscribed(BitfinexSubResponse {
358 channel: "channel".to_string(),
359 market: "market".to_string(),
360 channel_id: BitfinexChannelId(1),
361 })),
362 },
363 TestCase {
364 input: BitfinexPlatformEvent::Error(BitfinexError {
366 msg: "error message".to_string(),
367 code: 0,
368 channel: None,
369 symbol: None,
370 }),
371 expected: Err(SocketError::Subscribe(format!(
372 "received failure subscription response code: {} with message: {}",
373 0, "error message",
374 ))),
375 },
376 ];
377
378 for (index, test) in tests.into_iter().enumerate() {
379 let actual = test.input.validate();
380 match (actual, test.expected) {
381 (Ok(actual), Ok(expected)) => {
382 assert_eq!(actual, expected, "TC{} failed", index)
383 }
384 (Err(_), Err(_)) => {
385 }
387 (actual, expected) => {
388 panic!(
390 "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
391 );
392 }
393 }
394 }
395 }
396}