docker_wrapper/
error.rs

1//! Error types for docker-wrapper.
2//!
3//! All commands return `Result<T, Error>`. Match on specific error variants
4//! for detailed handling:
5//!
6//! ```rust,no_run
7//! use docker_wrapper::{DockerCommand, RunCommand, Error};
8//!
9//! # async fn example() {
10//! match RunCommand::new("nginx").execute().await {
11//!     Ok(id) => println!("Started: {}", id.short()),
12//!     Err(Error::DockerNotFound) => eprintln!("Docker is not installed"),
13//!     Err(Error::DaemonNotRunning) => eprintln!("Start the Docker daemon"),
14//!     Err(Error::CommandFailed { stderr, .. }) => eprintln!("Failed: {}", stderr),
15//!     Err(e) => eprintln!("Error: {}", e),
16//! }
17//! # }
18
19use thiserror::Error;
20
21/// Result type for docker-wrapper operations
22pub type Result<T> = std::result::Result<T, Error>;
23
24/// Main error type for all docker-wrapper operations
25#[derive(Error, Debug)]
26pub enum Error {
27    /// Docker binary not found in PATH
28    #[error("Docker binary not found in PATH")]
29    DockerNotFound,
30
31    /// Docker daemon is not running
32    #[error("Docker daemon is not running")]
33    DaemonNotRunning,
34
35    /// Docker version is not supported
36    #[error("Docker version {found} is not supported (minimum: {minimum})")]
37    UnsupportedVersion {
38        /// The Docker version that was found
39        found: String,
40        /// The minimum required version
41        minimum: String,
42    },
43
44    /// Failed to execute Docker command
45    #[error("Docker command failed: {command}")]
46    CommandFailed {
47        /// The command that failed
48        command: String,
49        /// Exit code returned by the command
50        exit_code: i32,
51        /// Standard output from the command
52        stdout: String,
53        /// Standard error from the command
54        stderr: String,
55    },
56
57    /// Failed to parse Docker output
58    #[error("Failed to parse Docker output: {message}")]
59    ParseError {
60        /// Error message describing the parse failure
61        message: String,
62    },
63
64    /// Invalid configuration provided
65    #[error("Invalid configuration: {message}")]
66    InvalidConfig {
67        /// Error message describing the configuration issue
68        message: String,
69    },
70
71    /// Docker container not found
72    #[error("Container not found: {container_id}")]
73    ContainerNotFound {
74        /// The container ID that was not found
75        container_id: String,
76    },
77
78    /// Docker image not found
79    #[error("Image not found: {image}")]
80    ImageNotFound {
81        /// The image name that was not found
82        image: String,
83    },
84
85    /// IO error occurred during operation
86    #[error("IO error: {message}")]
87    Io {
88        /// Error message describing the IO failure
89        message: String,
90        /// The underlying IO error
91        #[source]
92        source: std::io::Error,
93    },
94
95    /// JSON parsing or serialization error
96    #[error("JSON error: {message}")]
97    Json {
98        /// Error message describing the JSON failure
99        message: String,
100        /// The underlying JSON error
101        #[source]
102        source: serde_json::Error,
103    },
104
105    /// Operation timed out
106    #[error("Operation timed out after {timeout_seconds} seconds")]
107    Timeout {
108        /// Number of seconds after which the operation timed out
109        timeout_seconds: u64,
110    },
111
112    /// Operation was interrupted
113    #[error("Operation was interrupted")]
114    Interrupted,
115
116    /// Generic error with custom message
117    #[error("{message}")]
118    Custom {
119        /// Custom error message
120        message: String,
121    },
122}
123
124impl Error {
125    /// Create a new command failed error
126    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    /// Create a new parse error
141    pub fn parse_error(message: impl Into<String>) -> Self {
142        Self::ParseError {
143            message: message.into(),
144        }
145    }
146
147    /// Create a new invalid config error
148    pub fn invalid_config(message: impl Into<String>) -> Self {
149        Self::InvalidConfig {
150            message: message.into(),
151        }
152    }
153
154    /// Create a new container not found error
155    pub fn container_not_found(container_id: impl Into<String>) -> Self {
156        Self::ContainerNotFound {
157            container_id: container_id.into(),
158        }
159    }
160
161    /// Create a new image not found error
162    pub fn image_not_found(image: impl Into<String>) -> Self {
163        Self::ImageNotFound {
164            image: image.into(),
165        }
166    }
167
168    /// Create a new timeout error
169    #[must_use]
170    pub fn timeout(timeout_seconds: u64) -> Self {
171        Self::Timeout { timeout_seconds }
172    }
173
174    /// Create a new custom error
175    pub fn custom(message: impl Into<String>) -> Self {
176        Self::Custom {
177            message: message.into(),
178        }
179    }
180
181    /// Get the error category for logging and metrics
182    #[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    /// Check if this error is retryable
199    #[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}