barter_data/exchange/coinbase/
subscription.rs

1use barter_integration::{Validator, error::SocketError};
2use serde::{Deserialize, Serialize};
3
4/// [`Coinbase`](super::Coinbase) WebSocket subscription response.
5///
6/// ### Raw Payload Examples
7/// See docs: <https://docs.cloud.coinbase.com/exchange/docs/websocket-overview#subscribe>
8/// #### Subscripion Success
9/// ```json
10/// {
11///     "type":"subscriptions",
12///     "channels":[
13///         {"name":"matches","product_ids":["BTC-USD", "ETH-USD"]}
14///     ]
15/// }
16/// ```
17///
18/// #### Subscription Failure
19/// ```json
20/// {
21///     "type":"error",
22///     "message":"Failed to subscribe",
23///     "reason":"GIBBERISH-USD is not a valid product"
24/// }
25/// ```
26#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
27#[serde(tag = "type", rename_all = "lowercase")]
28pub enum CoinbaseSubResponse {
29    #[serde(alias = "subscriptions")]
30    Subscribed {
31        channels: Vec<CoinbaseChannels>,
32    },
33    Error {
34        reason: String,
35    },
36}
37
38/// Communicates the [`Coinbase`](super::Coinbase) product_ids (eg/ "ETH-USD") associated with
39/// a successful channel (eg/ "matches") subscription.
40///
41/// See [`CoinbaseSubResponse`] for full raw paylaod examples.
42///
43/// See docs: <https://docs.cloud.coinbase.com/exchange/docs/websocket-overview#subscribe>
44#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
45pub struct CoinbaseChannels {
46    #[serde(alias = "name")]
47    pub channel: String,
48    pub product_ids: Vec<String>,
49}
50
51impl Validator for CoinbaseSubResponse {
52    fn validate(self) -> Result<Self, SocketError>
53    where
54        Self: Sized,
55    {
56        match &self {
57            CoinbaseSubResponse::Subscribed { .. } => Ok(self),
58            CoinbaseSubResponse::Error { reason } => Err(SocketError::Subscribe(format!(
59                "received failure subscription response: {reason}",
60            ))),
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    mod de {
70        use super::*;
71
72        #[test]
73        fn test_coinbase_sub_response() {
74            struct TestCase {
75                input: &'static str,
76                expected: Result<CoinbaseSubResponse, SocketError>,
77            }
78
79            let cases = vec![
80                TestCase {
81                    // TC0: input response is Subscribed
82                    input: r#"
83                    {
84                        "type":"subscriptions",
85                        "channels":[
86                            {"name":"matches","product_ids":["BTC-USD", "ETH-USD"]}
87                        ]
88                    }
89                    "#,
90                    expected: Ok(CoinbaseSubResponse::Subscribed {
91                        channels: vec![CoinbaseChannels {
92                            channel: "matches".to_string(),
93                            product_ids: vec!["BTC-USD".to_string(), "ETH-USD".to_string()],
94                        }],
95                    }),
96                },
97                TestCase {
98                    // TC1: input response is failed subscription
99                    input: r#"
100                    {
101                        "type":"error",
102                        "message":"Failed to subscribe",
103                        "reason":"GIBBERISH-USD is not a valid product"
104                    }
105                    "#,
106                    expected: Ok(CoinbaseSubResponse::Error {
107                        reason: "GIBBERISH-USD is not a valid product".to_string(),
108                    }),
109                },
110            ];
111
112            for (index, test) in cases.into_iter().enumerate() {
113                let actual = serde_json::from_str::<CoinbaseSubResponse>(test.input);
114                match (actual, test.expected) {
115                    (Ok(actual), Ok(expected)) => {
116                        assert_eq!(actual, expected, "TC{} failed", index)
117                    }
118                    (Err(_), Err(_)) => {
119                        // Test passed
120                    }
121                    (actual, expected) => {
122                        // Test failed
123                        panic!(
124                            "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
125                        );
126                    }
127                }
128            }
129        }
130    }
131
132    #[test]
133    fn test_validate_coinbase_sub_response() {
134        struct TestCase {
135            input_response: CoinbaseSubResponse,
136            is_valid: bool,
137        }
138
139        let cases = vec![
140            TestCase {
141                // TC0: input response is successful subscription
142                input_response: CoinbaseSubResponse::Subscribed {
143                    channels: vec![CoinbaseChannels {
144                        channel: "matches".to_string(),
145                        product_ids: vec!["BTC-USD".to_string(), "ETH-USD".to_string()],
146                    }],
147                },
148                is_valid: true,
149            },
150            TestCase {
151                // TC1: input response is failed subscription
152                input_response: CoinbaseSubResponse::Error {
153                    reason: "GIBBERISH-USD is not a valid product".to_string(),
154                },
155                is_valid: false,
156            },
157        ];
158
159        for (index, test) in cases.into_iter().enumerate() {
160            let actual = test.input_response.validate().is_ok();
161            assert_eq!(actual, test.is_valid, "TestCase {} failed", index);
162        }
163    }
164}