1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13#[non_exhaustive]
14pub enum ErrorCode {
15 InvalidRequest,
17 UnknownCommand,
19 InvalidParams,
21 UnknownEpisode,
23 NotPlaying,
25 SeekUnsupported,
27 PlaybackFailed,
29 CatalogUnavailable,
31 DownloadFailed,
33 DownloadNotActive,
35 InsufficientSpace,
37 Internal,
39 #[serde(other)]
44 Unknown,
45}
46
47impl ErrorCode {
48 pub const fn as_str(self) -> &'static str {
50 match self {
51 Self::InvalidRequest => "invalid_request",
52 Self::UnknownCommand => "unknown_command",
53 Self::InvalidParams => "invalid_params",
54 Self::UnknownEpisode => "unknown_episode",
55 Self::NotPlaying => "not_playing",
56 Self::SeekUnsupported => "seek_unsupported",
57 Self::PlaybackFailed => "playback_failed",
58 Self::CatalogUnavailable => "catalog_unavailable",
59 Self::DownloadFailed => "download_failed",
60 Self::DownloadNotActive => "download_not_active",
61 Self::InsufficientSpace => "insufficient_space",
62 Self::Internal => "internal",
63 Self::Unknown => "unknown",
64 }
65 }
66}
67
68impl std::fmt::Display for ErrorCode {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.write_str(self.as_str())
71 }
72}
73
74#[derive(Debug, thiserror::Error)]
76#[non_exhaustive]
77pub enum Error {
78 #[error("invalid request: {0}")]
79 InvalidRequest(String),
80
81 #[error("unknown command: {0}")]
82 UnknownCommand(String),
83
84 #[error("invalid parameters: {0}")]
85 InvalidParams(String),
86
87 #[error("no episode with identifier {0}")]
88 UnknownEpisode(String),
89
90 #[error("nothing is loaded")]
91 NotPlaying,
92
93 #[error("the current source does not support seeking")]
94 SeekUnsupported,
95
96 #[error("playback failed: {0}")]
97 PlaybackFailed(String),
98
99 #[error("catalog unavailable: {0}")]
100 CatalogUnavailable(String),
101
102 #[error("download failed: {0}")]
103 DownloadFailed(String),
104
105 #[error("no download in progress for {0}")]
106 DownloadNotActive(String),
107
108 #[error("{required_bytes} bytes are required but only {available_bytes} are free")]
109 InsufficientSpace {
110 required_bytes: u64,
111 available_bytes: u64,
112 },
113
114 #[error("{0}")]
115 Internal(String),
116
117 #[error("{0}")]
119 Io(#[from] std::io::Error),
120}
121
122impl Error {
123 pub fn code(&self) -> ErrorCode {
125 match self {
126 Self::InvalidRequest(_) => ErrorCode::InvalidRequest,
127 Self::UnknownCommand(_) => ErrorCode::UnknownCommand,
128 Self::InvalidParams(_) => ErrorCode::InvalidParams,
129 Self::UnknownEpisode(_) => ErrorCode::UnknownEpisode,
130 Self::NotPlaying => ErrorCode::NotPlaying,
131 Self::SeekUnsupported => ErrorCode::SeekUnsupported,
132 Self::PlaybackFailed(_) => ErrorCode::PlaybackFailed,
133 Self::CatalogUnavailable(_) => ErrorCode::CatalogUnavailable,
134 Self::DownloadFailed(_) => ErrorCode::DownloadFailed,
135 Self::DownloadNotActive(_) => ErrorCode::DownloadNotActive,
136 Self::InsufficientSpace { .. } => ErrorCode::InsufficientSpace,
137 Self::Internal(_) | Self::Io(_) => ErrorCode::Internal,
138 }
139 }
140}
141
142pub type Result<T> = std::result::Result<T, Error>;
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn every_variant_maps_to_its_spec_code() {
150 let cases: Vec<(Error, &str)> = vec![
151 (Error::InvalidRequest("x".into()), "invalid_request"),
152 (Error::UnknownCommand("x".into()), "unknown_command"),
153 (Error::InvalidParams("x".into()), "invalid_params"),
154 (Error::UnknownEpisode("x".into()), "unknown_episode"),
155 (Error::NotPlaying, "not_playing"),
156 (Error::SeekUnsupported, "seek_unsupported"),
157 (Error::PlaybackFailed("x".into()), "playback_failed"),
158 (Error::CatalogUnavailable("x".into()), "catalog_unavailable"),
159 (Error::DownloadFailed("x".into()), "download_failed"),
160 (Error::DownloadNotActive("x".into()), "download_not_active"),
161 (
162 Error::InsufficientSpace {
163 required_bytes: 2,
164 available_bytes: 1,
165 },
166 "insufficient_space",
167 ),
168 (Error::Internal("x".into()), "internal"),
169 (
170 Error::Io(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
171 "internal",
172 ),
173 ];
174 for (error, code) in cases {
175 assert_eq!(error.code().as_str(), code);
176 }
177 }
178
179 #[test]
180 fn codes_serialise_to_their_wire_spelling() {
181 for code in [
182 ErrorCode::InvalidRequest,
183 ErrorCode::UnknownCommand,
184 ErrorCode::InvalidParams,
185 ErrorCode::UnknownEpisode,
186 ErrorCode::NotPlaying,
187 ErrorCode::SeekUnsupported,
188 ErrorCode::PlaybackFailed,
189 ErrorCode::CatalogUnavailable,
190 ErrorCode::DownloadFailed,
191 ErrorCode::DownloadNotActive,
192 ErrorCode::InsufficientSpace,
193 ErrorCode::Internal,
194 ErrorCode::Unknown,
195 ] {
196 let json = serde_json::to_string(&code).unwrap();
197 assert_eq!(json, format!("\"{}\"", code.as_str()));
198 assert_eq!(serde_json::from_str::<ErrorCode>(&json).unwrap(), code);
199 }
200 }
201
202 #[test]
205 fn a_code_this_release_does_not_know_reads_as_unknown() {
206 assert_eq!(
207 serde_json::from_str::<ErrorCode>(r#""a_code_from_a_later_release""#).unwrap(),
208 ErrorCode::Unknown
209 );
210 }
211
212 #[test]
213 fn an_io_failure_keeps_its_kind_and_its_source() {
214 let error = Error::Io(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
215
216 let Error::Io(source) = &error else {
217 panic!("{error:?} is not an I/O failure");
218 };
219 assert_eq!(source.kind(), std::io::ErrorKind::PermissionDenied);
220 assert!(std::error::Error::source(&error).is_some());
221 }
222}