Skip to main content

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