hwpforge_foundation/
error.rs

1//! Error types for the HwpForge Foundation crate.
2//!
3//! All Foundation types return [`FoundationError`] on validation failure.
4//! Each error variant carries enough context for debugging.
5//!
6//! # Error Code Ranges
7//!
8//! | Range | Crate |
9//! |-------|-------|
10//! | 1000-1999 | Foundation |
11//! | 2000-2999 | Core |
12//! | 3000-3999 | Blueprint |
13//! | 4000-4999 | Smithy-HWPX |
14//! | 5000-5999 | Smithy-HWP5 |
15//! | 6000-6999 | Smithy-MD |
16
17use std::fmt;
18
19/// Numeric error codes for programmatic handling and FFI.
20///
21/// Foundation errors occupy the 1000-1999 range.
22///
23/// # Examples
24///
25/// ```
26/// use hwpforge_foundation::ErrorCode;
27///
28/// let code = ErrorCode::InvalidHwpUnit;
29/// assert_eq!(code as u32, 1000);
30/// ```
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32#[repr(u32)]
33pub enum ErrorCode {
34    /// HwpUnit value out of valid range.
35    InvalidHwpUnit = 1000,
36    /// Color component or raw value invalid.
37    InvalidColor = 1001,
38    /// Branded index exceeded collection bounds.
39    IndexOutOfBounds = 1002,
40    /// String identifier was empty or invalid.
41    EmptyIdentifier = 1003,
42    /// Generic field validation failure.
43    InvalidField = 1004,
44    /// String-to-enum parsing failure.
45    ParseError = 1005,
46}
47
48impl fmt::Display for ErrorCode {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(f, "E{:04}", *self as u32)
51    }
52}
53
54/// Trait for mapping domain errors to numeric [`ErrorCode`] values.
55///
56/// Each crate's error type implements this to provide a stable,
57/// FFI-safe error code.
58///
59/// # Examples
60///
61/// ```
62/// use hwpforge_foundation::{FoundationError, ErrorCodeExt, ErrorCode};
63///
64/// let err = FoundationError::EmptyIdentifier {
65///     item: "FontId".to_string(),
66/// };
67/// assert_eq!(err.code(), ErrorCode::EmptyIdentifier);
68/// ```
69pub trait ErrorCodeExt {
70    /// Returns the numeric error code for this error.
71    fn code(&self) -> ErrorCode;
72}
73
74/// The primary error type for the Foundation crate.
75///
76/// Returned by constructors and validators when input violates
77/// constraints. Every variant carries enough context to produce
78/// a meaningful diagnostic message.
79///
80/// # Examples
81///
82/// ```
83/// use hwpforge_foundation::FoundationError;
84///
85/// let err = FoundationError::InvalidHwpUnit {
86///     value: 999_999_999,
87///     min: -100_000_000,
88///     max: 100_000_000,
89/// };
90/// assert!(err.to_string().contains("999999999"));
91/// ```
92#[derive(Debug, Clone, thiserror::Error)]
93pub enum FoundationError {
94    /// An HwpUnit value was outside the valid range.
95    #[error("invalid HwpUnit value {value}: must be in [{min}, {max}]")]
96    InvalidHwpUnit {
97        /// The rejected value (as i64 to avoid truncation in error messages).
98        value: i64,
99        /// Minimum allowed value.
100        min: i32,
101        /// Maximum allowed value.
102        max: i32,
103    },
104
105    /// A Color value or component was invalid.
106    #[error("invalid color {component}: value {value}")]
107    InvalidColor {
108        /// Which component failed (e.g. "red", "raw").
109        component: String,
110        /// The rejected value.
111        value: String,
112    },
113
114    /// A branded index exceeded the collection bounds.
115    #[error("index out of bounds: {type_name}[{index}] but max is {max}")]
116    IndexOutOfBounds {
117        /// The rejected index value.
118        index: usize,
119        /// The upper bound (exclusive).
120        max: usize,
121        /// The phantom type name for diagnostics.
122        type_name: &'static str,
123    },
124
125    /// A string identifier was empty.
126    #[error("{item} must not be empty")]
127    EmptyIdentifier {
128        /// What kind of identifier (e.g. "FontId", "TemplateName").
129        item: String,
130    },
131
132    /// A generic field validation failure.
133    #[error("invalid field '{field}': {reason}")]
134    InvalidField {
135        /// The field that failed validation.
136        field: String,
137        /// Why validation failed.
138        reason: String,
139    },
140
141    /// A string could not be parsed into the target enum.
142    #[error("cannot parse '{value}' as {type_name}; valid values: {valid_values}")]
143    ParseError {
144        /// The target type name (e.g. "Alignment").
145        type_name: String,
146        /// The rejected input string.
147        value: String,
148        /// Comma-separated list of valid values.
149        valid_values: String,
150    },
151}
152
153impl ErrorCodeExt for FoundationError {
154    fn code(&self) -> ErrorCode {
155        match self {
156            Self::InvalidHwpUnit { .. } => ErrorCode::InvalidHwpUnit,
157            Self::InvalidColor { .. } => ErrorCode::InvalidColor,
158            Self::IndexOutOfBounds { .. } => ErrorCode::IndexOutOfBounds,
159            Self::EmptyIdentifier { .. } => ErrorCode::EmptyIdentifier,
160            Self::InvalidField { .. } => ErrorCode::InvalidField,
161            Self::ParseError { .. } => ErrorCode::ParseError,
162        }
163    }
164}
165
166/// Convenience type alias for Foundation operations.
167pub type FoundationResult<T> = Result<T, FoundationError>;
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    // === Edge Case 1: All variants constructible ===
174
175    #[test]
176    fn error_invalid_hwpunit_displays_value_and_range() {
177        let err = FoundationError::InvalidHwpUnit {
178            value: 999_999_999,
179            min: -100_000_000,
180            max: 100_000_000,
181        };
182        let msg = err.to_string();
183        assert!(msg.contains("999999999"), "should contain value: {msg}");
184        assert!(msg.contains("-100000000"), "should contain min: {msg}");
185        assert!(msg.contains("100000000"), "should contain max: {msg}");
186    }
187
188    #[test]
189    fn error_invalid_color_displays_component_and_value() {
190        let err = FoundationError::InvalidColor {
191            component: "red".to_string(),
192            value: "256".to_string(),
193        };
194        let msg = err.to_string();
195        assert!(msg.contains("red"), "should contain component: {msg}");
196        assert!(msg.contains("256"), "should contain value: {msg}");
197    }
198
199    #[test]
200    fn error_index_out_of_bounds_displays_context() {
201        let err = FoundationError::IndexOutOfBounds { index: 42, max: 10, type_name: "CharShape" };
202        let msg = err.to_string();
203        assert!(msg.contains("42"), "should contain index: {msg}");
204        assert!(msg.contains("10"), "should contain max: {msg}");
205        assert!(msg.contains("CharShape"), "should contain type name: {msg}");
206    }
207
208    #[test]
209    fn error_empty_identifier_displays_item() {
210        let err = FoundationError::EmptyIdentifier { item: "FontId".to_string() };
211        let msg = err.to_string();
212        assert!(msg.contains("FontId"), "should contain item: {msg}");
213        assert!(msg.contains("empty"), "should mention empty: {msg}");
214    }
215
216    #[test]
217    fn error_invalid_field_displays_field_and_reason() {
218        let err = FoundationError::InvalidField {
219            field: "width".to_string(),
220            reason: "must be positive".to_string(),
221        };
222        let msg = err.to_string();
223        assert!(msg.contains("width"), "should contain field: {msg}");
224        assert!(msg.contains("must be positive"), "should contain reason: {msg}");
225    }
226
227    #[test]
228    fn error_parse_error_displays_type_value_and_valid() {
229        let err = FoundationError::ParseError {
230            type_name: "Alignment".to_string(),
231            value: "leftt".to_string(),
232            valid_values: "Left, Center, Right, Justify".to_string(),
233        };
234        let msg = err.to_string();
235        assert!(msg.contains("Alignment"), "should contain type: {msg}");
236        assert!(msg.contains("leftt"), "should contain value: {msg}");
237        assert!(msg.contains("Left"), "should contain valid values: {msg}");
238    }
239
240    // === Edge Case 2: ErrorCode numeric values ===
241
242    #[test]
243    fn error_codes_in_foundation_range() {
244        assert_eq!(ErrorCode::InvalidHwpUnit as u32, 1000);
245        assert_eq!(ErrorCode::InvalidColor as u32, 1001);
246        assert_eq!(ErrorCode::IndexOutOfBounds as u32, 1002);
247        assert_eq!(ErrorCode::EmptyIdentifier as u32, 1003);
248        assert_eq!(ErrorCode::InvalidField as u32, 1004);
249        assert_eq!(ErrorCode::ParseError as u32, 1005);
250    }
251
252    // === Edge Case 3: ErrorCode Display as E#### ===
253
254    #[test]
255    fn error_code_display_format() {
256        assert_eq!(ErrorCode::InvalidHwpUnit.to_string(), "E1000");
257        assert_eq!(ErrorCode::ParseError.to_string(), "E1005");
258    }
259
260    // === Edge Case 4: ErrorCodeExt mapping ===
261
262    #[test]
263    fn error_code_ext_maps_all_variants() {
264        let cases: Vec<(FoundationError, ErrorCode)> = vec![
265            (
266                FoundationError::InvalidHwpUnit { value: 0, min: 0, max: 0 },
267                ErrorCode::InvalidHwpUnit,
268            ),
269            (
270                FoundationError::InvalidColor { component: String::new(), value: String::new() },
271                ErrorCode::InvalidColor,
272            ),
273            (
274                FoundationError::IndexOutOfBounds { index: 0, max: 0, type_name: "" },
275                ErrorCode::IndexOutOfBounds,
276            ),
277            (FoundationError::EmptyIdentifier { item: String::new() }, ErrorCode::EmptyIdentifier),
278            (
279                FoundationError::InvalidField { field: String::new(), reason: String::new() },
280                ErrorCode::InvalidField,
281            ),
282            (
283                FoundationError::ParseError {
284                    type_name: String::new(),
285                    value: String::new(),
286                    valid_values: String::new(),
287                },
288                ErrorCode::ParseError,
289            ),
290        ];
291        for (err, expected_code) in cases {
292            assert_eq!(err.code(), expected_code, "mismatch for {err:?}");
293        }
294    }
295
296    // === Edge Case 5: Error is Send + Sync ===
297
298    #[test]
299    fn error_is_send_and_sync() {
300        fn assert_send<T: Send>() {}
301        fn assert_sync<T: Sync>() {}
302        assert_send::<FoundationError>();
303        assert_sync::<FoundationError>();
304    }
305
306    // === Edge Case 6: Error implements std::error::Error ===
307
308    #[test]
309    fn error_implements_std_error() {
310        let err = FoundationError::InvalidHwpUnit { value: 0, min: -1, max: 1 };
311        let _: &dyn std::error::Error = &err;
312    }
313
314    // === Edge Case 7: ErrorCode derives Clone, Copy, Hash ===
315
316    #[test]
317    fn error_code_is_copy_and_hashable() {
318        use std::collections::HashSet;
319        let code = ErrorCode::InvalidHwpUnit;
320        let code2 = code; // Copy
321        assert_eq!(code, code2);
322
323        let mut set = HashSet::new();
324        set.insert(ErrorCode::InvalidHwpUnit);
325        set.insert(ErrorCode::InvalidColor);
326        assert_eq!(set.len(), 2);
327    }
328
329    // === Edge Case 8: Empty string fields in error ===
330
331    #[test]
332    fn error_handles_empty_string_fields_gracefully() {
333        let err = FoundationError::ParseError {
334            type_name: String::new(),
335            value: String::new(),
336            valid_values: String::new(),
337        };
338        // Should not panic
339        let msg = err.to_string();
340        assert!(!msg.is_empty());
341    }
342
343    // === Edge Case 9: Very long strings in error ===
344
345    #[test]
346    fn error_handles_long_strings() {
347        let long = "x".repeat(10_000);
348        let err = FoundationError::InvalidField { field: long.clone(), reason: long.clone() };
349        let msg = err.to_string();
350        assert!(msg.len() > 10_000);
351    }
352
353    // === Edge Case 10: FoundationResult alias works ===
354
355    #[test]
356    fn foundation_result_alias_compiles() {
357        fn example() -> FoundationResult<i32> {
358            Ok(42)
359        }
360        assert_eq!(example().unwrap(), 42);
361    }
362}