bosk 0.1.0

Pure-Rust LightGBM inference: parses the text model format directly — no FFI, zero deps. Optional ONNX Runtime and CatBoost backends behind the same small Model trait.
Documentation
//! Error type for model loading and inference.

use std::fmt;

/// Convenience alias for results returned by this crate.
pub type Result<T> = std::result::Result<T, Error>;

/// Everything that can go wrong loading or evaluating a model.
///
/// Marked `#[non_exhaustive]`: matching on it must include a wildcard arm, so
/// new variants can be added in future releases without a breaking change.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
    /// Reading the model file from disk failed.
    Io {
        /// The path that could not be read.
        path: std::path::PathBuf,
        /// The underlying I/O error.
        source: std::io::Error,
    },

    /// The model text could not be parsed. `line` is 1-based.
    Parse {
        /// 1-based line number the failure was found on.
        line: usize,
        /// Human-readable description of what could not be parsed.
        message: String,
    },

    /// The file parsed but contained no decision trees.
    EmptyModel,

    /// The model uses a capability this crate does not implement (e.g.
    /// multiclass output or linear trees). Refusing to load is deliberate:
    /// evaluating such a model with the supported subset would return silently
    /// wrong predictions.
    Unsupported {
        /// What the model needs that is not implemented.
        message: String,
    },

    /// The file extension is not loadable in this build. `supported` lists the
    /// extensions the current feature set can load.
    UnsupportedFormat {
        /// The extension that was requested (without the leading dot).
        ext: String,
        /// Extensions this build can load, in preferred order.
        supported: &'static [&'static str],
    },

    /// The feature vector passed to [`predict`](crate::Model::predict) has the
    /// wrong length for the model. Refusing to predict is deliberate: the
    /// model would silently treat the out-of-range features as missing and
    /// return a plausible but wrong prediction.
    FeatureCount {
        /// The number of features the model was trained on.
        expected: usize,
        /// The number of features that was passed in.
        got: usize,
    },

    /// The flat batch passed to [`predict_batch`](crate::Model::predict_batch)
    /// cannot be split into rows of `n_features`.
    BatchShape {
        /// Total length of the flat feature buffer.
        len: usize,
        /// The row width that was requested (`0` is always invalid).
        n_features: usize,
    },

    /// A native backend (ONNX Runtime or CatBoost) failed to load or predict.
    Backend {
        /// Which backend produced the error (`"onnx"` / `"catboost"`).
        backend: &'static str,
        /// The backend's own error message.
        message: String,
    },
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::Io { path, source } => {
                write!(f, "reading {}: {source}", path.display())
            }
            Error::Parse { line, message } => {
                write!(f, "parse error on line {line}: {message}")
            }
            Error::EmptyModel => write!(f, "no decision trees found in model file"),
            Error::Unsupported { message } => {
                write!(f, "unsupported model: {message}")
            }
            Error::FeatureCount { expected, got } => write!(
                f,
                "model expects {expected} features, got {got}"
            ),
            Error::BatchShape { len, n_features } => write!(
                f,
                "flat batch of length {len} cannot be split into rows of {n_features} features"
            ),
            Error::UnsupportedFormat { ext, supported } => write!(
                f,
                "unsupported model format '.{ext}' in this build (supported: {supported:?})"
            ),
            Error::Backend { backend, message } => write!(f, "{backend} backend: {message}"),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::Io { source, .. } => Some(source),
            _ => None,
        }
    }
}