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}