Skip to main content

sonos_api/
error.rs

1use soap_client::SoapError;
2use thiserror::Error;
3
4/// High-level API errors for Sonos operations
5///
6/// This enum provides domain-specific error types that abstract away the underlying
7/// SOAP communication details and provide meaningful error information for common
8/// failure scenarios when controlling Sonos devices.
9#[derive(Debug, Error)]
10pub enum ApiError {
11    /// Network communication error
12    ///
13    /// This error occurs when there are network-level issues communicating
14    /// with the device, such as connection timeouts, DNS resolution failures,
15    /// or the device being unreachable.
16    #[error("Network error: {0}")]
17    NetworkError(String),
18
19    /// Response parsing error
20    ///
21    /// This error occurs when the device returns a valid response but
22    /// the response content cannot be parsed into the expected format.
23    /// This covers XML parsing errors, unexpected response formats, and event parsing issues.
24    #[error("Parse error: {0}")]
25    ParseError(String),
26
27    /// SOAP fault returned by device
28    ///
29    /// This error occurs when the device returns a SOAP fault response,
30    /// indicating that the request was malformed or the operation failed.
31    #[error("SOAP fault: error code {0}")]
32    SoapFault(u16),
33
34    /// Invalid parameter value
35    ///
36    /// This error is returned when an operation parameter has an invalid value.
37    /// This covers volume out of range, invalid device states, malformed URLs, etc.
38    #[error("Invalid parameter: {0}")]
39    InvalidParameter(String),
40
41    /// Subscription operation failed
42    ///
43    /// This error occurs when UPnP subscription operations (create, renew, unsubscribe) fail.
44    /// This covers subscription failures, renewal failures, expired subscriptions, etc.
45    #[error("Subscription error: {0}")]
46    SubscriptionError(String),
47
48    /// Device operation error
49    ///
50    /// This error covers device-specific issues like not being a group coordinator,
51    /// unsupported operations, or invalid device states.
52    #[error("Device error: {0}")]
53    DeviceError(String),
54}
55
56impl ApiError {
57    /// Create a subscription expired error (used by subscription management)
58    pub fn subscription_expired() -> Self {
59        Self::SubscriptionError("Subscription expired".to_string())
60    }
61}
62
63/// Type alias for results that can return an ApiError
64pub type Result<T> = std::result::Result<T, ApiError>;
65
66/// Convert from SoapError to ApiError
67impl From<SoapError> for ApiError {
68    fn from(error: SoapError) -> Self {
69        match error {
70            SoapError::Network(msg) => ApiError::NetworkError(msg),
71            SoapError::Parse(msg) => ApiError::ParseError(msg),
72            SoapError::Fault(code) => ApiError::SoapFault(code),
73        }
74    }
75}
76
77/// Convert from ValidationError to ApiError
78impl From<crate::operation::ValidationError> for ApiError {
79    fn from(validation_error: crate::operation::ValidationError) -> Self {
80        match validation_error {
81            crate::operation::ValidationError::InvalidValue {
82                parameter,
83                value,
84                reason,
85            } => ApiError::InvalidParameter(format!(
86                "Invalid value '{value}' for parameter '{parameter}': {reason}"
87            )),
88            crate::operation::ValidationError::RangeError {
89                parameter,
90                value,
91                min,
92                max,
93            } => ApiError::InvalidParameter(format!(
94                "Parameter '{parameter}' value {value} is out of range [{min}, {max}]"
95            )),
96            crate::operation::ValidationError::Custom { parameter, message } => {
97                ApiError::InvalidParameter(format!("Parameter '{parameter}': {message}"))
98            }
99            crate::operation::ValidationError::MissingParameter { parameter } => {
100                ApiError::InvalidParameter(format!("Required parameter '{parameter}' is missing"))
101            }
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_subscription_expired() {
112        let error = ApiError::subscription_expired();
113        assert!(matches!(error, ApiError::SubscriptionError(_)));
114        let error_str = format!("{error}");
115        assert!(error_str.contains("expired"));
116    }
117
118    #[test]
119    fn test_soap_error_conversion() {
120        let soap_error = SoapError::Network("connection timeout".to_string());
121        let api_error: ApiError = soap_error.into();
122        assert!(matches!(api_error, ApiError::NetworkError(_)));
123
124        let soap_error = SoapError::Parse("invalid XML".to_string());
125        let api_error: ApiError = soap_error.into();
126        assert!(matches!(api_error, ApiError::ParseError(_)));
127
128        let soap_error = SoapError::Fault(500);
129        let api_error: ApiError = soap_error.into();
130        assert!(matches!(api_error, ApiError::SoapFault(500)));
131    }
132
133    #[test]
134    fn test_error_display() {
135        let network_err = ApiError::NetworkError("connection failed".to_string());
136        assert_eq!(format!("{network_err}"), "Network error: connection failed");
137
138        let parse_err = ApiError::ParseError("invalid XML".to_string());
139        assert_eq!(format!("{parse_err}"), "Parse error: invalid XML");
140
141        let soap_fault = ApiError::SoapFault(500);
142        assert_eq!(format!("{soap_fault}"), "SOAP fault: error code 500");
143    }
144}