ffmpeg_common/
error.rs

1use std::io;
2use std::process::ExitStatus;
3use thiserror::Error;
4
5/// Result type for FFmpeg suite operations
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Main error type for FFmpeg suite operations
9#[derive(Error, Debug)]
10pub enum Error {
11    /// FFmpeg/FFprobe/FFplay executable not found
12    #[error("Executable not found: {0}")]
13    ExecutableNotFound(String),
14
15    /// IO error occurred
16    #[error("IO error: {0}")]
17    Io(#[from] io::Error),
18
19    /// Process execution failed
20    #[error("Process execution failed: {message}")]
21    ProcessFailed {
22        message: String,
23        exit_status: Option<ExitStatus>,
24        stderr: Option<String>,
25    },
26
27    /// Invalid argument provided
28    #[error("Invalid argument: {0}")]
29    InvalidArgument(String),
30
31    /// Parse error occurred
32    #[error("Parse error: {0}")]
33    ParseError(String),
34
35    /// Timeout occurred
36    #[error("Operation timed out after {0:?}")]
37    Timeout(std::time::Duration),
38
39    /// Feature not supported
40    #[error("Feature not supported: {0}")]
41    Unsupported(String),
42
43    /// Invalid output from FFmpeg tool
44    #[error("Invalid output: {0}")]
45    InvalidOutput(String),
46
47    /// Multiple errors occurred
48    #[error("Multiple errors occurred")]
49    Multiple(Vec<Error>),
50
51    /// Generic error with context
52    #[error("{context}: {source}")]
53    WithContext {
54        context: String,
55        #[source]
56        source: Box<Error>,
57    },
58}
59
60impl Error {
61    /// Add context to an error
62    pub fn context<S: Into<String>>(self, context: S) -> Self {
63        Error::WithContext {
64            context: context.into(),
65            source: Box::new(self),
66        }
67    }
68
69    /// Create a process failed error
70    pub fn process_failed(message: impl Into<String>, exit_status: Option<ExitStatus>, stderr: Option<String>) -> Self {
71        Error::ProcessFailed {
72            message: message.into(),
73            exit_status,
74            stderr,
75        }
76    }
77
78    /// Check if this is a timeout error
79    pub fn is_timeout(&self) -> bool {
80        matches!(self, Error::Timeout(_))
81    }
82
83    /// Check if this is an IO error
84    pub fn is_io(&self) -> bool {
85        matches!(self, Error::Io(_))
86    }
87}
88
89/// Extension trait for adding context to Results
90pub trait ResultExt<T> {
91    fn context<S: Into<String>>(self, context: S) -> Result<T>;
92}
93
94impl<T> ResultExt<T> for Result<T> {
95    fn context<S: Into<String>>(self, context: S) -> Result<T> {
96        self.map_err(|e| e.context(context))
97    }
98}
99
100/// Builder for creating detailed error messages
101pub struct ErrorBuilder {
102    message: String,
103    details: Vec<String>,
104}
105
106impl ErrorBuilder {
107    pub fn new(message: impl Into<String>) -> Self {
108        Self {
109            message: message.into(),
110            details: Vec::new(),
111        }
112    }
113
114    pub fn detail(mut self, detail: impl Into<String>) -> Self {
115        self.details.push(detail.into());
116        self
117    }
118
119    pub fn build(self) -> Error {
120        let mut message = self.message;
121        if !self.details.is_empty() {
122            message.push_str("\nDetails:\n");
123            for detail in self.details {
124                message.push_str(&format!("  - {}\n", detail));
125            }
126        }
127        Error::InvalidArgument(message)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_error_context() {
137        let error = Error::Io(io::Error::new(io::ErrorKind::NotFound, "file not found"));
138        let with_context = error.context("Failed to read input file");
139
140        match with_context {
141            Error::WithContext { context, source } => {
142                assert_eq!(context, "Failed to read input file");
143                assert!(matches!(*source, Error::Io(_)));
144            }
145            _ => panic!("Expected WithContext error"),
146        }
147    }
148
149    #[test]
150    fn test_error_builder() {
151        let error = ErrorBuilder::new("Invalid codec")
152            .detail("Codec 'invalid' is not supported")
153            .detail("Use 'ffmpeg -codecs' to see available codecs")
154            .build();
155
156        match error {
157            Error::InvalidArgument(msg) => {
158                assert!(msg.contains("Invalid codec"));
159                assert!(msg.contains("Details:"));
160                assert!(msg.contains("Codec 'invalid' is not supported"));
161            }
162            _ => panic!("Expected InvalidArgument error"),
163        }
164    }
165}