Skip to main content

mabi_core/
error.rs

1//! Error types for the simulator.
2//!
3//! This module provides comprehensive error handling with:
4//! - Detailed error variants for different failure modes
5//! - Validation error support with field-level details
6//! - Error context and chaining
7//! - Recoverable vs. fatal error classification
8
9use std::collections::HashMap;
10use std::io;
11use thiserror::Error;
12
13/// Result type alias using [`Error`].
14pub type Result<T> = std::result::Result<T, Error>;
15
16/// Simulator error types.
17#[derive(Debug, Error)]
18pub enum Error {
19    /// Device not found.
20    #[error("Device not found: {device_id}")]
21    DeviceNotFound { device_id: String },
22
23    /// Device already exists.
24    #[error("Device already exists: {device_id}")]
25    DeviceAlreadyExists { device_id: String },
26
27    /// Data point not found.
28    #[error("Data point not found: {device_id}/{point_id}")]
29    DataPointNotFound { device_id: String, point_id: String },
30
31    /// Invalid address.
32    #[error("Invalid address: {address} (expected range: {min}-{max})")]
33    InvalidAddress { address: u32, min: u32, max: u32 },
34
35    /// Invalid value.
36    #[error("Invalid value for data point {point_id}: {reason}")]
37    InvalidValue { point_id: String, reason: String },
38
39    /// Type mismatch.
40    #[error("Type mismatch: expected {expected}, got {actual}")]
41    TypeMismatch { expected: String, actual: String },
42
43    /// Protocol error.
44    #[error("Protocol error: {0}")]
45    Protocol(String),
46
47    /// Configuration error.
48    #[error("Configuration error: {0}")]
49    Config(String),
50
51    /// Validation errors (multiple field errors).
52    #[error("Validation failed: {message}")]
53    Validation {
54        message: String,
55        errors: ValidationErrors,
56    },
57
58    /// I/O error.
59    #[error("I/O error: {0}")]
60    Io(#[from] io::Error),
61
62    /// Engine error.
63    #[error("Engine error: {0}")]
64    Engine(String),
65
66    /// Lifecycle error.
67    #[error("Lifecycle error: invalid transition from {from:?} to {to:?}")]
68    Lifecycle {
69        from: crate::device::DeviceState,
70        to: crate::device::DeviceState,
71    },
72
73    /// Capacity exceeded.
74    #[error("Capacity exceeded: {current}/{max} {resource}")]
75    CapacityExceeded {
76        current: usize,
77        max: usize,
78        resource: String,
79    },
80
81    /// Timeout.
82    #[error("Operation timed out after {duration_ms}ms")]
83    Timeout { duration_ms: u64 },
84
85    /// Not supported.
86    #[error("Operation not supported: {0}")]
87    NotSupported(String),
88
89    /// Internal error.
90    #[error("Internal error: {0}")]
91    Internal(String),
92
93    /// Serialization error.
94    #[error("Serialization error: {0}")]
95    Serialization(String),
96
97    /// Channel error.
98    #[error("Channel error: {0}")]
99    Channel(String),
100
101    /// Access denied (for access mode violations).
102    #[error("Access denied: {operation} not allowed on {point_id} (mode: {mode})")]
103    AccessDenied {
104        point_id: String,
105        operation: String,
106        mode: String,
107    },
108
109    /// Range error (for out-of-range values).
110    #[error("Value {value} out of range [{min}, {max}] for {point_id}")]
111    OutOfRange {
112        point_id: String,
113        value: f64,
114        min: f64,
115        max: f64,
116    },
117}
118
119impl Error {
120    /// Create a device not found error.
121    pub fn device_not_found(device_id: impl Into<String>) -> Self {
122        Self::DeviceNotFound {
123            device_id: device_id.into(),
124        }
125    }
126
127    /// Create a data point not found error.
128    pub fn point_not_found(device_id: impl Into<String>, point_id: impl Into<String>) -> Self {
129        Self::DataPointNotFound {
130            device_id: device_id.into(),
131            point_id: point_id.into(),
132        }
133    }
134
135    /// Create a capacity exceeded error.
136    pub fn capacity_exceeded(current: usize, max: usize, resource: impl Into<String>) -> Self {
137        Self::CapacityExceeded {
138            current,
139            max,
140            resource: resource.into(),
141        }
142    }
143
144    /// Create an access denied error.
145    pub fn access_denied(
146        point_id: impl Into<String>,
147        operation: impl Into<String>,
148        mode: impl Into<String>,
149    ) -> Self {
150        Self::AccessDenied {
151            point_id: point_id.into(),
152            operation: operation.into(),
153            mode: mode.into(),
154        }
155    }
156
157    /// Create an out of range error.
158    pub fn out_of_range(point_id: impl Into<String>, value: f64, min: f64, max: f64) -> Self {
159        Self::OutOfRange {
160            point_id: point_id.into(),
161            value,
162            min,
163            max,
164        }
165    }
166
167    /// Create a validation error from a ValidationErrors instance.
168    pub fn validation(errors: ValidationErrors) -> Self {
169        let count = errors.len();
170        Self::Validation {
171            message: format!("{} validation error(s)", count),
172            errors,
173        }
174    }
175
176    /// Check if this error is recoverable.
177    pub fn is_recoverable(&self) -> bool {
178        matches!(self, Self::Timeout { .. } | Self::Io(_) | Self::Channel(_))
179    }
180
181    /// Check if this is a validation error.
182    pub fn is_validation(&self) -> bool {
183        matches!(self, Self::Validation { .. })
184    }
185
186    /// Check if this is a not found error.
187    pub fn is_not_found(&self) -> bool {
188        matches!(
189            self,
190            Self::DeviceNotFound { .. } | Self::DataPointNotFound { .. }
191        )
192    }
193
194    /// Get error severity.
195    pub fn severity(&self) -> ErrorSeverity {
196        match self {
197            Self::Internal(_) | Self::Engine(_) => ErrorSeverity::Critical,
198            Self::Protocol(_) | Self::Lifecycle { .. } => ErrorSeverity::High,
199            Self::Timeout { .. } | Self::Io(_) | Self::Channel(_) => ErrorSeverity::Medium,
200            Self::Validation { .. }
201            | Self::InvalidValue { .. }
202            | Self::TypeMismatch { .. }
203            | Self::OutOfRange { .. }
204            | Self::AccessDenied { .. } => ErrorSeverity::Low,
205            _ => ErrorSeverity::Medium,
206        }
207    }
208}
209
210/// Error severity levels.
211#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
212pub enum ErrorSeverity {
213    /// Low severity - validation errors, user input errors.
214    Low,
215    /// Medium severity - recoverable errors, timeouts.
216    Medium,
217    /// High severity - protocol errors, state errors.
218    High,
219    /// Critical severity - internal errors, engine failures.
220    Critical,
221}
222
223/// Collection of validation errors with field-level details.
224#[derive(Debug, Clone, Default)]
225pub struct ValidationErrors {
226    errors: HashMap<String, Vec<String>>,
227}
228
229impl ValidationErrors {
230    /// Create a new empty validation errors collection.
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    /// Add an error for a specific field.
236    pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
237        self.errors
238            .entry(field.into())
239            .or_default()
240            .push(message.into());
241    }
242
243    /// Add error if condition is true.
244    pub fn add_if(
245        &mut self,
246        condition: bool,
247        field: impl Into<String>,
248        message: impl Into<String>,
249    ) {
250        if condition {
251            self.add(field, message);
252        }
253    }
254
255    /// Check if there are any errors.
256    pub fn is_empty(&self) -> bool {
257        self.errors.is_empty()
258    }
259
260    /// Get the number of fields with errors.
261    pub fn len(&self) -> usize {
262        self.errors.len()
263    }
264
265    /// Get total error count across all fields.
266    pub fn total_errors(&self) -> usize {
267        self.errors.values().map(|v| v.len()).sum()
268    }
269
270    /// Get errors for a specific field.
271    pub fn get(&self, field: &str) -> Option<&Vec<String>> {
272        self.errors.get(field)
273    }
274
275    /// Get all field names with errors.
276    pub fn fields(&self) -> impl Iterator<Item = &String> {
277        self.errors.keys()
278    }
279
280    /// Iterate over all errors.
281    pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
282        self.errors.iter()
283    }
284
285    /// Convert to an Error if not empty.
286    pub fn into_result<T>(self, ok: T) -> Result<T> {
287        if self.is_empty() {
288            Ok(ok)
289        } else {
290            Err(Error::validation(self))
291        }
292    }
293
294    /// Merge with another ValidationErrors.
295    pub fn merge(&mut self, other: ValidationErrors) {
296        for (field, messages) in other.errors {
297            self.errors.entry(field).or_default().extend(messages);
298        }
299    }
300}
301
302impl std::fmt::Display for ValidationErrors {
303    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304        let mut first = true;
305        for (field, messages) in &self.errors {
306            for message in messages {
307                if !first {
308                    write!(f, "; ")?;
309                }
310                write!(f, "{}: {}", field, message)?;
311                first = false;
312            }
313        }
314        Ok(())
315    }
316}
317
318/// Builder for ValidationErrors.
319#[derive(Debug, Default)]
320pub struct ValidationErrorsBuilder {
321    errors: ValidationErrors,
322}
323
324impl ValidationErrorsBuilder {
325    /// Create a new builder.
326    pub fn new() -> Self {
327        Self::default()
328    }
329
330    /// Add an error.
331    pub fn add(mut self, field: impl Into<String>, message: impl Into<String>) -> Self {
332        self.errors.add(field, message);
333        self
334    }
335
336    /// Add error if condition is true.
337    pub fn add_if(
338        mut self,
339        condition: bool,
340        field: impl Into<String>,
341        message: impl Into<String>,
342    ) -> Self {
343        self.errors.add_if(condition, field, message);
344        self
345    }
346
347    /// Build and return the ValidationErrors.
348    pub fn build(self) -> ValidationErrors {
349        self.errors
350    }
351
352    /// Build and return as Result.
353    pub fn into_result<T>(self, ok: T) -> Result<T> {
354        self.errors.into_result(ok)
355    }
356}
357
358impl From<serde_json::Error> for Error {
359    fn from(err: serde_json::Error) -> Self {
360        Self::Serialization(err.to_string())
361    }
362}
363
364impl From<serde_yaml::Error> for Error {
365    fn from(err: serde_yaml::Error) -> Self {
366        Self::Serialization(err.to_string())
367    }
368}
369
370/// Extension trait for Result to add context.
371pub trait ResultExt<T> {
372    /// Add context to an error.
373    fn with_context<F, S>(self, f: F) -> Result<T>
374    where
375        F: FnOnce() -> S,
376        S: Into<String>;
377}
378
379impl<T> ResultExt<T> for Result<T> {
380    fn with_context<F, S>(self, f: F) -> Result<T>
381    where
382        F: FnOnce() -> S,
383        S: Into<String>,
384    {
385        self.map_err(|e| {
386            let context = f().into();
387            Error::Internal(format!("{}: {}", context, e))
388        })
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    #[test]
397    fn test_error_display() {
398        let err = Error::device_not_found("device-001");
399        assert_eq!(err.to_string(), "Device not found: device-001");
400    }
401
402    #[test]
403    fn test_error_recoverable() {
404        assert!(Error::Timeout { duration_ms: 1000 }.is_recoverable());
405        assert!(!Error::DeviceNotFound {
406            device_id: "x".into()
407        }
408        .is_recoverable());
409    }
410
411    #[test]
412    fn test_validation_errors() {
413        let mut errors = ValidationErrors::new();
414        errors.add("name", "cannot be empty");
415        errors.add("name", "must be at least 3 characters");
416        errors.add("id", "must be unique");
417
418        assert_eq!(errors.len(), 2);
419        assert_eq!(errors.total_errors(), 3);
420        assert_eq!(errors.get("name").unwrap().len(), 2);
421    }
422
423    #[test]
424    fn test_validation_errors_builder() {
425        let errors = ValidationErrorsBuilder::new()
426            .add("field1", "error1")
427            .add_if(true, "field2", "error2")
428            .add_if(false, "field3", "error3") // Should not be added
429            .build();
430
431        assert_eq!(errors.len(), 2);
432        assert!(errors.get("field3").is_none());
433    }
434
435    #[test]
436    fn test_validation_into_result() {
437        let errors = ValidationErrors::new();
438        let result: Result<i32> = errors.into_result(42);
439        assert!(result.is_ok());
440
441        let mut errors = ValidationErrors::new();
442        errors.add("field", "error");
443        let result: Result<i32> = errors.into_result(42);
444        assert!(result.is_err());
445    }
446
447    #[test]
448    fn test_error_severity() {
449        assert_eq!(
450            Error::Internal("test".into()).severity(),
451            ErrorSeverity::Critical
452        );
453        assert_eq!(
454            Error::Timeout { duration_ms: 1000 }.severity(),
455            ErrorSeverity::Medium
456        );
457        assert_eq!(
458            Error::InvalidValue {
459                point_id: "x".into(),
460                reason: "y".into()
461            }
462            .severity(),
463            ErrorSeverity::Low
464        );
465    }
466
467    #[test]
468    fn test_access_denied_error() {
469        let err = Error::access_denied("temp", "write", "readonly");
470        assert_eq!(
471            err.to_string(),
472            "Access denied: write not allowed on temp (mode: readonly)"
473        );
474    }
475
476    #[test]
477    fn test_out_of_range_error() {
478        let err = Error::out_of_range("temp", 150.0, 0.0, 100.0);
479        assert_eq!(
480            err.to_string(),
481            "Value 150 out of range [0, 100] for temp"
482        );
483    }
484}