Skip to main content

irithyll_core/
error.rs

1//! Error types for irithyll-core.
2//!
3//! No `thiserror`, no mandatory `alloc` — just `core::fmt::Display` impls.
4//!
5//! - [`FormatError`] — packed binary validation errors (always available, `Copy`).
6//! - [`ConfigError`] — configuration validation errors (requires `alloc` for `String` fields).
7
8#[cfg(feature = "alloc")]
9use alloc::string::String;
10
11// ---------------------------------------------------------------------------
12// FormatError — packed binary validation (no_std, no alloc)
13// ---------------------------------------------------------------------------
14
15/// Errors that can occur when parsing or validating a packed ensemble binary.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum FormatError {
18    /// Magic bytes do not match `"IRIT"` (`0x54495249` LE).
19    BadMagic,
20    /// Format version is not supported by this build.
21    UnsupportedVersion,
22    /// Input buffer is too short to contain the declared structures.
23    Truncated,
24    /// Input buffer pointer is not aligned to 4 bytes.
25    Unaligned,
26    /// A node's child index points outside the node array bounds.
27    InvalidNodeIndex,
28    /// A node references a feature index >= `n_features`.
29    InvalidFeatureIndex,
30    /// A tree entry's byte offset is not aligned to `size_of::<PackedNode>()`.
31    MisalignedTreeOffset,
32}
33
34impl core::fmt::Display for FormatError {
35    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
36        match self {
37            FormatError::BadMagic => write!(f, "bad magic: expected \"IRIT\""),
38            FormatError::UnsupportedVersion => write!(f, "unsupported format version"),
39            FormatError::Truncated => write!(f, "buffer truncated"),
40            FormatError::Unaligned => write!(f, "buffer not 4-byte aligned"),
41            FormatError::InvalidNodeIndex => write!(f, "node child index out of bounds"),
42            FormatError::InvalidFeatureIndex => write!(f, "feature index exceeds n_features"),
43            FormatError::MisalignedTreeOffset => {
44                write!(f, "tree offset not aligned to node size")
45            }
46        }
47    }
48}
49
50// ---------------------------------------------------------------------------
51// ConfigError — configuration validation (requires alloc for String fields)
52// ---------------------------------------------------------------------------
53
54/// Structured error for configuration validation failures.
55///
56/// Instead of opaque strings, each variant carries the parameter name and
57/// constraint so callers can programmatically inspect what went wrong.
58///
59/// Requires the `alloc` feature for `String` fields.
60#[cfg(feature = "alloc")]
61#[derive(Debug, Clone)]
62pub enum ConfigError {
63    /// A parameter value is outside its valid range.
64    ///
65    /// # Examples
66    ///
67    /// ```text
68    /// n_steps must be > 0 (got 0)
69    /// learning_rate must be in (0, 1] (got 1.5)
70    /// ```
71    OutOfRange {
72        /// The parameter name (e.g. `"n_steps"`, `"drift_detector.Adwin.delta"`).
73        param: &'static str,
74        /// The constraint that was violated (e.g. `"must be > 0"`).
75        constraint: &'static str,
76        /// The actual value that was provided.
77        value: String,
78    },
79
80    /// A parameter is invalid for a structural reason, typically involving
81    /// a relationship between two parameters.
82    ///
83    /// # Examples
84    ///
85    /// ```text
86    /// split_reeval_interval must be >= grace_period (200), got 50
87    /// drift_detector.Ddm.drift_level must be > warning_level (3.0), got 2.0
88    /// ```
89    Invalid {
90        /// The parameter name.
91        param: &'static str,
92        /// Why the value is invalid.
93        reason: String,
94    },
95}
96
97#[cfg(feature = "alloc")]
98impl ConfigError {
99    /// Convenience for the common "value out of range" case.
100    pub fn out_of_range(
101        param: &'static str,
102        constraint: &'static str,
103        value: impl core::fmt::Display,
104    ) -> Self {
105        use alloc::format;
106        ConfigError::OutOfRange {
107            param,
108            constraint,
109            value: format!("{}", value),
110        }
111    }
112
113    /// Convenience for the "invalid relationship" case.
114    pub fn invalid(param: &'static str, reason: impl Into<String>) -> Self {
115        ConfigError::Invalid {
116            param,
117            reason: reason.into(),
118        }
119    }
120}
121
122#[cfg(feature = "alloc")]
123impl core::fmt::Display for ConfigError {
124    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
125        match self {
126            ConfigError::OutOfRange {
127                param,
128                constraint,
129                value,
130            } => write!(f, "{} {} (got {})", param, constraint, value),
131            ConfigError::Invalid { param, reason } => write!(f, "{} {}", param, reason),
132        }
133    }
134}
135
136// std::error::Error impls (requires std feature)
137#[cfg(feature = "std")]
138impl std::error::Error for ConfigError {}
139
140// ---------------------------------------------------------------------------
141// IrithyllError — top-level error enum (requires alloc)
142// ---------------------------------------------------------------------------
143
144/// Top-level error type for the irithyll crate.
145///
146/// Requires the `alloc` feature for `String` fields.
147#[cfg(feature = "alloc")]
148#[derive(Debug)]
149pub enum IrithyllError {
150    /// Configuration validation failed.
151    InvalidConfig(ConfigError),
152    /// Not enough data to perform the requested operation.
153    InsufficientData(String),
154    /// Feature dimension mismatch between sample and model.
155    DimensionMismatch {
156        /// Expected number of features.
157        expected: usize,
158        /// Actual number of features received.
159        got: usize,
160    },
161    /// Model has not been trained yet.
162    NotTrained,
163}
164
165#[cfg(feature = "alloc")]
166impl core::fmt::Display for IrithyllError {
167    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
168        match self {
169            IrithyllError::InvalidConfig(e) => write!(f, "invalid configuration: {}", e),
170            IrithyllError::InsufficientData(msg) => write!(f, "insufficient data: {}", msg),
171            IrithyllError::DimensionMismatch { expected, got } => {
172                write!(f, "dimension mismatch: expected {}, got {}", expected, got)
173            }
174            IrithyllError::NotTrained => write!(f, "model not trained"),
175        }
176    }
177}
178
179#[cfg(feature = "alloc")]
180impl From<ConfigError> for IrithyllError {
181    fn from(e: ConfigError) -> Self {
182        IrithyllError::InvalidConfig(e)
183    }
184}
185
186/// Result type using [`IrithyllError`].
187#[cfg(feature = "alloc")]
188pub type Result<T> = core::result::Result<T, IrithyllError>;
189
190// ---------------------------------------------------------------------------
191// Tests
192// ---------------------------------------------------------------------------
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use alloc::string::ToString;
198
199    #[test]
200    fn format_error_display() {
201        assert_eq!(
202            FormatError::BadMagic.to_string(),
203            "bad magic: expected \"IRIT\""
204        );
205        assert_eq!(FormatError::Truncated.to_string(), "buffer truncated");
206    }
207
208    #[cfg(feature = "alloc")]
209    #[test]
210    fn config_error_out_of_range_display() {
211        let e = ConfigError::out_of_range("n_steps", "must be > 0", 0);
212        assert_eq!(e.to_string(), "n_steps must be > 0 (got 0)");
213    }
214
215    #[cfg(feature = "alloc")]
216    #[test]
217    fn config_error_invalid_display() {
218        let e = ConfigError::invalid(
219            "split_reeval_interval",
220            "must be >= grace_period (200), got 50",
221        );
222        assert!(e.to_string().contains("split_reeval_interval"));
223        assert!(e.to_string().contains("must be >= grace_period"));
224    }
225
226    #[cfg(feature = "alloc")]
227    #[test]
228    fn irithyll_error_from_config_error() {
229        let ce = ConfigError::out_of_range("learning_rate", "must be in (0, 1]", 1.5);
230        let ie: IrithyllError = ce.into();
231        let msg = ie.to_string();
232        assert!(msg.contains("invalid configuration"));
233        assert!(msg.contains("learning_rate"));
234    }
235
236    #[cfg(feature = "alloc")]
237    #[test]
238    fn irithyll_error_dimension_mismatch() {
239        let e = IrithyllError::DimensionMismatch {
240            expected: 10,
241            got: 5,
242        };
243        assert_eq!(e.to_string(), "dimension mismatch: expected 10, got 5");
244    }
245
246    #[cfg(feature = "alloc")]
247    #[test]
248    fn irithyll_error_not_trained() {
249        assert_eq!(IrithyllError::NotTrained.to_string(), "model not trained");
250    }
251}