Skip to main content

linreg_core/
error.rs

1//! Error types for the linear regression library.
2//!
3//! This module provides a comprehensive error type for all failure modes in
4//! linear regression operations, including matrix operations, statistical
5//! computations, and data parsing.
6
7use std::fmt;
8
9/// Error types for linear regression operations
10///
11/// # Example
12///
13/// ```
14/// # use linreg_core::Error;
15/// let err = Error::InvalidInput("negative value".to_string());
16/// assert!(err.to_string().contains("Invalid input"));
17/// ```
18#[derive(Debug, Clone, PartialEq)]
19pub enum Error {
20    /// Matrix is singular (perfect multicollinearity).
21    ///
22    /// This occurs when one or more predictor variables are linear combinations
23    /// of others, making the matrix non-invertible. Remove redundant variables
24    /// to resolve this error.
25    SingularMatrix,
26
27    /// Insufficient data points for the model.
28    ///
29    /// OLS regression requires more observations than predictor variables.
30    InsufficientData {
31        /// Minimum number of observations required
32        required: usize,
33        /// Actual number of observations available
34        available: usize,
35    },
36
37    /// Invalid input parameter.
38    ///
39    /// Indicates that an input parameter has an invalid value (e.g., negative
40    /// variance, empty data arrays, incompatible dimensions).
41    InvalidInput(String),
42
43    /// Dimension mismatch in matrix/vector operations.
44    ///
45    /// This occurs when the dimensions of matrices or vectors are incompatible
46    /// for the requested operation.
47    DimensionMismatch(String),
48
49    /// Computation failed due to numerical issues.
50    ///
51    /// This occurs when a numerical computation fails due to issues like
52    /// singularity, non-convergence, or overflow/underflow.
53    ComputationFailed(String),
54
55    /// Parse error for JSON/CSV data.
56    ///
57    /// Raised when input data cannot be parsed as JSON or CSV.
58    ParseError(String),
59
60    /// Domain check failed (for WASM with domain restriction enabled).
61    ///
62    /// By default, the WASM module allows all domains. This error is only returned
63    /// when the `LINREG_DOMAIN_RESTRICT` environment variable is set at build time
64    /// and the module is accessed from an unauthorized domain.
65    ///
66    /// To enable domain restriction:
67    /// ```bash
68    /// LINREG_DOMAIN_RESTRICT=example.com,yoursite.com wasm-pack build
69    /// ```
70    DomainCheck(String),
71
72    /// File I/O error during model save/load operations.
73    ///
74    /// Raised when reading or writing model files fails due to permissions,
75    /// missing files, or other I/O issues.
76    IoError(String),
77
78    /// Serialization error when converting model to JSON.
79    ///
80    /// Raised when a model cannot be serialized to JSON format.
81    SerializationError(String),
82
83    /// Deserialization error when parsing model from JSON.
84    ///
85    /// Raised when a JSON file cannot be parsed into a model structure.
86    DeserializationError(String),
87
88    /// Incompatible format version when loading a model.
89    ///
90    /// Raised when the format version of a saved model is not compatible
91    /// with the current library version.
92    IncompatibleFormatVersion {
93        /// Version from the file
94        file_version: String,
95        /// Version supported by this library
96        supported: String,
97    },
98
99    /// Model type mismatch when loading a model.
100    ///
101    /// Raised when attempting to load a model as the wrong type
102    /// (e.g., loading an OLS model as Ridge).
103    ModelTypeMismatch {
104        /// Expected model type
105        expected: String,
106        /// Actual model type found in file
107        found: String,
108    },
109}
110
111impl fmt::Display for Error {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            Error::SingularMatrix => {
115                write!(
116                    f,
117                    "Matrix is singular (perfect multicollinearity). Remove redundant variables."
118                )
119            },
120            Error::InsufficientData {
121                required,
122                available,
123            } => {
124                write!(
125                    f,
126                    "Insufficient data: need at least {} observations, have {}",
127                    required, available
128                )
129            },
130            Error::InvalidInput(msg) => {
131                write!(f, "Invalid input: {}", msg)
132            },
133            Error::DimensionMismatch(msg) => {
134                write!(f, "Dimension mismatch: {}", msg)
135            },
136            Error::ComputationFailed(msg) => {
137                write!(f, "Computation failed: {}", msg)
138            },
139            Error::ParseError(msg) => {
140                write!(f, "Parse error: {}", msg)
141            },
142            Error::DomainCheck(msg) => {
143                write!(f, "Domain check failed: {}", msg)
144            },
145            Error::IoError(msg) => {
146                write!(f, "I/O error: {}", msg)
147            },
148            Error::SerializationError(msg) => {
149                write!(f, "Serialization error: {}", msg)
150            },
151            Error::DeserializationError(msg) => {
152                write!(f, "Deserialization error: {}", msg)
153            },
154            Error::IncompatibleFormatVersion { file_version, supported } => {
155                write!(
156                    f,
157                    "Incompatible format version: file has version {}, supported version is {}",
158                    file_version, supported
159                )
160            },
161            Error::ModelTypeMismatch { expected, found } => {
162                write!(
163                    f,
164                    "Model type mismatch: expected {}, found {}",
165                    expected, found
166                )
167            },
168        }
169    }
170}
171
172impl std::error::Error for Error {}
173
174/// Result type for linear regression operations.
175///
176/// Alias for `std::result::Result<T, Error>`.
177///
178/// # Example
179///
180/// ```
181/// # use linreg_core::{Error, Result};
182/// # fn falls_back() -> Result<f64> {
183/// #     Ok(42.0)
184/// # }
185/// let result: Result<f64> = falls_back();
186/// assert_eq!(result.unwrap(), 42.0);
187/// ```
188pub type Result<T> = std::result::Result<T, Error>;
189
190// ============================================================================
191// Helper Functions for WASM Integration
192// ============================================================================
193//
194// These functions convert errors to JSON format for use in WASM bindings,
195// enabling proper error reporting to JavaScript code.
196
197/// Converts an error message to a JSON error string.
198///
199/// Creates a JSON object with a single "error" field containing the message.
200/// Used in WASM bindings to return error information to JavaScript.
201///
202/// # Examples
203///
204/// ```
205/// # use linreg_core::error_json;
206/// let json = error_json("Invalid input");
207/// assert_eq!(json, r#"{"error":"Invalid input"}"#);
208/// ```
209pub fn error_json(msg: &str) -> String {
210    serde_json::json!({ "error": msg }).to_string()
211}
212
213/// Converts an [`Error`] to a JSON error string.
214///
215/// Convenience function that converts any error variant to its display
216/// representation and wraps it in a JSON object.
217///
218/// # Examples
219///
220/// ```
221/// # use linreg_core::Error;
222/// # use linreg_core::error_to_json;
223/// let err = Error::SingularMatrix;
224/// let json = error_to_json(&err);
225/// assert!(json.contains("singular"));
226/// ```
227pub fn error_to_json(err: &Error) -> String {
228    error_json(&err.to_string())
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    /// Test Error::SingularMatrix Display implementation
236    #[test]
237    fn test_singular_matrix_display() {
238        let err = Error::SingularMatrix;
239        let msg = err.to_string();
240        assert!(msg.contains("singular"));
241        assert!(msg.contains("multicollinearity"));
242    }
243
244    /// Test Error::InsufficientData Display implementation
245    #[test]
246    fn test_insufficient_data_display() {
247        let err = Error::InsufficientData {
248            required: 10,
249            available: 5,
250        };
251        let msg = err.to_string();
252        assert!(msg.contains("Insufficient data"));
253        assert!(msg.contains("10"));
254        assert!(msg.contains("5"));
255    }
256
257    /// Test Error::InvalidInput Display implementation
258    #[test]
259    fn test_invalid_input_display() {
260        let err = Error::InvalidInput("negative value".to_string());
261        let msg = err.to_string();
262        assert!(msg.contains("Invalid input"));
263        assert!(msg.contains("negative value"));
264    }
265
266    /// Test Error::DimensionMismatch Display implementation
267    ///
268    /// Covers lines 95-96: DimensionMismatch Display impl
269    #[test]
270    fn test_dimension_mismatch_display() {
271        let err = Error::DimensionMismatch("matrix 3x3 cannot multiply with 2x2".to_string());
272        let msg = err.to_string();
273        assert!(msg.contains("Dimension mismatch"));
274        assert!(msg.contains("matrix 3x3"));
275    }
276
277    /// Test Error::ComputationFailed Display implementation
278    ///
279    /// Covers lines 98-99: ComputationFailed Display impl
280    #[test]
281    fn test_computation_failed_display() {
282        let err = Error::ComputationFailed("QR decomposition failed".to_string());
283        let msg = err.to_string();
284        assert!(msg.contains("Computation failed"));
285        assert!(msg.contains("QR decomposition"));
286    }
287
288    /// Test Error::ParseError Display implementation
289    #[test]
290    fn test_parse_error_display() {
291        let err = Error::ParseError("invalid JSON syntax".to_string());
292        let msg = err.to_string();
293        assert!(msg.contains("Parse error"));
294        assert!(msg.contains("JSON"));
295    }
296
297    /// Test Error::DomainCheck Display implementation
298    #[test]
299    fn test_domain_check_display() {
300        let err = Error::DomainCheck("unauthorized domain".to_string());
301        let msg = err.to_string();
302        assert!(msg.contains("Domain check failed"));
303        assert!(msg.contains("unauthorized"));
304    }
305
306    /// Test error_json function
307    #[test]
308    fn test_error_json() {
309        let json = error_json("test error");
310        assert_eq!(json, r#"{"error":"test error"}"#);
311    }
312
313    /// Test error_to_json function with SingularMatrix
314    #[test]
315    fn test_error_to_json_singular_matrix() {
316        let err = Error::SingularMatrix;
317        let json = error_to_json(&err);
318        assert!(json.contains(r#""error":"#));
319        assert!(json.contains("singular"));
320    }
321
322    /// Test error_to_json function with DimensionMismatch
323    #[test]
324    fn test_error_to_json_dimension_mismatch() {
325        let err = Error::DimensionMismatch("incompatible dimensions".to_string());
326        let json = error_to_json(&err);
327        assert!(json.contains(r#""error":"#));
328        assert!(json.contains("Dimension"));
329    }
330
331    /// Test error_to_json function with ComputationFailed
332    #[test]
333    fn test_error_to_json_computation_failed() {
334        let err = Error::ComputationFailed("convergence failure".to_string());
335        let json = error_to_json(&err);
336        assert!(json.contains(r#""error":"#));
337        assert!(json.contains("Computation"));
338    }
339
340    /// Test Error PartialEq implementation
341    #[test]
342    fn test_error_partial_eq() {
343        let err1 = Error::SingularMatrix;
344        let err2 = Error::SingularMatrix;
345        let err3 = Error::InvalidInput("test".to_string());
346
347        assert_eq!(err1, err2);
348        assert_ne!(err1, err3);
349    }
350
351    /// Test Error Clone implementation
352    #[test]
353    fn test_error_clone() {
354        let err1 = Error::InvalidInput("test".to_string());
355        let err2 = err1.clone();
356        assert_eq!(err1, err2);
357    }
358
359    /// Test Error Debug implementation
360    #[test]
361    fn test_error_debug() {
362        let err = Error::ComputationFailed("test failure".to_string());
363        let debug_str = format!("{:?}", err);
364        assert!(debug_str.contains("ComputationFailed"));
365    }
366
367    /// Test Result type alias
368    #[test]
369    fn test_result_type_alias() {
370        fn returns_ok() -> Result<f64> {
371            Ok(42.0)
372        }
373        fn returns_err() -> Result<f64> {
374            Err(Error::InvalidInput("test".to_string()))
375        }
376
377        assert_eq!(returns_ok().unwrap(), 42.0);
378        assert!(returns_err().is_err());
379    }
380
381    /// Test Error::IoError Display implementation
382    #[test]
383    fn test_io_error_display() {
384        let err = Error::IoError("Failed to open file".to_string());
385        let msg = err.to_string();
386        assert!(msg.contains("I/O error"));
387        assert!(msg.contains("Failed to open file"));
388    }
389
390    /// Test Error::SerializationError Display implementation
391    #[test]
392    fn test_serialization_error_display() {
393        let err = Error::SerializationError("Failed to serialize model".to_string());
394        let msg = err.to_string();
395        assert!(msg.contains("Serialization error"));
396        assert!(msg.contains("Failed to serialize"));
397    }
398
399    /// Test Error::DeserializationError Display implementation
400    #[test]
401    fn test_deserialization_error_display() {
402        let err = Error::DeserializationError("Invalid JSON".to_string());
403        let msg = err.to_string();
404        assert!(msg.contains("Deserialization error"));
405        assert!(msg.contains("Invalid JSON"));
406    }
407
408    /// Test Error::IncompatibleFormatVersion Display implementation
409    #[test]
410    fn test_incompatible_format_version_display() {
411        let err = Error::IncompatibleFormatVersion {
412            file_version: "2.0".to_string(),
413            supported: "1.0".to_string(),
414        };
415        let msg = err.to_string();
416        assert!(msg.contains("Incompatible format version"));
417        assert!(msg.contains("2.0"));
418        assert!(msg.contains("1.0"));
419    }
420
421    /// Test Error::ModelTypeMismatch Display implementation
422    #[test]
423    fn test_model_type_mismatch_display() {
424        let err = Error::ModelTypeMismatch {
425            expected: "OLS".to_string(),
426            found: "Ridge".to_string(),
427        };
428        let msg = err.to_string();
429        assert!(msg.contains("Model type mismatch"));
430        assert!(msg.contains("OLS"));
431        assert!(msg.contains("Ridge"));
432    }
433
434    /// Test serialization errors work with error_to_json
435    #[test]
436    fn test_error_to_json_serialization() {
437        let err = Error::SerializationError("test".to_string());
438        let json = error_to_json(&err);
439        assert!(json.contains(r#""error":"#));
440        assert!(json.contains("Serialization"));
441    }
442
443    /// Test deserialization errors work with error_to_json
444    #[test]
445    fn test_error_to_json_deserialization() {
446        let err = Error::DeserializationError("test".to_string());
447        let json = error_to_json(&err);
448        assert!(json.contains(r#""error":"#));
449        assert!(json.contains("Deserialization"));
450    }
451}