1use thiserror::Error;
20
21pub type Result<T> = std::result::Result<T, Error>;
23
24#[derive(Error, Debug)]
26pub enum Error {
27 #[error("Docker binary not found in PATH")]
29 DockerNotFound,
30
31 #[error("Docker daemon is not running")]
33 DaemonNotRunning,
34
35 #[error("Docker version {found} is not supported (minimum: {minimum})")]
37 UnsupportedVersion {
38 found: String,
40 minimum: String,
42 },
43
44 #[error("Docker command failed: {command}")]
46 CommandFailed {
47 command: String,
49 exit_code: i32,
51 stdout: String,
53 stderr: String,
55 },
56
57 #[error("Failed to parse Docker output: {message}")]
59 ParseError {
60 message: String,
62 },
63
64 #[error("Invalid configuration: {message}")]
66 InvalidConfig {
67 message: String,
69 },
70
71 #[error("Container not found: {container_id}")]
73 ContainerNotFound {
74 container_id: String,
76 },
77
78 #[error("Image not found: {image}")]
80 ImageNotFound {
81 image: String,
83 },
84
85 #[error("IO error: {message}")]
87 Io {
88 message: String,
90 #[source]
92 source: std::io::Error,
93 },
94
95 #[error("JSON error: {message}")]
97 Json {
98 message: String,
100 #[source]
102 source: serde_json::Error,
103 },
104
105 #[error("Operation timed out after {timeout_seconds} seconds")]
107 Timeout {
108 timeout_seconds: u64,
110 },
111
112 #[error("Operation was interrupted")]
114 Interrupted,
115
116 #[error("{message}")]
118 Custom {
119 message: String,
121 },
122}
123
124impl Error {
125 pub fn command_failed(
127 command: impl Into<String>,
128 exit_code: i32,
129 stdout: impl Into<String>,
130 stderr: impl Into<String>,
131 ) -> Self {
132 Self::CommandFailed {
133 command: command.into(),
134 exit_code,
135 stdout: stdout.into(),
136 stderr: stderr.into(),
137 }
138 }
139
140 pub fn parse_error(message: impl Into<String>) -> Self {
142 Self::ParseError {
143 message: message.into(),
144 }
145 }
146
147 pub fn invalid_config(message: impl Into<String>) -> Self {
149 Self::InvalidConfig {
150 message: message.into(),
151 }
152 }
153
154 pub fn container_not_found(container_id: impl Into<String>) -> Self {
156 Self::ContainerNotFound {
157 container_id: container_id.into(),
158 }
159 }
160
161 pub fn image_not_found(image: impl Into<String>) -> Self {
163 Self::ImageNotFound {
164 image: image.into(),
165 }
166 }
167
168 #[must_use]
170 pub fn timeout(timeout_seconds: u64) -> Self {
171 Self::Timeout { timeout_seconds }
172 }
173
174 pub fn custom(message: impl Into<String>) -> Self {
176 Self::Custom {
177 message: message.into(),
178 }
179 }
180
181 #[must_use]
183 pub fn category(&self) -> &'static str {
184 match self {
185 Self::DockerNotFound | Self::DaemonNotRunning | Self::UnsupportedVersion { .. } => {
186 "prerequisites"
187 }
188 Self::CommandFailed { .. } | Self::Timeout { .. } | Self::Interrupted => "command",
189 Self::ParseError { .. } | Self::Json { .. } => "parsing",
190 Self::InvalidConfig { .. } => "config",
191 Self::ContainerNotFound { .. } => "container",
192 Self::ImageNotFound { .. } => "image",
193 Self::Io { .. } => "io",
194 Self::Custom { .. } => "custom",
195 }
196 }
197
198 #[must_use]
200 pub fn is_retryable(&self) -> bool {
201 matches!(
202 self,
203 Self::CommandFailed { .. } | Self::Timeout { .. } | Self::Io { .. }
204 )
205 }
206}
207
208impl From<std::io::Error> for Error {
209 fn from(err: std::io::Error) -> Self {
210 Self::Io {
211 message: err.to_string(),
212 source: err,
213 }
214 }
215}
216
217impl From<serde_json::Error> for Error {
218 fn from(err: serde_json::Error) -> Self {
219 Self::Json {
220 message: err.to_string(),
221 source: err,
222 }
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_error_categories() {
232 assert_eq!(Error::DockerNotFound.category(), "prerequisites");
233 assert_eq!(
234 Error::command_failed("test", 1, "", "").category(),
235 "command"
236 );
237 assert_eq!(Error::parse_error("test").category(), "parsing");
238 assert_eq!(Error::invalid_config("test").category(), "config");
239 assert_eq!(Error::container_not_found("test").category(), "container");
240 assert_eq!(Error::image_not_found("test").category(), "image");
241 assert_eq!(Error::custom("test").category(), "custom");
242 }
243
244 #[test]
245 fn test_retryable_errors() {
246 assert!(Error::command_failed("test", 1, "", "").is_retryable());
247 assert!(Error::timeout(30).is_retryable());
248 assert!(!Error::DockerNotFound.is_retryable());
249 assert!(!Error::invalid_config("test").is_retryable());
250 }
251
252 #[test]
253 fn test_error_constructors() {
254 let cmd_err = Error::command_failed("docker run", 1, "output", "error");
255 match cmd_err {
256 Error::CommandFailed {
257 command,
258 exit_code,
259 stdout,
260 stderr,
261 } => {
262 assert_eq!(command, "docker run");
263 assert_eq!(exit_code, 1);
264 assert_eq!(stdout, "output");
265 assert_eq!(stderr, "error");
266 }
267 _ => panic!("Wrong error type"),
268 }
269
270 let parse_err = Error::parse_error("invalid format");
271 match parse_err {
272 Error::ParseError { message } => {
273 assert_eq!(message, "invalid format");
274 }
275 _ => panic!("Wrong error type"),
276 }
277 }
278
279 #[test]
280 fn test_from_io_error() {
281 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
282 let docker_err: Error = io_err.into();
283
284 match docker_err {
285 Error::Io { message, .. } => {
286 assert!(message.contains("file not found"));
287 }
288 _ => panic!("Wrong error type"),
289 }
290 }
291
292 #[test]
293 fn test_from_json_error() {
294 let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
295 let docker_err: Error = json_err.into();
296
297 match docker_err {
298 Error::Json { message, .. } => {
299 assert!(!message.is_empty());
300 }
301 _ => panic!("Wrong error type"),
302 }
303 }
304}