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}