Skip to main content

nautilus_betfair/stream/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Betfair stream client error types.
17
18/// Represents stream client errors for the Betfair adapter.
19#[derive(Debug, Clone, thiserror::Error)]
20pub enum BetfairStreamError {
21    /// Failed to establish a connection.
22    #[error("Connection failed: {0}")]
23    ConnectionFailed(String),
24    /// Stream authentication failed.
25    #[error("Authentication failed: {0}")]
26    AuthenticationFailed(String),
27    /// Stream protocol error (unexpected message format).
28    #[error("Protocol error: {0}")]
29    ProtocolError(String),
30    /// JSON serialization/deserialization error.
31    #[error("JSON error: {0}")]
32    JsonError(String),
33    /// Connection or read timeout.
34    #[error("Timeout: {0}")]
35    Timeout(String),
36    /// Connection was lost.
37    #[error("Disconnected: {0}")]
38    Disconnected(String),
39}
40
41impl From<serde_json::Error> for BetfairStreamError {
42    fn from(error: serde_json::Error) -> Self {
43        Self::JsonError(error.to_string())
44    }
45}
46
47/// Determines if a failed book recovery attempt should retry within its budget.
48///
49/// A full writer queue and a missed snapshot deadline are transient. A closed client, a missing
50/// subscription, and authentication or serialization failures do not clear on their own.
51pub(crate) fn should_retry_stream_error(error: &BetfairStreamError) -> bool {
52    matches!(
53        error,
54        BetfairStreamError::ConnectionFailed(_) | BetfairStreamError::Timeout(_)
55    )
56}
57
58#[cfg(test)]
59mod tests {
60    use rstest::rstest;
61
62    use super::*;
63
64    #[rstest]
65    #[case(
66        BetfairStreamError::ConnectionFailed("refused".to_string()),
67        "Connection failed: refused"
68    )]
69    #[case(
70        BetfairStreamError::AuthenticationFailed("invalid token".to_string()),
71        "Authentication failed: invalid token"
72    )]
73    #[case(
74        BetfairStreamError::ProtocolError("bad frame".to_string()),
75        "Protocol error: bad frame"
76    )]
77    #[case(
78        BetfairStreamError::JsonError("parse error".to_string()),
79        "JSON error: parse error"
80    )]
81    #[case(
82        BetfairStreamError::Timeout("read".to_string()),
83        "Timeout: read"
84    )]
85    #[case(
86        BetfairStreamError::Disconnected("reset".to_string()),
87        "Disconnected: reset"
88    )]
89    fn test_display(#[case] error: BetfairStreamError, #[case] expected: &str) {
90        assert_eq!(error.to_string(), expected);
91    }
92
93    #[rstest]
94    #[case::queue_full(BetfairStreamError::ConnectionFailed("full".to_string()), true)]
95    #[case::timeout(BetfairStreamError::Timeout("snapshot".to_string()), true)]
96    #[case::closed(BetfairStreamError::Disconnected("closed".to_string()), false)]
97    #[case::auth(BetfairStreamError::AuthenticationFailed("token".to_string()), false)]
98    #[case::protocol(BetfairStreamError::ProtocolError("no subscription".to_string()), false)]
99    #[case::json(BetfairStreamError::JsonError("encode".to_string()), false)]
100    fn test_should_retry_stream_error(#[case] error: BetfairStreamError, #[case] expected: bool) {
101        assert_eq!(should_retry_stream_error(&error), expected);
102    }
103
104    #[rstest]
105    fn test_from_serde_error() {
106        let json_err = serde_json::from_str::<String>("bad").unwrap_err();
107        let err: BetfairStreamError = json_err.into();
108        assert!(matches!(err, BetfairStreamError::JsonError(_)));
109    }
110}