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
73impl fmt::Display for Error {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Error::SingularMatrix => {
77                write!(
78                    f,
79                    "Matrix is singular (perfect multicollinearity). Remove redundant variables."
80                )
81            },
82            Error::InsufficientData {
83                required,
84                available,
85            } => {
86                write!(
87                    f,
88                    "Insufficient data: need at least {} observations, have {}",
89                    required, available
90                )
91            },
92            Error::InvalidInput(msg) => {
93                write!(f, "Invalid input: {}", msg)
94            },
95            Error::DimensionMismatch(msg) => {
96                write!(f, "Dimension mismatch: {}", msg)
97            },
98            Error::ComputationFailed(msg) => {
99                write!(f, "Computation failed: {}", msg)
100            },
101            Error::ParseError(msg) => {
102                write!(f, "Parse error: {}", msg)
103            },
104            Error::DomainCheck(msg) => {
105                write!(f, "Domain check failed: {}", msg)
106            },
107        }
108    }
109}
110
111impl std::error::Error for Error {}
112
113/// Result type for linear regression operations.
114///
115/// Alias for `std::result::Result<T, Error>`.
116///
117/// # Example
118///
119/// ```
120/// # use linreg_core::{Error, Result};
121/// # fn falls_back() -> Result<f64> {
122/// #     Ok(42.0)
123/// # }
124/// let result: Result<f64> = falls_back();
125/// assert_eq!(result.unwrap(), 42.0);
126/// ```
127pub type Result<T> = std::result::Result<T, Error>;
128
129// ============================================================================
130// Helper Functions for WASM Integration
131// ============================================================================
132//
133// These functions convert errors to JSON format for use in WASM bindings,
134// enabling proper error reporting to JavaScript code.
135
136/// Converts an error message to a JSON error string.
137///
138/// Creates a JSON object with a single "error" field containing the message.
139/// Used in WASM bindings to return error information to JavaScript.
140///
141/// # Examples
142///
143/// ```
144/// # use linreg_core::error_json;
145/// let json = error_json("Invalid input");
146/// assert_eq!(json, r#"{"error":"Invalid input"}"#);
147/// ```
148pub fn error_json(msg: &str) -> String {
149    serde_json::json!({ "error": msg }).to_string()
150}
151
152/// Converts an [`Error`] to a JSON error string.
153///
154/// Convenience function that converts any error variant to its display
155/// representation and wraps it in a JSON object.
156///
157/// # Examples
158///
159/// ```
160/// # use linreg_core::Error;
161/// # use linreg_core::error_to_json;
162/// let err = Error::SingularMatrix;
163/// let json = error_to_json(&err);
164/// assert!(json.contains("singular"));
165/// ```
166pub fn error_to_json(err: &Error) -> String {
167    error_json(&err.to_string())
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    /// Test Error::SingularMatrix Display implementation
175    #[test]
176    fn test_singular_matrix_display() {
177        let err = Error::SingularMatrix;
178        let msg = err.to_string();
179        assert!(msg.contains("singular"));
180        assert!(msg.contains("multicollinearity"));
181    }
182
183    /// Test Error::InsufficientData Display implementation
184    #[test]
185    fn test_insufficient_data_display() {
186        let err = Error::InsufficientData {
187            required: 10,
188            available: 5,
189        };
190        let msg = err.to_string();
191        assert!(msg.contains("Insufficient data"));
192        assert!(msg.contains("10"));
193        assert!(msg.contains("5"));
194    }
195
196    /// Test Error::InvalidInput Display implementation
197    #[test]
198    fn test_invalid_input_display() {
199        let err = Error::InvalidInput("negative value".to_string());
200        let msg = err.to_string();
201        assert!(msg.contains("Invalid input"));
202        assert!(msg.contains("negative value"));
203    }
204
205    /// Test Error::DimensionMismatch Display implementation
206    ///
207    /// Covers lines 95-96: DimensionMismatch Display impl
208    #[test]
209    fn test_dimension_mismatch_display() {
210        let err = Error::DimensionMismatch("matrix 3x3 cannot multiply with 2x2".to_string());
211        let msg = err.to_string();
212        assert!(msg.contains("Dimension mismatch"));
213        assert!(msg.contains("matrix 3x3"));
214    }
215
216    /// Test Error::ComputationFailed Display implementation
217    ///
218    /// Covers lines 98-99: ComputationFailed Display impl
219    #[test]
220    fn test_computation_failed_display() {
221        let err = Error::ComputationFailed("QR decomposition failed".to_string());
222        let msg = err.to_string();
223        assert!(msg.contains("Computation failed"));
224        assert!(msg.contains("QR decomposition"));
225    }
226
227    /// Test Error::ParseError Display implementation
228    #[test]
229    fn test_parse_error_display() {
230        let err = Error::ParseError("invalid JSON syntax".to_string());
231        let msg = err.to_string();
232        assert!(msg.contains("Parse error"));
233        assert!(msg.contains("JSON"));
234    }
235
236    /// Test Error::DomainCheck Display implementation
237    #[test]
238    fn test_domain_check_display() {
239        let err = Error::DomainCheck("unauthorized domain".to_string());
240        let msg = err.to_string();
241        assert!(msg.contains("Domain check failed"));
242        assert!(msg.contains("unauthorized"));
243    }
244
245    /// Test error_json function
246    #[test]
247    fn test_error_json() {
248        let json = error_json("test error");
249        assert_eq!(json, r#"{"error":"test error"}"#);
250    }
251
252    /// Test error_to_json function with SingularMatrix
253    #[test]
254    fn test_error_to_json_singular_matrix() {
255        let err = Error::SingularMatrix;
256        let json = error_to_json(&err);
257        assert!(json.contains(r#""error":"#));
258        assert!(json.contains("singular"));
259    }
260
261    /// Test error_to_json function with DimensionMismatch
262    #[test]
263    fn test_error_to_json_dimension_mismatch() {
264        let err = Error::DimensionMismatch("incompatible dimensions".to_string());
265        let json = error_to_json(&err);
266        assert!(json.contains(r#""error":"#));
267        assert!(json.contains("Dimension"));
268    }
269
270    /// Test error_to_json function with ComputationFailed
271    #[test]
272    fn test_error_to_json_computation_failed() {
273        let err = Error::ComputationFailed("convergence failure".to_string());
274        let json = error_to_json(&err);
275        assert!(json.contains(r#""error":"#));
276        assert!(json.contains("Computation"));
277    }
278
279    /// Test Error PartialEq implementation
280    #[test]
281    fn test_error_partial_eq() {
282        let err1 = Error::SingularMatrix;
283        let err2 = Error::SingularMatrix;
284        let err3 = Error::InvalidInput("test".to_string());
285
286        assert_eq!(err1, err2);
287        assert_ne!(err1, err3);
288    }
289
290    /// Test Error Clone implementation
291    #[test]
292    fn test_error_clone() {
293        let err1 = Error::InvalidInput("test".to_string());
294        let err2 = err1.clone();
295        assert_eq!(err1, err2);
296    }
297
298    /// Test Error Debug implementation
299    #[test]
300    fn test_error_debug() {
301        let err = Error::ComputationFailed("test failure".to_string());
302        let debug_str = format!("{:?}", err);
303        assert!(debug_str.contains("ComputationFailed"));
304    }
305
306    /// Test Result type alias
307    #[test]
308    fn test_result_type_alias() {
309        fn returns_ok() -> Result<f64> {
310            Ok(42.0)
311        }
312        fn returns_err() -> Result<f64> {
313            Err(Error::InvalidInput("test".to_string()))
314        }
315
316        assert_eq!(returns_ok().unwrap(), 42.0);
317        assert!(returns_err().is_err());
318    }
319}