Skip to main content

philote_mdo/
error.rs

1//! Error types for Philote operations
2//!
3//! This module defines the error types used throughout the Philote library.
4//! All errors implement the standard [`std::error::Error`] trait using the
5//! [`thiserror`] crate for ergonomic error handling.
6//!
7//! # Error Categories
8//!
9//! - **Variable Errors**: [`VariableNotFound`], [`InvalidVariableType`], [`ShapeMismatch`]
10//! - **Lifecycle Errors**: [`DisciplineNotInitialized`], [`SetupNotCalled`]
11//! - **Communication Errors**: [`GrpcError`], [`ProtobufError`]
12//! - **Data Errors**: [`ArrayError`], [`IndexOutOfBounds`]
13//! - **Configuration Errors**: [`InvalidOption`], [`ConfigurationError`]
14//!
15//! [`VariableNotFound`]: PhiloteError::VariableNotFound
16//! [`InvalidVariableType`]: PhiloteError::InvalidVariableType
17//! [`ShapeMismatch`]: PhiloteError::ShapeMismatch
18//! [`DisciplineNotInitialized`]: PhiloteError::DisciplineNotInitialized
19//! [`SetupNotCalled`]: PhiloteError::SetupNotCalled
20//! [`GrpcError`]: PhiloteError::GrpcError
21//! [`ProtobufError`]: PhiloteError::ProtobufError
22//! [`ArrayError`]: PhiloteError::ArrayError
23//! [`IndexOutOfBounds`]: PhiloteError::IndexOutOfBounds
24//! [`InvalidOption`]: PhiloteError::InvalidOption
25//! [`ConfigurationError`]: PhiloteError::ConfigurationError
26//!
27//! # Example
28//!
29//! ```rust
30//! use philote_mdo::PhiloteError;
31//!
32//! fn find_variable(name: &str) -> Result<f64, PhiloteError> {
33//!     if name == "x" {
34//!         Ok(42.0)
35//!     } else {
36//!         Err(PhiloteError::VariableNotFound(name.to_string()))
37//!     }
38//! }
39//! ```
40
41use thiserror::Error;
42
43/// Error types for Philote operations
44#[derive(Error, Debug)]
45pub enum PhiloteError {
46    #[error("Variable '{0}' not found")]
47    VariableNotFound(String),
48
49    #[error("Shape mismatch: expected {expected:?}, got {actual:?}")]
50    ShapeMismatch {
51        expected: Vec<usize>,
52        actual: Vec<usize>,
53    },
54
55    #[error("Invalid variable type: {0}")]
56    InvalidVariableType(String),
57
58    #[error("Discipline not initialized")]
59    DisciplineNotInitialized,
60
61    #[error("Setup not called")]
62    SetupNotCalled,
63
64    #[error("Option '{name}' not found or has invalid type")]
65    InvalidOption { name: String },
66
67    #[error("Array index out of bounds: {index} >= {size}")]
68    IndexOutOfBounds { index: usize, size: usize },
69
70    #[error("gRPC communication error: {0}")]
71    GrpcError(Box<tonic::Status>),
72
73    #[error("Protocol buffer error: {0}")]
74    ProtobufError(#[from] prost::DecodeError),
75
76    #[error("I/O error: {0}")]
77    IoError(#[from] std::io::Error),
78
79    #[error("Array operation error: {0}")]
80    ArrayError(String),
81
82    #[error("Not implemented: {0}")]
83    NotImplemented(String),
84
85    #[error("Configuration error: {0}")]
86    ConfigurationError(String),
87
88    #[error("Operation cancelled")]
89    Cancelled,
90}
91
92impl PhiloteError {
93    /// Create an array operation error with a custom message
94    pub fn array_error<S: Into<String>>(msg: S) -> Self {
95        PhiloteError::ArrayError(msg.into())
96    }
97
98    /// Create a "not implemented" error for features not yet supported
99    pub fn not_implemented<S: Into<String>>(feature: S) -> Self {
100        PhiloteError::NotImplemented(feature.into())
101    }
102
103    /// Create a configuration error with a custom message
104    pub fn config_error<S: Into<String>>(msg: S) -> Self {
105        PhiloteError::ConfigurationError(msg.into())
106    }
107}
108
109impl From<tonic::Status> for PhiloteError {
110    fn from(status: tonic::Status) -> Self {
111        PhiloteError::GrpcError(Box::new(status))
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_array_error_helper() {
121        let err = PhiloteError::array_error("test message");
122        assert!(matches!(err, PhiloteError::ArrayError(_)));
123        assert_eq!(err.to_string(), "Array operation error: test message");
124    }
125
126    #[test]
127    fn test_not_implemented_helper() {
128        let err = PhiloteError::not_implemented("test feature");
129        assert!(matches!(err, PhiloteError::NotImplemented(_)));
130        assert_eq!(err.to_string(), "Not implemented: test feature");
131    }
132
133    #[test]
134    fn test_config_error_helper() {
135        let err = PhiloteError::config_error("test config");
136        assert!(matches!(err, PhiloteError::ConfigurationError(_)));
137        assert_eq!(err.to_string(), "Configuration error: test config");
138    }
139
140    #[test]
141    fn test_variable_not_found_display() {
142        let err = PhiloteError::VariableNotFound("x".to_string());
143        assert_eq!(err.to_string(), "Variable 'x' not found");
144    }
145
146    #[test]
147    fn test_shape_mismatch_display() {
148        let err = PhiloteError::ShapeMismatch {
149            expected: vec![2, 3],
150            actual: vec![3, 2],
151        };
152        assert_eq!(
153            err.to_string(),
154            "Shape mismatch: expected [2, 3], got [3, 2]"
155        );
156    }
157
158    #[test]
159    fn test_invalid_variable_type_display() {
160        let err = PhiloteError::InvalidVariableType("unknown".to_string());
161        assert_eq!(err.to_string(), "Invalid variable type: unknown");
162    }
163
164    #[test]
165    fn test_discipline_not_initialized() {
166        let err = PhiloteError::DisciplineNotInitialized;
167        assert_eq!(err.to_string(), "Discipline not initialized");
168    }
169
170    #[test]
171    fn test_setup_not_called() {
172        let err = PhiloteError::SetupNotCalled;
173        assert_eq!(err.to_string(), "Setup not called");
174    }
175
176    #[test]
177    fn test_invalid_option_display() {
178        let err = PhiloteError::InvalidOption {
179            name: "test_opt".to_string(),
180        };
181        assert_eq!(
182            err.to_string(),
183            "Option 'test_opt' not found or has invalid type"
184        );
185    }
186
187    #[test]
188    fn test_index_out_of_bounds_display() {
189        let err = PhiloteError::IndexOutOfBounds { index: 5, size: 3 };
190        assert_eq!(err.to_string(), "Array index out of bounds: 5 >= 3");
191    }
192
193    #[test]
194    fn test_from_tonic_status() {
195        let status = tonic::Status::internal("test error");
196        let err: PhiloteError = status.into();
197        assert!(matches!(err, PhiloteError::GrpcError(_)));
198        assert!(err.to_string().contains("test error"));
199    }
200
201    #[test]
202    fn test_from_io_error() {
203        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
204        let err: PhiloteError = io_err.into();
205        assert!(matches!(err, PhiloteError::IoError(_)));
206        assert!(err.to_string().contains("file not found"));
207    }
208}