ass_core/utils/errors/
mod.rs

1//! Core error types for ASS-RS utilities and cross-module error handling
2//!
3//! Provides the main `CoreError` enum that wraps all error types from different
4//! modules in the crate. Designed for easy error propagation and conversion.
5//!
6//! # Error Philosophy
7//!
8//! - Use `thiserror` for structured error handling (no `anyhow` bloat)
9//! - Provide detailed context for debugging and user feedback
10//! - Support error chains with source information
11//! - Include suggestions for common error scenarios
12//! - Maintain zero-cost error handling where possible
13//!
14//! # Examples
15//!
16//! ```rust
17//! use ass_core::utils::errors::{CoreError, Result, ErrorCategory};
18//!
19//! // Create specific error types
20//! let color_err = CoreError::invalid_color("invalid");
21//! let time_err = CoreError::invalid_time("1:23", "missing seconds");
22//!
23//! // Check error properties
24//! assert_eq!(color_err.category(), ErrorCategory::Format);
25//! assert!(color_err.suggestion().is_some());
26//! ```
27
28#[cfg(not(feature = "std"))]
29extern crate alloc;
30#[cfg(not(feature = "std"))]
31use alloc::format;
32mod category;
33mod core;
34pub mod encoding;
35mod format;
36pub mod resource;
37
38// Re-export all public types to maintain API compatibility
39pub use category::ErrorCategory;
40pub use core::{CoreError, Result};
41
42// Re-export utility functions from sub-modules
43pub use encoding::{
44    utf8_error, validate_ass_text_content, validate_bom_handling, validate_utf8_detailed,
45};
46pub use format::{invalid_color, invalid_numeric, invalid_time, validate_color_format};
47pub use resource::{
48    check_memory_limit, feature_not_supported, out_of_memory, resource_limit_exceeded,
49};
50
51impl CoreError {
52    /// Create color error from invalid format
53    pub fn invalid_color<T: ::core::fmt::Display>(format: T) -> Self {
54        format::invalid_color(format)
55    }
56
57    /// Create numeric error from parsing failure
58    pub fn invalid_numeric<T: ::core::fmt::Display>(value: T, reason: &str) -> Self {
59        format::invalid_numeric(value, reason)
60    }
61
62    /// Create time error from invalid format
63    pub fn invalid_time<T: ::core::fmt::Display>(time: T, reason: &str) -> Self {
64        format::invalid_time(time, reason)
65    }
66
67    /// Create UTF-8 error with position
68    #[must_use]
69    pub const fn utf8_error(position: usize, message: alloc::string::String) -> Self {
70        encoding::utf8_error(position, message)
71    }
72
73    /// Create feature not supported error
74    #[must_use]
75    pub fn feature_not_supported(feature: &str, required_feature: &str) -> Self {
76        resource::feature_not_supported(feature, required_feature)
77    }
78
79    /// Create resource limit error
80    #[must_use]
81    pub fn resource_limit_exceeded(resource: &str, current: usize, limit: usize) -> Self {
82        resource::resource_limit_exceeded(resource, current, limit)
83    }
84}
85
86/// Convert from parser errors
87impl From<crate::parser::ParseError> for CoreError {
88    fn from(err: crate::parser::ParseError) -> Self {
89        Self::Parse(err)
90    }
91}
92
93/// Convert from standard I/O errors (when std is available)
94#[cfg(feature = "std")]
95impl From<std::io::Error> for CoreError {
96    fn from(err: std::io::Error) -> Self {
97        Self::Io(format!("{err}"))
98    }
99}
100
101/// Convert from `core::str::Utf8Error`
102impl From<::core::str::Utf8Error> for CoreError {
103    fn from(err: ::core::str::Utf8Error) -> Self {
104        Self::Utf8Error {
105            position: 0, // Position not available from Utf8Error
106            message: format!("{err}"),
107        }
108    }
109}
110
111/// Convert from integer parse errors
112impl From<::core::num::ParseIntError> for CoreError {
113    fn from(err: ::core::num::ParseIntError) -> Self {
114        Self::InvalidNumeric(format!("Integer parse error: {err}"))
115    }
116}
117
118/// Convert from float parse errors
119impl From<::core::num::ParseFloatError> for CoreError {
120    fn from(err: ::core::num::ParseFloatError) -> Self {
121        Self::InvalidNumeric(format!("Float parse error: {err}"))
122    }
123}
124
125#[cfg(test)]
126mod tests;
127
128#[cfg(test)]
129mod inline_tests {
130    use super::*;
131    #[cfg(not(feature = "std"))]
132    use alloc::{format, string::ToString, vec};
133
134    #[test]
135    fn error_creation_methods() {
136        let parse_err = CoreError::parse("test message");
137        assert!(matches!(parse_err, CoreError::Parse(_)));
138
139        let color_err = CoreError::invalid_color("invalid");
140        assert!(matches!(color_err, CoreError::InvalidColor(_)));
141
142        let time_err = CoreError::invalid_time("invalid", "wrong format");
143        assert!(matches!(time_err, CoreError::InvalidTime(_)));
144    }
145
146    #[test]
147    fn error_display() {
148        let error = CoreError::invalid_color("test");
149        let display_str = format!("{error}");
150        assert!(display_str.contains("Invalid color format"));
151        assert!(display_str.contains("test"));
152    }
153
154    #[test]
155    fn error_conversion() {
156        let parse_int_err: ::core::num::ParseIntError = "abc".parse::<i32>().unwrap_err();
157        let core_err: CoreError = parse_int_err.into();
158        assert!(matches!(core_err, CoreError::InvalidNumeric(_)));
159
160        let parse_float_err: ::core::num::ParseFloatError = "xyz".parse::<f32>().unwrap_err();
161        let core_err: CoreError = parse_float_err.into();
162        assert!(matches!(core_err, CoreError::InvalidNumeric(_)));
163    }
164
165    #[test]
166    fn error_properties() {
167        let error = CoreError::invalid_color("test");
168        assert_eq!(error.category(), ErrorCategory::Format);
169        assert!(error.suggestion().is_some());
170        assert!(error.is_recoverable());
171        assert!(!error.is_internal_bug());
172    }
173
174    #[test]
175    fn result_type_alias() {
176        fn test_function() -> i32 {
177            42
178        }
179
180        assert_eq!(test_function(), 42);
181    }
182
183    #[test]
184    fn core_error_invalid_color_convenience() {
185        let color_err = CoreError::invalid_color("red");
186        assert!(matches!(color_err, CoreError::InvalidColor(_)));
187        if let CoreError::InvalidColor(msg) = color_err {
188            assert!(msg.contains("red"));
189        }
190    }
191
192    #[test]
193    fn core_error_invalid_numeric_convenience() {
194        let numeric_err = CoreError::invalid_numeric("abc", "not a number");
195        assert!(matches!(numeric_err, CoreError::InvalidNumeric(_)));
196        if let CoreError::InvalidNumeric(msg) = numeric_err {
197            assert!(msg.contains("abc"));
198            assert!(msg.contains("not a number"));
199        }
200    }
201
202    #[test]
203    fn core_error_invalid_time_convenience() {
204        let time_err = CoreError::invalid_time("25:00:00", "hours out of range");
205        assert!(matches!(time_err, CoreError::InvalidTime(_)));
206        if let CoreError::InvalidTime(msg) = time_err {
207            assert!(msg.contains("25:00:00"));
208            assert!(msg.contains("hours out of range"));
209        }
210    }
211
212    #[test]
213    fn core_error_utf8_error_convenience() {
214        let utf8_err = CoreError::utf8_error(42, "invalid sequence".to_string());
215        assert!(matches!(utf8_err, CoreError::Utf8Error { .. }));
216        if let CoreError::Utf8Error { position, message } = utf8_err {
217            assert_eq!(position, 42);
218            assert_eq!(message, "invalid sequence");
219        }
220    }
221
222    #[test]
223    fn core_error_feature_not_supported_convenience() {
224        let feature_err = CoreError::feature_not_supported("simd", "cpu-features");
225        assert!(matches!(feature_err, CoreError::FeatureNotSupported { .. }));
226        if let CoreError::FeatureNotSupported {
227            feature,
228            required_feature,
229        } = feature_err
230        {
231            assert_eq!(feature, "simd");
232            assert_eq!(required_feature, "cpu-features");
233        }
234    }
235
236    #[test]
237    fn core_error_resource_limit_exceeded_convenience() {
238        let resource_err = CoreError::resource_limit_exceeded("memory", 1024, 512);
239        assert!(matches!(
240            resource_err,
241            CoreError::ResourceLimitExceeded { .. }
242        ));
243        if let CoreError::ResourceLimitExceeded {
244            resource,
245            current,
246            limit,
247        } = resource_err
248        {
249            assert_eq!(resource, "memory");
250            assert_eq!(current, 1024);
251            assert_eq!(limit, 512);
252        }
253    }
254
255    #[test]
256    fn utf8_error_conversion() {
257        let invalid_bytes = vec![0xFF, 0xFE];
258        let utf8_err = ::core::str::from_utf8(&invalid_bytes).unwrap_err();
259        let core_err: CoreError = utf8_err.into();
260        assert!(matches!(core_err, CoreError::Utf8Error { .. }));
261        if let CoreError::Utf8Error { position, message } = core_err {
262            assert_eq!(position, 0); // Position not available from Utf8Error
263            assert!(message.contains("invalid utf-8"));
264        }
265    }
266
267    #[cfg(feature = "std")]
268    #[test]
269    fn io_error_conversion() {
270        use std::io::{Error, ErrorKind};
271
272        let io_err = Error::new(ErrorKind::NotFound, "file not found");
273        let core_err: CoreError = io_err.into();
274        assert!(matches!(core_err, CoreError::Io(_)));
275        if let CoreError::Io(msg) = core_err {
276            assert!(msg.contains("file not found"));
277        }
278    }
279
280    #[test]
281    fn parse_error_conversion() {
282        let parse_err = crate::parser::ParseError::IoError {
283            message: "io failure".to_string(),
284        };
285        let core_err: CoreError = parse_err.into();
286        assert!(matches!(core_err, CoreError::Parse(_)));
287    }
288
289    #[test]
290    fn numeric_conversion_edge_cases() {
291        // Test different numeric parsing errors
292        let int_overflow: ::core::num::ParseIntError =
293            "999999999999999999999999999".parse::<i32>().unwrap_err();
294        let core_err: CoreError = int_overflow.into();
295        assert!(matches!(core_err, CoreError::InvalidNumeric(_)));
296        if let CoreError::InvalidNumeric(msg) = core_err {
297            assert!(msg.contains("Integer parse error"));
298        }
299
300        let float_err: ::core::num::ParseFloatError = "not_a_float".parse::<f64>().unwrap_err();
301        let core_err: CoreError = float_err.into();
302        assert!(matches!(core_err, CoreError::InvalidNumeric(_)));
303        if let CoreError::InvalidNumeric(msg) = core_err {
304            assert!(msg.contains("Float parse error"));
305        }
306    }
307
308    #[test]
309    fn module_re_exports() {
310        // Test that all re-exported functions are accessible
311        let _color_err = invalid_color("test");
312        let _numeric_err = invalid_numeric("test", "reason");
313        let _time_err = invalid_time("test", "reason");
314        let _utf8_err = utf8_error(0, "message".to_string());
315        let _feature_err = feature_not_supported("feature", "required");
316        let _resource_err = resource_limit_exceeded("resource", 100, 50);
317        let _memory_err = out_of_memory("test");
318        let _limit_err = check_memory_limit(1000, 500, 800);
319
320        // Test validation functions
321        let _validated = validate_utf8_detailed(b"test");
322        let _color_validated = validate_color_format("&H000000");
323        let _ass_validated = validate_ass_text_content("test");
324        let _bom_validated = validate_bom_handling(b"test");
325    }
326
327    #[test]
328    fn error_display_consistency() {
329        // Test that all error types have consistent display formatting
330        let errors = vec![
331            CoreError::Tokenization("test".to_string()),
332            CoreError::Analysis("test".to_string()),
333            CoreError::Plugin("test".to_string()),
334            CoreError::InvalidColor("test".to_string()),
335            CoreError::InvalidNumeric("test".to_string()),
336            CoreError::InvalidTime("test".to_string()),
337            CoreError::Io("test".to_string()),
338            CoreError::OutOfMemory("test".to_string()),
339            CoreError::Config("test".to_string()),
340            CoreError::Validation("test".to_string()),
341            CoreError::SecurityViolation("test".to_string()),
342            CoreError::Internal("test".to_string()),
343        ];
344
345        for error in errors {
346            let display_str = format!("{error}");
347            assert!(!display_str.is_empty());
348            assert!(display_str.contains("test"));
349        }
350
351        // Test complex error types
352        let utf8_err = CoreError::Utf8Error {
353            position: 42,
354            message: "test".to_string(),
355        };
356        let display_str = format!("{utf8_err}");
357        assert!(display_str.contains("42"));
358        assert!(display_str.contains("test"));
359
360        let feature_err = CoreError::FeatureNotSupported {
361            feature: "feature1".to_string(),
362            required_feature: "feature2".to_string(),
363        };
364        let display_str = format!("{feature_err}");
365        assert!(display_str.contains("feature1"));
366        assert!(display_str.contains("feature2"));
367
368        let version_err = CoreError::VersionIncompatible {
369            message: "version mismatch".to_string(),
370        };
371        let display_str = format!("{version_err}");
372        assert!(display_str.contains("version mismatch"));
373
374        let resource_err = CoreError::ResourceLimitExceeded {
375            resource: "memory".to_string(),
376            current: 100,
377            limit: 50,
378        };
379        let display_str = format!("{resource_err}");
380        assert!(display_str.contains("memory"));
381        assert!(display_str.contains("100"));
382        assert!(display_str.contains("50"));
383    }
384}