#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("input is empty: {0}")]
EmptyInput(String),
#[error("dimension mismatch: expected {expected}, found {found}")]
DimensionMismatch {
expected: usize,
found: usize,
},
#[error("shape mismatch: expected {expected:?}, found {found:?}")]
ShapeMismatch {
expected: Vec<usize>,
found: Vec<usize>,
},
#[error("non-finite value (NaN or infinity) encountered in {0}")]
NonFinite(String),
#[error("invalid parameter `{name}`: {reason}")]
InvalidParameter {
name: String,
reason: String,
},
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("model `{0}` has not been fitted; call `fit` before this operation")]
NotFitted(&'static str),
#[error("failed to converge: {0}")]
NotConverged(String),
#[error("computation failed: {context}")]
Computation {
context: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
#[cfg(feature = "neural_network")]
#[error(transparent)]
NeuralNetwork(#[from] crate::neural_network::NnError),
#[cfg(feature = "machine_learning")]
#[error(transparent)]
Tree(#[from] crate::machine_learning::TreeError),
#[error(transparent)]
Io(#[from] IoError),
}
impl Error {
#[cold]
pub fn empty_input(what: impl Into<String>) -> Self {
Self::EmptyInput(what.into())
}
#[cold]
pub fn dimension_mismatch(expected: usize, found: usize) -> Self {
Self::DimensionMismatch { expected, found }
}
#[cold]
pub fn shape_mismatch(expected: impl Into<Vec<usize>>, found: impl Into<Vec<usize>>) -> Self {
Self::ShapeMismatch {
expected: expected.into(),
found: found.into(),
}
}
#[cold]
pub fn non_finite(context: impl Into<String>) -> Self {
Self::NonFinite(context.into())
}
#[cold]
pub fn invalid_parameter(name: impl Into<String>, reason: impl Into<String>) -> Self {
Self::InvalidParameter {
name: name.into(),
reason: reason.into(),
}
}
#[cold]
pub fn invalid_input(msg: impl Into<String>) -> Self {
Self::InvalidInput(msg.into())
}
#[cold]
pub fn not_fitted(model: &'static str) -> Self {
Self::NotFitted(model)
}
#[cold]
pub fn not_converged(msg: impl Into<String>) -> Self {
Self::NotConverged(msg.into())
}
#[cold]
pub fn computation(context: impl Into<String>) -> Self {
Self::Computation {
context: context.into(),
source: None,
}
}
}
impl From<std::io::Error> for Error {
#[cold]
fn from(e: std::io::Error) -> Self {
Self::Io(IoError::Std(e))
}
}
impl From<postcard::Error> for Error {
#[cold]
fn from(e: postcard::Error) -> Self {
Self::Io(IoError::Serialization(e))
}
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum IoError {
#[error("I/O error: {0}")]
Std(#[from] std::io::Error),
#[error("serialization error: {0}")]
Serialization(#[from] postcard::Error),
#[error("model structure mismatch: {0}")]
ModelStructureMismatch(String),
#[error("unsupported model format: {0}")]
UnsupportedModelFormat(String),
}
pub type RustymlResult<T> = std::result::Result<T, Error>;
pub trait Context<T> {
fn context(self, context: impl Into<String>) -> RustymlResult<T>;
fn with_context<F, S>(self, f: F) -> RustymlResult<T>
where
F: FnOnce() -> S,
S: Into<String>;
}
impl<T, E> Context<T> for std::result::Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
#[cold]
fn context(self, context: impl Into<String>) -> RustymlResult<T> {
self.map_err(|e| Error::Computation {
context: context.into(),
source: Some(Box::new(e)),
})
}
#[cold]
fn with_context<F, S>(self, f: F) -> RustymlResult<T>
where
F: FnOnce() -> S,
S: Into<String>,
{
self.map_err(|e| Error::Computation {
context: f().into(),
source: Some(Box::new(e)),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::error::Error as StdError;
#[test]
fn context_wraps_err_as_computation_preserving_source() {
let parsed: Result<i32, _> = "not a number".parse::<i32>();
match parsed.context("parsing the threshold") {
Err(Error::Computation { context, source }) => {
assert_eq!(context, "parsing the threshold");
let src = source.expect("the original error must be preserved as the source");
assert!(
src.downcast_ref::<std::num::ParseIntError>().is_some(),
"source must downcast back to the original ParseIntError"
);
}
other => panic!("expected Error::Computation, got {other:?}"),
}
}
#[test]
fn context_exposes_wrapped_error_via_std_source() {
let err = "x".parse::<i32>().context("ctx").unwrap_err();
let src = StdError::source(&err).expect("source() must return Some");
assert!(src.downcast_ref::<std::num::ParseIntError>().is_some());
}
#[test]
fn context_is_passthrough_on_ok() {
let ok: Result<i32, std::num::ParseIntError> = Ok(42);
assert_eq!(ok.context("unused").unwrap(), 42);
}
#[test]
fn with_context_closure_runs_only_on_err() {
let ran_on_ok = Cell::new(false);
let ok: Result<i32, std::num::ParseIntError> = Ok(7);
let passed = ok.with_context(|| {
ran_on_ok.set(true);
"should never be built"
});
assert_eq!(passed.unwrap(), 7);
assert!(
!ran_on_ok.get(),
"with_context closure must not run on the Ok path"
);
let ran_on_err = Cell::new(false);
let wrapped = "nope".parse::<i32>().with_context(|| {
ran_on_err.set(true);
format!("lazy context {}", 1)
});
assert!(
ran_on_err.get(),
"with_context closure must run on the Err path"
);
match wrapped {
Err(Error::Computation { context, source }) => {
assert_eq!(context, "lazy context 1");
assert!(source.is_some(), "source must be preserved");
}
other => panic!("expected Error::Computation, got {other:?}"),
}
}
#[test]
fn display_empty_input() {
let e = Error::empty_input("target vector");
assert_eq!(e.to_string(), "input is empty: target vector");
}
#[test]
fn display_dimension_mismatch() {
let e = Error::dimension_mismatch(3, 5);
assert_eq!(e.to_string(), "dimension mismatch: expected 3, found 5");
}
#[test]
fn display_shape_mismatch() {
let e = Error::shape_mismatch(vec![2usize, 3], vec![2usize, 4]);
assert_eq!(
e.to_string(),
"shape mismatch: expected [2, 3], found [2, 4]"
);
}
#[test]
fn display_invalid_parameter() {
let e = Error::invalid_parameter("C", "must be > 0");
assert_eq!(e.to_string(), "invalid parameter `C`: must be > 0");
}
#[test]
fn display_not_fitted() {
let e = Error::not_fitted("KMeans");
assert_eq!(
e.to_string(),
"model `KMeans` has not been fitted; call `fit` before this operation"
);
}
#[test]
fn non_finite_constructor_carries_context() {
match Error::non_finite("weights") {
Error::NonFinite(ref ctx) => assert!(
ctx.contains("weights"),
"context should mention 'weights', got: {ctx}"
),
other => panic!("expected NonFinite, got {other:?}"),
}
}
#[test]
fn invalid_input_constructor_carries_message() {
match Error::invalid_input("unexpected rank") {
Error::InvalidInput(ref msg) => assert!(
msg.contains("unexpected rank"),
"message should mention the supplied text, got: {msg}"
),
other => panic!("expected InvalidInput, got {other:?}"),
}
}
#[test]
fn computation_constructor_has_no_source() {
match Error::computation("overflow") {
Error::Computation {
ref context,
ref source,
} => {
assert!(
context.contains("overflow"),
"context should contain 'overflow'"
);
assert!(
source.is_none(),
"source should be None for Error::computation"
);
}
other => panic!("expected Computation, got {other:?}"),
}
}
#[test]
fn rustyml_result_is_result_alias() {
let ok: RustymlResult<i32> = Ok(42);
assert!(matches!(ok, Ok(42)));
let err: RustymlResult<i32> = Err(Error::empty_input("test"));
assert!(matches!(err, Err(Error::EmptyInput(_))));
}
}