Skip to main content

sonos_cli/
errors.rs

1//! Error types for the sonos-cli application.
2
3use std::process::ExitCode;
4use thiserror::Error;
5
6use crate::diagnostics;
7
8/// Domain error type with recovery hints and exit codes.
9#[derive(Error, Debug)]
10pub enum CliError {
11    #[error("speaker \"{0}\" not found")]
12    SpeakerNotFound(String),
13
14    #[error("group \"{0}\" not found")]
15    GroupNotFound(String),
16
17    #[error("SDK error: {0}")]
18    Sdk(#[from] sonos_sdk::SdkError),
19
20    #[error("configuration error: {0}")]
21    #[allow(dead_code)]
22    Config(String),
23
24    #[error("validation error: {0}")]
25    Validation(String),
26}
27
28impl CliError {
29    /// Returns actionable follow-up text for the user.
30    pub fn recovery_hint(&self) -> Option<&'static str> {
31        match self {
32            Self::SpeakerNotFound(_) | Self::GroupNotFound(_) => {
33                Some(diagnostics::discovery_hint())
34            }
35            Self::Sdk(e) => {
36                if matches!(e, sonos_sdk::SdkError::DiscoveryFailed(_))
37                    || e.to_string().contains("Network error")
38                {
39                    Some(diagnostics::discovery_hint())
40                } else {
41                    Some("Check network connectivity and speaker power.")
42                }
43            }
44            Self::Config(_) | Self::Validation(_) => None,
45        }
46    }
47
48    /// Returns the appropriate exit code.
49    pub fn exit_code(&self) -> ExitCode {
50        match self {
51            Self::Validation(_) => ExitCode::from(2), // usage error
52            _ => ExitCode::from(1),                   // runtime error
53        }
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn speaker_not_found_has_recovery_hint() {
63        let err = CliError::SpeakerNotFound("Kitchen".to_string());
64        assert!(err.recovery_hint().is_some());
65        assert!(err.recovery_hint().unwrap().contains("this could mean:"));
66    }
67
68    #[test]
69    fn group_not_found_has_recovery_hint() {
70        let err = CliError::GroupNotFound("Living Room".to_string());
71        assert!(err.recovery_hint().is_some());
72    }
73
74    #[test]
75    fn validation_error_has_no_hint() {
76        let err = CliError::Validation("invalid volume".to_string());
77        assert!(err.recovery_hint().is_none());
78    }
79
80    #[test]
81    fn validation_error_returns_exit_code_2() {
82        let err = CliError::Validation("bad input".to_string());
83        assert_eq!(err.exit_code(), ExitCode::from(2));
84    }
85
86    #[test]
87    fn runtime_errors_return_exit_code_1() {
88        let err = CliError::SpeakerNotFound("x".to_string());
89        assert_eq!(err.exit_code(), ExitCode::from(1));
90    }
91}