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