1use std::io;
2use std::process::ExitStatus;
3use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Error, Debug)]
10pub enum Error {
11 #[error("Executable not found: {0}")]
13 ExecutableNotFound(String),
14
15 #[error("IO error: {0}")]
17 Io(#[from] io::Error),
18
19 #[error("Process execution failed: {message}")]
21 ProcessFailed {
22 message: String,
23 exit_status: Option<ExitStatus>,
24 stderr: Option<String>,
25 },
26
27 #[error("Invalid argument: {0}")]
29 InvalidArgument(String),
30
31 #[error("Parse error: {0}")]
33 ParseError(String),
34
35 #[error("Operation timed out after {0:?}")]
37 Timeout(std::time::Duration),
38
39 #[error("Feature not supported: {0}")]
41 Unsupported(String),
42
43 #[error("Invalid output: {0}")]
45 InvalidOutput(String),
46
47 #[error("Multiple errors occurred")]
49 Multiple(Vec<Error>),
50
51 #[error("{context}: {source}")]
53 WithContext {
54 context: String,
55 #[source]
56 source: Box<Error>,
57 },
58}
59
60impl Error {
61 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 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 pub fn is_timeout(&self) -> bool {
80 matches!(self, Error::Timeout(_))
81 }
82
83 pub fn is_io(&self) -> bool {
85 matches!(self, Error::Io(_))
86 }
87}
88
89pub 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
100pub 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}