Skip to main content

anomstream_core/
error.rs

1//! Error types used across the crate.
2//!
3//! [`RcfError`] is the canonical error returned by every fallible
4//! operation in `anomstream-core`. Each variant carries enough context for the
5//! caller to act without re-fetching state. [`RcfResult`] is the
6//! convenient `Result` alias used in public signatures.
7
8use alloc::boxed::Box;
9use alloc::string::String;
10
11use thiserror::Error;
12
13/// Errors produced by `anomstream-core`.
14///
15/// Variants are stable across `0.x` patch releases — adding a new
16/// variant is a minor-version change.
17///
18/// # Examples
19///
20/// ```
21/// use anomstream_core::{ForestBuilder, RcfError};
22///
23/// let err = ForestBuilder::<4>::new().num_trees(10).build().unwrap_err();
24/// assert!(matches!(err, RcfError::InvalidConfig(_)));
25/// ```
26#[derive(Debug, Error)]
27#[non_exhaustive]
28pub enum RcfError {
29    /// A point with the wrong dimensionality was supplied.
30    #[error("dimension mismatch: expected {expected}, got {got}")]
31    DimensionMismatch {
32        /// Dimensionality the forest was configured with.
33        expected: usize,
34        /// Dimensionality of the offending input.
35        got: usize,
36    },
37
38    /// A configuration value falls outside the AWS `SageMaker` spec
39    /// bounds enforced by `ForestBuilder`. The message payload is
40    /// `Box<str>` rather than `String` so the variant fits in
41    /// 16 bytes on 64-bit targets (vs 24 for `String`) — matters
42    /// when `RcfError` propagates through hot-path return values.
43    #[error("invalid configuration: {0}")]
44    InvalidConfig(Box<str>),
45
46    /// An operation that requires a non-empty forest was attempted on
47    /// an empty one (e.g. scoring before any `update` call).
48    #[error("forest is empty")]
49    EmptyForest,
50
51    /// A bounding box operation was requested on an empty box.
52    #[error("bounding box is empty")]
53    EmptyBoundingBox,
54
55    /// A floating-point input contained `NaN`, which would break the
56    /// total ordering required by every algorithm in the crate.
57    #[error("input contains NaN")]
58    NaNValue,
59
60    /// An indexed access fell outside the live range.
61    #[error("index {index} out of bounds (len={len})")]
62    OutOfBounds {
63        /// Index that was attempted.
64        index: usize,
65        /// Current length of the underlying collection.
66        len: usize,
67    },
68
69    /// Persistence: serialising the forest failed.
70    ///
71    /// Left as `String` (not `Box<str>`) because every emission
72    /// site formats a fresh heap-allocated message from an
73    /// upstream `serde` / `postcard` error — the extra 8 bytes
74    /// per variant vs `Box<str>` would cost a round-trip through
75    /// `Box::from(String)` on an already-cold path.
76    #[error("serialization failed: {0}")]
77    SerializationFailed(String),
78
79    /// Persistence: deserialising the forest failed (truncated bytes,
80    /// malformed JSON, version-skew payload, etc). See
81    /// [`Self::SerializationFailed`] for the `String`-vs-`Box<str>`
82    /// rationale.
83    #[error("deserialization failed: {0}")]
84    DeserializationFailed(String),
85
86    /// Persistence: the encoded version prefix does not match the
87    /// running library's expected version.
88    #[error("incompatible persistence version: found {found}, expected {expected}")]
89    IncompatibleVersion {
90        /// Version embedded in the loaded payload.
91        found: u32,
92        /// Version the running library understands.
93        expected: u32,
94    },
95}
96
97/// Convenience alias for `Result<T, RcfError>`.
98///
99/// # Examples
100///
101/// ```
102/// use anomstream_core::{RcfError, RcfResult};
103///
104/// fn check(n: u64) -> RcfResult<u64> {
105///     if n == 0 { Err(RcfError::EmptyForest) } else { Ok(n) }
106/// }
107/// assert_eq!(check(7).unwrap(), 7);
108/// assert!(check(0).is_err());
109/// ```
110pub type RcfResult<T> = Result<T, RcfError>;
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn dimension_mismatch_renders_both_values() {
118        let err = RcfError::DimensionMismatch {
119            expected: 4,
120            got: 7,
121        };
122        let msg = err.to_string();
123        assert!(msg.contains('4'));
124        assert!(msg.contains('7'));
125    }
126
127    #[test]
128    fn invalid_config_carries_message() {
129        let err = RcfError::InvalidConfig("num_trees=42 below minimum 50".into());
130        assert!(err.to_string().contains("num_trees"));
131    }
132
133    #[test]
134    fn out_of_bounds_renders_index_and_len() {
135        let err = RcfError::OutOfBounds { index: 12, len: 10 };
136        let msg = err.to_string();
137        assert!(msg.contains("12"));
138        assert!(msg.contains("10"));
139    }
140
141    #[test]
142    fn empty_variants_render_static_message() {
143        assert_eq!(RcfError::EmptyForest.to_string(), "forest is empty");
144        assert_eq!(
145            RcfError::EmptyBoundingBox.to_string(),
146            "bounding box is empty"
147        );
148        assert_eq!(RcfError::NaNValue.to_string(), "input contains NaN");
149    }
150
151    #[test]
152    fn rcf_result_alias_aliases_correctly() {
153        let ok: RcfResult<u32> = Ok(7);
154        let err: RcfResult<u32> = Err(RcfError::EmptyForest);
155        assert!(matches!(ok, Ok(7)));
156        assert!(err.is_err());
157    }
158}