use std::fs::File;
use std::io::BufReader;
#[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>>,
},
#[error(transparent)]
NeuralNetwork(#[from] NnError),
#[error(transparent)]
Tree(#[from] TreeError),
#[error(transparent)]
Io(#[from] IoError),
}
impl Error {
pub fn empty_input(what: impl Into<String>) -> Self {
Self::EmptyInput(what.into())
}
pub fn dimension_mismatch(expected: usize, found: usize) -> Self {
Self::DimensionMismatch { expected, found }
}
pub fn shape_mismatch(expected: impl Into<Vec<usize>>, found: impl Into<Vec<usize>>) -> Self {
Self::ShapeMismatch {
expected: expected.into(),
found: found.into(),
}
}
pub fn non_finite(context: impl Into<String>) -> Self {
Self::NonFinite(context.into())
}
pub fn invalid_parameter(name: impl Into<String>, reason: impl Into<String>) -> Self {
Self::InvalidParameter {
name: name.into(),
reason: reason.into(),
}
}
pub fn invalid_input(msg: impl Into<String>) -> Self {
Self::InvalidInput(msg.into())
}
pub fn not_fitted(model: &'static str) -> Self {
Self::NotFitted(model)
}
pub fn not_converged(msg: impl Into<String>) -> Self {
Self::NotConverged(msg.into())
}
pub fn computation(context: impl Into<String>) -> Self {
Self::Computation {
context: context.into(),
source: None,
}
}
pub fn forward_pass_not_run(layer: &'static str) -> Self {
Self::NeuralNetwork(NnError::ForwardPassNotRun(layer))
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Self::Io(IoError::Std(e))
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Self::Io(IoError::Json(e))
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum NnError {
#[error(
"forward pass has not been run on layer `{0}`; run `forward` before accessing outputs or `backward`"
)]
ForwardPassNotRun(&'static str),
#[error("weight shape mismatch for `{name}`: layer expects {expected:?}, got {found:?}")]
WeightShape {
name: String,
expected: Vec<usize>,
found: Vec<usize>,
},
#[error("model has not been compiled: `{0}` is not specified")]
NotCompiled(&'static str),
#[error("model has no layers")]
EmptyModel,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum TreeError {
#[error("operation requires a classification tree")]
NotClassificationTree,
#[error("corrupt tree structure: {0}")]
CorruptStructure(&'static str),
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum IoError {
#[error("I/O error: {0}")]
Std(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("model structure mismatch: {0}")]
ModelStructureMismatch(String),
}
impl IoError {
pub fn load_in_buf_reader(
path: impl AsRef<std::path::Path>,
) -> std::io::Result<BufReader<File>> {
Ok(BufReader::new(File::open(path)?))
}
}
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,
{
fn context(self, context: impl Into<String>) -> RustymlResult<T> {
self.map_err(|e| Error::Computation {
context: context.into(),
source: Some(Box::new(e)),
})
}
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 display_neural_network_transparent_forwards_inner() {
let inner = NnError::EmptyModel;
assert_eq!(inner.to_string(), "model has no layers");
let outer: Error = Error::from(NnError::EmptyModel);
assert_eq!(outer.to_string(), inner.to_string());
}
#[test]
fn display_neural_network_transparent_forwards_parameterized_inner() {
let outer: Error = Error::from(NnError::NotCompiled("optimizer"));
assert_eq!(
outer.to_string(),
"model has not been compiled: `optimizer` is not specified"
);
}
#[test]
fn display_tree_transparent_forwards_inner() {
let inner = TreeError::NotClassificationTree;
assert_eq!(
inner.to_string(),
"operation requires a classification tree"
);
let outer: Error = Error::from(TreeError::NotClassificationTree);
assert_eq!(outer.to_string(), inner.to_string());
}
}