docker_wrapper/
error.rs

1//! Error types for the docker-wrapper crate.
2//!
3//! This module provides comprehensive error handling for all Docker operations,
4//! with clear error messages and helpful context.
5
6use thiserror::Error;
7
8/// Result type for docker-wrapper operations
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Main error type for all docker-wrapper operations
12#[derive(Error, Debug)]
13pub enum Error {
14    /// Docker binary not found in PATH
15    #[error("Docker binary not found in PATH")]
16    DockerNotFound,
17
18    /// Docker daemon is not running
19    #[error("Docker daemon is not running")]
20    DaemonNotRunning,
21
22    /// Docker version is not supported
23    #[error("Docker version {found} is not supported (minimum: {minimum})")]
24    UnsupportedVersion {
25        /// The Docker version that was found
26        found: String,
27        /// The minimum required version
28        minimum: String,
29    },
30
31    /// Failed to execute Docker command
32    #[error("Docker command failed: {command}")]
33    CommandFailed {
34        /// The command that failed
35        command: String,
36        /// Exit code returned by the command
37        exit_code: i32,
38        /// Standard output from the command
39        stdout: String,
40        /// Standard error from the command
41        stderr: String,
42    },
43
44    /// Failed to parse Docker output
45    #[error("Failed to parse Docker output: {message}")]
46    ParseError {
47        /// Error message describing the parse failure
48        message: String,
49    },
50
51    /// Invalid configuration provided
52    #[error("Invalid configuration: {message}")]
53    InvalidConfig {
54        /// Error message describing the configuration issue
55        message: String,
56    },
57
58    /// Docker container not found
59    #[error("Container not found: {container_id}")]
60    ContainerNotFound {
61        /// The container ID that was not found
62        container_id: String,
63    },
64
65    /// Docker image not found
66    #[error("Image not found: {image}")]
67    ImageNotFound {
68        /// The image name that was not found
69        image: String,
70    },
71
72    /// IO error occurred during operation
73    #[error("IO error: {message}")]
74    Io {
75        /// Error message describing the IO failure
76        message: String,
77        /// The underlying IO error
78        #[source]
79        source: std::io::Error,
80    },
81
82    /// JSON parsing or serialization error
83    #[error("JSON error: {message}")]
84    Json {
85        /// Error message describing the JSON failure
86        message: String,
87        /// The underlying JSON error
88        #[source]
89        source: serde_json::Error,
90    },
91
92    /// Operation timed out
93    #[error("Operation timed out after {timeout_seconds} seconds")]
94    Timeout {
95        /// Number of seconds after which the operation timed out
96        timeout_seconds: u64,
97    },
98
99    /// Operation was interrupted
100    #[error("Operation was interrupted")]
101    Interrupted,
102
103    /// Generic error with custom message
104    #[error("{message}")]
105    Custom {
106        /// Custom error message
107        message: String,
108    },
109}
110
111impl Error {
112    /// Create a new command failed error
113    pub fn command_failed(
114        command: impl Into<String>,
115        exit_code: i32,
116        stdout: impl Into<String>,
117        stderr: impl Into<String>,
118    ) -> Self {
119        Self::CommandFailed {
120            command: command.into(),
121            exit_code,
122            stdout: stdout.into(),
123            stderr: stderr.into(),
124        }
125    }
126
127    /// Create a new parse error
128    pub fn parse_error(message: impl Into<String>) -> Self {
129        Self::ParseError {
130            message: message.into(),
131        }
132    }
133
134    /// Create a new invalid config error
135    pub fn invalid_config(message: impl Into<String>) -> Self {
136        Self::InvalidConfig {
137            message: message.into(),
138        }
139    }
140
141    /// Create a new container not found error
142    pub fn container_not_found(container_id: impl Into<String>) -> Self {
143        Self::ContainerNotFound {
144            container_id: container_id.into(),
145        }
146    }
147
148    /// Create a new image not found error
149    pub fn image_not_found(image: impl Into<String>) -> Self {
150        Self::ImageNotFound {
151            image: image.into(),
152        }
153    }
154
155    /// Create a new timeout error
156    #[must_use]
157    pub fn timeout(timeout_seconds: u64) -> Self {
158        Self::Timeout { timeout_seconds }
159    }
160
161    /// Create a new custom error
162    pub fn custom(message: impl Into<String>) -> Self {
163        Self::Custom {
164            message: message.into(),
165        }
166    }
167
168    /// Get the error category for logging and metrics
169    #[must_use]
170    pub fn category(&self) -> &'static str {
171        match self {
172            Self::DockerNotFound | Self::DaemonNotRunning | Self::UnsupportedVersion { .. } => {
173                "prerequisites"
174            }
175            Self::CommandFailed { .. } | Self::Timeout { .. } | Self::Interrupted => "command",
176            Self::ParseError { .. } | Self::Json { .. } => "parsing",
177            Self::InvalidConfig { .. } => "config",
178            Self::ContainerNotFound { .. } => "container",
179            Self::ImageNotFound { .. } => "image",
180            Self::Io { .. } => "io",
181            Self::Custom { .. } => "custom",
182        }
183    }
184
185    /// Check if this error is retryable
186    #[must_use]
187    pub fn is_retryable(&self) -> bool {
188        matches!(
189            self,
190            Self::CommandFailed { .. } | Self::Timeout { .. } | Self::Io { .. }
191        )
192    }
193}
194
195impl From<std::io::Error> for Error {
196    fn from(err: std::io::Error) -> Self {
197        Self::Io {
198            message: err.to_string(),
199            source: err,
200        }
201    }
202}
203
204impl From<serde_json::Error> for Error {
205    fn from(err: serde_json::Error) -> Self {
206        Self::Json {
207            message: err.to_string(),
208            source: err,
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_error_categories() {
219        assert_eq!(Error::DockerNotFound.category(), "prerequisites");
220        assert_eq!(
221            Error::command_failed("test", 1, "", "").category(),
222            "command"
223        );
224        assert_eq!(Error::parse_error("test").category(), "parsing");
225        assert_eq!(Error::invalid_config("test").category(), "config");
226        assert_eq!(Error::container_not_found("test").category(), "container");
227        assert_eq!(Error::image_not_found("test").category(), "image");
228        assert_eq!(Error::custom("test").category(), "custom");
229    }
230
231    #[test]
232    fn test_retryable_errors() {
233        assert!(Error::command_failed("test", 1, "", "").is_retryable());
234        assert!(Error::timeout(30).is_retryable());
235        assert!(!Error::DockerNotFound.is_retryable());
236        assert!(!Error::invalid_config("test").is_retryable());
237    }
238
239    #[test]
240    fn test_error_constructors() {
241        let cmd_err = Error::command_failed("docker run", 1, "output", "error");
242        match cmd_err {
243            Error::CommandFailed {
244                command,
245                exit_code,
246                stdout,
247                stderr,
248            } => {
249                assert_eq!(command, "docker run");
250                assert_eq!(exit_code, 1);
251                assert_eq!(stdout, "output");
252                assert_eq!(stderr, "error");
253            }
254            _ => panic!("Wrong error type"),
255        }
256
257        let parse_err = Error::parse_error("invalid format");
258        match parse_err {
259            Error::ParseError { message } => {
260                assert_eq!(message, "invalid format");
261            }
262            _ => panic!("Wrong error type"),
263        }
264    }
265
266    #[test]
267    fn test_from_io_error() {
268        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
269        let docker_err: Error = io_err.into();
270
271        match docker_err {
272            Error::Io { message, .. } => {
273                assert!(message.contains("file not found"));
274            }
275            _ => panic!("Wrong error type"),
276        }
277    }
278
279    #[test]
280    fn test_from_json_error() {
281        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
282        let docker_err: Error = json_err.into();
283
284        match docker_err {
285            Error::Json { message, .. } => {
286                assert!(!message.is_empty());
287            }
288            _ => panic!("Wrong error type"),
289        }
290    }
291}