1use std::process::ExitCode;
4use thiserror::Error;
5
6#[derive(Error, Debug)]
8pub enum CliError {
9 #[error("speaker \"{0}\" not found")]
10 SpeakerNotFound(String),
11
12 #[error("group \"{0}\" not found")]
13 GroupNotFound(String),
14
15 #[error("SDK error: {0}")]
16 Sdk(#[from] sonos_sdk::SdkError),
17
18 #[error("configuration error: {0}")]
19 #[allow(dead_code)]
20 Config(String),
21
22 #[error("validation error: {0}")]
23 Validation(String),
24}
25
26impl CliError {
27 pub fn recovery_hint(&self) -> Option<&str> {
29 match self {
30 Self::SpeakerNotFound(_) | Self::GroupNotFound(_) => {
31 Some("Check that your speakers are on the same network, then retry.")
32 }
33 Self::Sdk(_) => Some("Check network connectivity and speaker power."),
34 Self::Config(_) | Self::Validation(_) => None,
35 }
36 }
37
38 pub fn exit_code(&self) -> ExitCode {
40 match self {
41 Self::Validation(_) => ExitCode::from(2), _ => ExitCode::from(1), }
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50
51 #[test]
52 fn speaker_not_found_has_recovery_hint() {
53 let err = CliError::SpeakerNotFound("Kitchen".to_string());
54 assert!(err.recovery_hint().is_some());
55 assert!(err.recovery_hint().unwrap().contains("same network"));
56 }
57
58 #[test]
59 fn group_not_found_has_recovery_hint() {
60 let err = CliError::GroupNotFound("Living Room".to_string());
61 assert!(err.recovery_hint().is_some());
62 }
63
64 #[test]
65 fn validation_error_has_no_hint() {
66 let err = CliError::Validation("invalid volume".to_string());
67 assert!(err.recovery_hint().is_none());
68 }
69
70 #[test]
71 fn validation_error_returns_exit_code_2() {
72 let err = CliError::Validation("bad input".to_string());
73 assert_eq!(err.exit_code(), ExitCode::from(2));
74 }
75
76 #[test]
77 fn runtime_errors_return_exit_code_1() {
78 let err = CliError::SpeakerNotFound("x".to_string());
79 assert_eq!(err.exit_code(), ExitCode::from(1));
80 }
81}