subx_cli/
error.rs

1//! Comprehensive error types for the SubX CLI application operations.
2//!
3//! This module defines the `SubXError` enum covering all error conditions
4//! that can occur during subtitle processing, AI service integration,
5//! audio analysis, file matching, and general command execution.
6//!
7//! It also provides helper methods to construct errors and generate
8//! user-friendly messages.
9use thiserror::Error;
10
11/// Represents all possible errors in the SubX application.
12///
13/// Each variant provides specific context to facilitate debugging and
14/// user-friendly reporting.
15///
16/// # Examples
17///
18/// ```rust
19/// use subx_cli::error::{SubXError, SubXResult};
20///
21/// fn example() -> SubXResult<()> {
22///     Err(SubXError::SubtitleFormat {
23///         format: "SRT".to_string(),
24///         message: "Invalid timestamp format".to_string(),
25///     })
26/// }
27/// ```
28///
29/// # Exit Codes
30///
31/// Each error variant maps to an exit code via `SubXError::exit_code`.
32#[derive(Error, Debug)]
33pub enum SubXError {
34    /// I/O operation failed during file system access.
35    ///
36    /// This variant wraps `std::io::Error` and provides context about
37    /// file operations that failed.
38    ///
39    /// # Common Causes
40    /// - Permission issues
41    /// - Insufficient disk space
42    /// - Network filesystem errors
43    #[error("I/O error: {0}")]
44    Io(#[from] std::io::Error),
45
46    /// Configuration error due to invalid or missing settings.
47    ///
48    /// Contains a human-readable message describing the issue.
49    #[error("Configuration error: {message}")]
50    Config {
51        /// Description of the configuration error
52        message: String,
53    },
54
55    /// Subtitle format error indicating invalid timestamps or structure.
56    ///
57    /// Provides the subtitle format and detailed message.
58    #[error("Subtitle format error [{format}]: {message}")]
59    SubtitleFormat {
60        /// The subtitle format that caused the error (e.g., "SRT", "ASS")
61        format: String,
62        /// Detailed error message describing the issue
63        message: String,
64    },
65
66    /// AI service encountered an error.
67    ///
68    /// Captures the raw error message from the AI provider.
69    #[error("AI service error: {0}")]
70    AiService(String),
71
72    /// Audio processing error during analysis or format conversion.
73    ///
74    /// Provides a message describing the audio processing failure.
75    #[error("Audio processing error: {message}")]
76    AudioProcessing {
77        /// Description of the audio processing error
78        message: String,
79    },
80
81    /// Error during file matching or discovery.
82    ///
83    /// Contains details about path resolution or pattern matching failures.
84    #[error("File matching error: {message}")]
85    FileMatching {
86        /// Description of the file matching error
87        message: String,
88    },
89    /// Indicates that a file operation failed because the target exists.
90    #[error("File already exists: {0}")]
91    FileAlreadyExists(String),
92    /// Indicates that the specified file was not found.
93    #[error("File not found: {0}")]
94    FileNotFound(String),
95    /// Invalid file name encountered.
96    #[error("Invalid file name: {0}")]
97    InvalidFileName(String),
98    /// Generic file operation failure with message.
99    #[error("File operation failed: {0}")]
100    FileOperationFailed(String),
101    /// Generic command execution error.
102    #[error("{0}")]
103    CommandExecution(String),
104
105    /// Catch-all error variant wrapping any other failure.
106    #[error("Unknown error: {0}")]
107    Other(#[from] anyhow::Error),
108}
109
110// Unit test: SubXError error types and helper methods
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use std::io;
115
116    #[test]
117    fn test_config_error_creation() {
118        let error = SubXError::config("test config error");
119        assert!(matches!(error, SubXError::Config { .. }));
120        assert_eq!(error.to_string(), "Configuration error: test config error");
121    }
122
123    #[test]
124    fn test_subtitle_format_error_creation() {
125        let error = SubXError::subtitle_format("SRT", "invalid format");
126        assert!(matches!(error, SubXError::SubtitleFormat { .. }));
127        let msg = error.to_string();
128        assert!(msg.contains("SRT"));
129        assert!(msg.contains("invalid format"));
130    }
131
132    #[test]
133    fn test_audio_processing_error_creation() {
134        let error = SubXError::audio_processing("decode failed");
135        assert!(matches!(error, SubXError::AudioProcessing { .. }));
136        assert_eq!(error.to_string(), "Audio processing error: decode failed");
137    }
138
139    #[test]
140    fn test_file_matching_error_creation() {
141        let error = SubXError::file_matching("match failed");
142        assert!(matches!(error, SubXError::FileMatching { .. }));
143        assert_eq!(error.to_string(), "File matching error: match failed");
144    }
145
146    #[test]
147    fn test_io_error_conversion() {
148        let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
149        let subx_error: SubXError = io_error.into();
150        assert!(matches!(subx_error, SubXError::Io(_)));
151    }
152
153    #[test]
154    fn test_exit_codes() {
155        assert_eq!(SubXError::config("test").exit_code(), 2);
156        assert_eq!(SubXError::subtitle_format("SRT", "test").exit_code(), 4);
157        assert_eq!(SubXError::audio_processing("test").exit_code(), 5);
158        assert_eq!(SubXError::file_matching("test").exit_code(), 6);
159    }
160
161    #[test]
162    fn test_user_friendly_messages() {
163        let config_error = SubXError::config("missing key");
164        let message = config_error.user_friendly_message();
165        assert!(message.contains("Configuration error:"));
166        assert!(message.contains("subx config --help"));
167
168        let ai_error = SubXError::ai_service("network failure".to_string());
169        let message = ai_error.user_friendly_message();
170        assert!(message.contains("AI service error:"));
171        assert!(message.contains("check network connection"));
172    }
173}
174
175// Convert reqwest error to AI service error
176impl From<reqwest::Error> for SubXError {
177    fn from(err: reqwest::Error) -> Self {
178        SubXError::AiService(err.to_string())
179    }
180}
181
182// Convert file exploration error to file matching error
183impl From<walkdir::Error> for SubXError {
184    fn from(err: walkdir::Error) -> Self {
185        SubXError::FileMatching {
186            message: err.to_string(),
187        }
188    }
189}
190// Convert symphonia error to audio processing error
191impl From<symphonia::core::errors::Error> for SubXError {
192    fn from(err: symphonia::core::errors::Error) -> Self {
193        SubXError::audio_processing(err.to_string())
194    }
195}
196
197// Convert config crate error to configuration error
198impl From<config::ConfigError> for SubXError {
199    fn from(err: config::ConfigError) -> Self {
200        SubXError::Config {
201            message: format!("Configuration loading error: {}", err),
202        }
203    }
204}
205
206/// Specialized `Result` type for SubX operations.
207pub type SubXResult<T> = Result<T, SubXError>;
208
209impl SubXError {
210    /// Create a configuration error with the given message.
211    ///
212    /// # Examples
213    ///
214    /// ```rust
215    /// # use subx_cli::error::SubXError;
216    /// let err = SubXError::config("invalid setting");
217    /// assert_eq!(err.to_string(), "Configuration error: invalid setting");
218    /// ```
219    pub fn config<S: Into<String>>(message: S) -> Self {
220        SubXError::Config {
221            message: message.into(),
222        }
223    }
224
225    /// Create a subtitle format error for the given format and message.
226    ///
227    /// # Examples
228    ///
229    /// ```rust
230    /// # use subx_cli::error::SubXError;
231    /// let err = SubXError::subtitle_format("SRT", "invalid timestamp");
232    /// assert!(err.to_string().contains("SRT"));
233    /// ```
234    pub fn subtitle_format<S1, S2>(format: S1, message: S2) -> Self
235    where
236        S1: Into<String>,
237        S2: Into<String>,
238    {
239        SubXError::SubtitleFormat {
240            format: format.into(),
241            message: message.into(),
242        }
243    }
244
245    /// Create an audio processing error with the given message.
246    ///
247    /// # Examples
248    ///
249    /// ```rust
250    /// # use subx_cli::error::SubXError;
251    /// let err = SubXError::audio_processing("decode failed");
252    /// assert_eq!(err.to_string(), "Audio processing error: decode failed");
253    /// ```
254    pub fn audio_processing<S: Into<String>>(message: S) -> Self {
255        SubXError::AudioProcessing {
256            message: message.into(),
257        }
258    }
259
260    /// Create an AI service error with the given message.
261    ///
262    /// # Examples
263    ///
264    /// ```rust
265    /// # use subx_cli::error::SubXError;
266    /// let err = SubXError::ai_service("network failure");
267    /// assert_eq!(err.to_string(), "AI service error: network failure");
268    /// ```
269    pub fn ai_service<S: Into<String>>(message: S) -> Self {
270        SubXError::AiService(message.into())
271    }
272
273    /// Create a file matching error with the given message.
274    ///
275    /// # Examples
276    ///
277    /// ```rust
278    /// # use subx_cli::error::SubXError;
279    /// let err = SubXError::file_matching("not found");
280    /// assert_eq!(err.to_string(), "File matching error: not found");
281    /// ```
282    pub fn file_matching<S: Into<String>>(message: S) -> Self {
283        SubXError::FileMatching {
284            message: message.into(),
285        }
286    }
287    /// Create a parallel processing error with the given message.
288    pub fn parallel_processing(msg: String) -> Self {
289        SubXError::CommandExecution(format!("Parallel processing error: {}", msg))
290    }
291    /// Create a task execution failure error with task ID and reason.
292    pub fn task_execution_failed(task_id: String, reason: String) -> Self {
293        SubXError::CommandExecution(format!("Task {} execution failed: {}", task_id, reason))
294    }
295    /// Create a worker pool exhausted error.
296    pub fn worker_pool_exhausted() -> Self {
297        SubXError::CommandExecution("Worker pool exhausted".to_string())
298    }
299    /// Create a task timeout error with task ID and duration.
300    pub fn task_timeout(task_id: String, duration: std::time::Duration) -> Self {
301        SubXError::CommandExecution(format!(
302            "Task {} timed out (limit: {:?})",
303            task_id, duration
304        ))
305    }
306    /// Create a dialogue detection failure error with the given message.
307    pub fn dialogue_detection_failed<S: Into<String>>(msg: S) -> Self {
308        SubXError::AudioProcessing {
309            message: format!("Dialogue detection failed: {}", msg.into()),
310        }
311    }
312    /// Create an invalid audio format error for the given format.
313    pub fn invalid_audio_format<S: Into<String>>(format: S) -> Self {
314        SubXError::AudioProcessing {
315            message: format!("Unsupported audio format: {}", format.into()),
316        }
317    }
318    /// Create an invalid dialogue segment error with the given reason.
319    pub fn dialogue_segment_invalid<S: Into<String>>(reason: S) -> Self {
320        SubXError::AudioProcessing {
321            message: format!("Invalid dialogue segment: {}", reason.into()),
322        }
323    }
324    /// Return the corresponding exit code for this error variant.
325    ///
326    /// # Examples
327    ///
328    /// ```rust
329    /// # use subx_cli::error::SubXError;
330    /// assert_eq!(SubXError::config("x").exit_code(), 2);
331    /// ```
332    pub fn exit_code(&self) -> i32 {
333        match self {
334            SubXError::Io(_) => 1,
335            SubXError::Config { .. } => 2,
336            SubXError::AiService(_) => 3,
337            SubXError::SubtitleFormat { .. } => 4,
338            SubXError::AudioProcessing { .. } => 5,
339            SubXError::FileMatching { .. } => 6,
340            _ => 1,
341        }
342    }
343
344    /// Return a user-friendly error message with suggested remedies.
345    ///
346    /// # Examples
347    ///
348    /// ```rust
349    /// # use subx_cli::error::SubXError;
350    /// let msg = SubXError::config("missing key").user_friendly_message();
351    /// assert!(msg.contains("Configuration error:"));
352    /// ```
353    pub fn user_friendly_message(&self) -> String {
354        match self {
355            SubXError::Io(e) => format!("File operation error: {}", e),
356            SubXError::Config { message } => format!(
357                "Configuration error: {}\nHint: run 'subx config --help' for details",
358                message
359            ),
360            SubXError::AiService(msg) => format!(
361                "AI service error: {}\nHint: check network connection and API key settings",
362                msg
363            ),
364            SubXError::SubtitleFormat { message, .. } => format!(
365                "Subtitle processing error: {}\nHint: check file format and encoding",
366                message
367            ),
368            SubXError::AudioProcessing { message } => format!(
369                "Audio processing error: {}\nHint: ensure media file integrity and support",
370                message
371            ),
372            SubXError::FileMatching { message } => format!(
373                "File matching error: {}\nHint: verify file paths and patterns",
374                message
375            ),
376            SubXError::FileAlreadyExists(path) => format!("File already exists: {}", path),
377            SubXError::FileNotFound(path) => format!("File not found: {}", path),
378            SubXError::InvalidFileName(name) => format!("Invalid file name: {}", name),
379            SubXError::FileOperationFailed(msg) => format!("File operation failed: {}", msg),
380            SubXError::CommandExecution(msg) => msg.clone(),
381            SubXError::Other(err) => {
382                format!("Unknown error: {}\nHint: please report this issue", err)
383            }
384        }
385    }
386}
387
388// aus error conversion
389impl From<aus::AudioError> for SubXError {
390    fn from(error: aus::AudioError) -> Self {
391        SubXError::audio_processing(format!("{:?}", error))
392    }
393}
394
395impl From<aus::spectrum::SpectrumError> for SubXError {
396    fn from(error: aus::spectrum::SpectrumError) -> Self {
397        SubXError::audio_processing(format!("Spectrum processing error: {:?}", error))
398    }
399}