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}