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