1use std::fmt;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32#[repr(u32)]
33pub enum ErrorCode {
34 InvalidHwpUnit = 1000,
36 InvalidColor = 1001,
38 IndexOutOfBounds = 1002,
40 EmptyIdentifier = 1003,
42 InvalidField = 1004,
44 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
54pub trait ErrorCodeExt {
70 fn code(&self) -> ErrorCode;
72}
73
74#[derive(Debug, Clone, thiserror::Error)]
93pub enum FoundationError {
94 #[error("invalid HwpUnit value {value}: must be in [{min}, {max}]")]
96 InvalidHwpUnit {
97 value: i64,
99 min: i32,
101 max: i32,
103 },
104
105 #[error("invalid color {component}: value {value}")]
107 InvalidColor {
108 component: String,
110 value: String,
112 },
113
114 #[error("index out of bounds: {type_name}[{index}] but max is {max}")]
116 IndexOutOfBounds {
117 index: usize,
119 max: usize,
121 type_name: &'static str,
123 },
124
125 #[error("{item} must not be empty")]
127 EmptyIdentifier {
128 item: String,
130 },
131
132 #[error("invalid field '{field}': {reason}")]
134 InvalidField {
135 field: String,
137 reason: String,
139 },
140
141 #[error("cannot parse '{value}' as {type_name}; valid values: {valid_values}")]
143 ParseError {
144 type_name: String,
146 value: String,
148 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
166pub type FoundationResult<T> = Result<T, FoundationError>;
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
317 fn error_code_is_copy_and_hashable() {
318 use std::collections::HashSet;
319 let code = ErrorCode::InvalidHwpUnit;
320 let code2 = code; 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 #[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 let msg = err.to_string();
340 assert!(!msg.is_empty());
341 }
342
343 #[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 #[test]
356 fn foundation_result_alias_compiles() {
357 fn example() -> FoundationResult<i32> {
358 Ok(42)
359 }
360 assert_eq!(example().unwrap(), 42);
361 }
362}