1use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Error, Debug)]
13pub enum Error {
14 #[error("Docker binary not found in PATH")]
16 DockerNotFound,
17
18 #[error("Docker daemon is not running")]
20 DaemonNotRunning,
21
22 #[error("Docker version {found} is not supported (minimum: {minimum})")]
24 UnsupportedVersion {
25 found: String,
27 minimum: String,
29 },
30
31 #[error("Docker command failed: {command}")]
33 CommandFailed {
34 command: String,
36 exit_code: i32,
38 stdout: String,
40 stderr: String,
42 },
43
44 #[error("Failed to parse Docker output: {message}")]
46 ParseError {
47 message: String,
49 },
50
51 #[error("Invalid configuration: {message}")]
53 InvalidConfig {
54 message: String,
56 },
57
58 #[error("Container not found: {container_id}")]
60 ContainerNotFound {
61 container_id: String,
63 },
64
65 #[error("Image not found: {image}")]
67 ImageNotFound {
68 image: String,
70 },
71
72 #[error("IO error: {message}")]
74 Io {
75 message: String,
77 #[source]
79 source: std::io::Error,
80 },
81
82 #[error("JSON error: {message}")]
84 Json {
85 message: String,
87 #[source]
89 source: serde_json::Error,
90 },
91
92 #[error("Operation timed out after {timeout_seconds} seconds")]
94 Timeout {
95 timeout_seconds: u64,
97 },
98
99 #[error("Operation was interrupted")]
101 Interrupted,
102
103 #[error("{message}")]
105 Custom {
106 message: String,
108 },
109}
110
111impl Error {
112 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 pub fn parse_error(message: impl Into<String>) -> Self {
129 Self::ParseError {
130 message: message.into(),
131 }
132 }
133
134 pub fn invalid_config(message: impl Into<String>) -> Self {
136 Self::InvalidConfig {
137 message: message.into(),
138 }
139 }
140
141 pub fn container_not_found(container_id: impl Into<String>) -> Self {
143 Self::ContainerNotFound {
144 container_id: container_id.into(),
145 }
146 }
147
148 pub fn image_not_found(image: impl Into<String>) -> Self {
150 Self::ImageNotFound {
151 image: image.into(),
152 }
153 }
154
155 #[must_use]
157 pub fn timeout(timeout_seconds: u64) -> Self {
158 Self::Timeout { timeout_seconds }
159 }
160
161 pub fn custom(message: impl Into<String>) -> Self {
163 Self::Custom {
164 message: message.into(),
165 }
166 }
167
168 #[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 #[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}