Skip to main content

crispy_xtream/
error.rs

1//! Error types for the Xtream API client.
2
3use thiserror::Error;
4
5/// Errors that can occur when interacting with an Xtream Codes API server.
6#[derive(Debug, Error)]
7pub enum XtreamError {
8    /// Authentication failed (invalid credentials, disabled account).
9    #[error("auth error: {0}")]
10    Auth(String),
11
12    /// Account expired or session invalid.
13    #[error("session expired: {0}")]
14    SessionExpired(String),
15
16    /// Server returned 429 Too Many Requests.
17    #[error("rate limited: retry after {retry_after_secs}s")]
18    RateLimited {
19        /// Suggested retry delay in seconds (from `Retry-After` header or default).
20        retry_after_secs: u64,
21    },
22
23    /// HTTP or network-level failure.
24    #[error("network error: {0}")]
25    Network(String),
26
27    /// Request timed out.
28    #[error("timeout: {0}")]
29    Timeout(String),
30
31    /// Server returned JSON we could not deserialize.
32    #[error("unexpected response: {0}")]
33    UnexpectedResponse(String),
34
35    /// The requested resource was not found (empty info array, null name, etc.).
36    #[error("not found: {0}")]
37    NotFound(String),
38
39    /// Invalid URL or configuration.
40    #[error("invalid url: {0}")]
41    InvalidUrl(String),
42}
43
44impl From<reqwest::Error> for XtreamError {
45    fn from(err: reqwest::Error) -> Self {
46        if err.is_timeout() {
47            return Self::Timeout(err.to_string());
48        }
49        if err.is_connect() {
50            return Self::Network(format!("connection failed: {err}"));
51        }
52        if err.is_decode() {
53            return Self::UnexpectedResponse(format!("decode error: {err}"));
54        }
55        Self::Network(err.to_string())
56    }
57}
58
59impl From<serde_json::Error> for XtreamError {
60    fn from(err: serde_json::Error) -> Self {
61        Self::UnexpectedResponse(format!("json parse error: {err}"))
62    }
63}
64
65impl From<url::ParseError> for XtreamError {
66    fn from(err: url::ParseError) -> Self {
67        Self::InvalidUrl(err.to_string())
68    }
69}
70
71impl From<XtreamError> for crispy_iptv_types::IptvError {
72    fn from(err: XtreamError) -> Self {
73        match err {
74            XtreamError::Auth(msg) => Self::Auth(msg),
75            XtreamError::SessionExpired(msg) => Self::SessionExpired(msg),
76            XtreamError::RateLimited { retry_after_secs } => Self::RateLimited { retry_after_secs },
77            XtreamError::Network(msg) => Self::Network(msg),
78            XtreamError::Timeout(msg) => Self::Timeout(0).into_network(msg),
79            XtreamError::UnexpectedResponse(msg) => Self::UnexpectedResponse(msg),
80            XtreamError::NotFound(msg) => Self::UnexpectedResponse(msg),
81            XtreamError::InvalidUrl(msg) => Self::InvalidUrl(msg),
82        }
83    }
84}
85
86/// Helper trait — not part of the public API.
87trait IntoNetwork {
88    fn into_network(self, msg: String) -> crispy_iptv_types::IptvError;
89}
90
91impl IntoNetwork for crispy_iptv_types::IptvError {
92    fn into_network(self, msg: String) -> crispy_iptv_types::IptvError {
93        crispy_iptv_types::IptvError::Network(msg)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn display_auth_error() {
103        let err = XtreamError::Auth("bad credentials".into());
104        assert_eq!(err.to_string(), "auth error: bad credentials");
105    }
106
107    #[test]
108    fn display_rate_limited() {
109        let err = XtreamError::RateLimited {
110            retry_after_secs: 30,
111        };
112        assert_eq!(err.to_string(), "rate limited: retry after 30s");
113    }
114
115    #[test]
116    fn from_serde_json_error() {
117        let raw = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
118        let err = XtreamError::from(raw);
119        assert!(matches!(err, XtreamError::UnexpectedResponse(_)));
120    }
121
122    #[test]
123    fn from_url_parse_error() {
124        let raw = url::Url::parse("://bad").unwrap_err();
125        let err = XtreamError::from(raw);
126        assert!(matches!(err, XtreamError::InvalidUrl(_)));
127    }
128
129    #[test]
130    fn converts_to_iptv_error() {
131        let err = XtreamError::Auth("test".into());
132        let iptv: crispy_iptv_types::IptvError = err.into();
133        assert!(matches!(iptv, crispy_iptv_types::IptvError::Auth(_)));
134    }
135}