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            // The device's `<errorDescription>` is intentionally dropped here:
73            // widening the public `SoapFault` variant to carry it would be a
74            // breaking change to this semver-checked crate. The description
75            // remains available on `SoapError::Fault` for internal consumers.
76            SoapError::Fault { code, .. } => ApiError::SoapFault(code),
77        }
78    }
79}
80
81/// Convert from ValidationError to ApiError
82impl From<crate::operation::ValidationError> for ApiError {
83    fn from(validation_error: crate::operation::ValidationError) -> Self {
84        match validation_error {
85            crate::operation::ValidationError::InvalidValue {
86                parameter,
87                value,
88                reason,
89            } => ApiError::InvalidParameter(format!(
90                "Invalid value '{value}' for parameter '{parameter}': {reason}"
91            )),
92            crate::operation::ValidationError::RangeError {
93                parameter,
94                value,
95                min,
96                max,
97            } => ApiError::InvalidParameter(format!(
98                "Parameter '{parameter}' value {value} is out of range [{min}, {max}]"
99            )),
100            crate::operation::ValidationError::Custom { parameter, message } => {
101                ApiError::InvalidParameter(format!("Parameter '{parameter}': {message}"))
102            }
103            crate::operation::ValidationError::MissingParameter { parameter } => {
104                ApiError::InvalidParameter(format!("Required parameter '{parameter}' is missing"))
105            }
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_subscription_expired() {
116        let error = ApiError::subscription_expired();
117        assert!(matches!(error, ApiError::SubscriptionError(_)));
118        let error_str = format!("{error}");
119        assert!(error_str.contains("expired"));
120    }
121
122    #[test]
123    fn test_soap_error_conversion() {
124        let soap_error = SoapError::Network("connection timeout".to_string());
125        let api_error: ApiError = soap_error.into();
126        assert!(matches!(api_error, ApiError::NetworkError(_)));
127
128        let soap_error = SoapError::Parse("invalid XML".to_string());
129        let api_error: ApiError = soap_error.into();
130        assert!(matches!(api_error, ApiError::ParseError(_)));
131
132        let soap_error = SoapError::fault(500);
133        let api_error: ApiError = soap_error.into();
134        assert!(matches!(api_error, ApiError::SoapFault(500)));
135
136        // A described fault still maps to the code; description is dropped.
137        let soap_error = SoapError::Fault {
138            code: 402,
139            description: Some("Invalid Args".to_string()),
140        };
141        let api_error: ApiError = soap_error.into();
142        assert!(matches!(api_error, ApiError::SoapFault(402)));
143    }
144
145    #[test]
146    fn test_error_display() {
147        let network_err = ApiError::NetworkError("connection failed".to_string());
148        assert_eq!(format!("{network_err}"), "Network error: connection failed");
149
150        let parse_err = ApiError::ParseError("invalid XML".to_string());
151        assert_eq!(format!("{parse_err}"), "Parse error: invalid XML");
152
153        let soap_fault = ApiError::SoapFault(500);
154        assert_eq!(format!("{soap_fault}"), "SOAP fault: error code 500");
155    }
156}