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: {}",
60                reason
61            ))),
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    mod de {
71        use super::*;
72
73        #[test]
74        fn test_coinbase_sub_response() {
75            struct TestCase {
76                input: &'static str,
77                expected: Result<CoinbaseSubResponse, SocketError>,
78            }
79
80            let cases = vec![
81                TestCase {
82                    // TC0: input response is Subscribed
83                    input: r#"
84                    {
85                        "type":"subscriptions",
86                        "channels":[
87                            {"name":"matches","product_ids":["BTC-USD", "ETH-USD"]}
88                        ]
89                    }
90                    "#,
91                    expected: Ok(CoinbaseSubResponse::Subscribed {
92                        channels: vec![CoinbaseChannels {
93                            channel: "matches".to_string(),
94                            product_ids: vec!["BTC-USD".to_string(), "ETH-USD".to_string()],
95                        }],
96                    }),
97                },
98                TestCase {
99                    // TC1: input response is failed subscription
100                    input: r#"
101                    {
102                        "type":"error",
103                        "message":"Failed to subscribe",
104                        "reason":"GIBBERISH-USD is not a valid product"
105                    }
106                    "#,
107                    expected: Ok(CoinbaseSubResponse::Error {
108                        reason: "GIBBERISH-USD is not a valid product".to_string(),
109                    }),
110                },
111            ];
112
113            for (index, test) in cases.into_iter().enumerate() {
114                let actual = serde_json::from_str::<CoinbaseSubResponse>(test.input);
115                match (actual, test.expected) {
116                    (Ok(actual), Ok(expected)) => {
117                        assert_eq!(actual, expected, "TC{} failed", index)
118                    }
119                    (Err(_), Err(_)) => {
120                        // Test passed
121                    }
122                    (actual, expected) => {
123                        // Test failed
124                        panic!(
125                            "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
126                        );
127                    }
128                }
129            }
130        }
131    }
132
133    #[test]
134    fn test_validate_coinbase_sub_response() {
135        struct TestCase {
136            input_response: CoinbaseSubResponse,
137            is_valid: bool,
138        }
139
140        let cases = vec![
141            TestCase {
142                // TC0: input response is successful subscription
143                input_response: CoinbaseSubResponse::Subscribed {
144                    channels: vec![CoinbaseChannels {
145                        channel: "matches".to_string(),
146                        product_ids: vec!["BTC-USD".to_string(), "ETH-USD".to_string()],
147                    }],
148                },
149                is_valid: true,
150            },
151            TestCase {
152                // TC1: input response is failed subscription
153                input_response: CoinbaseSubResponse::Error {
154                    reason: "GIBBERISH-USD is not a valid product".to_string(),
155                },
156                is_valid: false,
157            },
158        ];
159
160        for (index, test) in cases.into_iter().enumerate() {
161            let actual = test.input_response.validate().is_ok();
162            assert_eq!(actual, test.is_valid, "TestCase {} failed", index);
163        }
164    }
165}