1use std::fmt;
2
3#[derive(Clone, Copy, PartialEq, Eq, Hash)]
5pub struct Hash([u8; 32]);
6
7impl Hash {
8 pub fn from_bytes(bytes: &[u8; 32]) -> Self {
10 Hash(*bytes)
11 }
12
13 pub fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
15 if bytes.len() < 32 {
16 return Err(AreevError::Format(format!(
17 "hash requires 32 bytes, got {}",
18 bytes.len()
19 )));
20 }
21 let mut arr = [0u8; 32];
22 arr.copy_from_slice(&bytes[..32]);
23 Ok(Hash(arr))
24 }
25
26 pub fn from_hex(hex_str: &str) -> Result<Self> {
27 let bytes = hex::decode(hex_str)
28 .map_err(|e| AreevError::Format(format!("invalid hex hash: {}", e)))?;
29 if bytes.len() != 32 {
30 return Err(AreevError::Format(format!(
31 "hash must be 32 bytes, got {}",
32 bytes.len()
33 )));
34 }
35 Ok(Self::from_bytes(&bytes.try_into().unwrap()))
36 }
37
38 pub fn as_bytes(&self) -> &[u8; 32] {
39 &self.0
40 }
41
42 pub fn to_hex(&self) -> String {
43 hex::encode(self.0)
44 }
45}
46
47impl fmt::Debug for Hash {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 write!(f, "Hash({})", &self.to_hex()[..16])
50 }
51}
52
53impl fmt::Display for Hash {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 write!(f, "{}", self.to_hex())
56 }
57}
58
59impl serde::Serialize for Hash {
60 fn serialize<S: serde::Serializer>(
61 &self,
62 serializer: S,
63 ) -> std::result::Result<S::Ok, S::Error> {
64 serializer.serialize_str(&self.to_hex())
65 }
66}
67
68impl<'de> serde::Deserialize<'de> for Hash {
69 fn deserialize<D: serde::Deserializer<'de>>(
70 deserializer: D,
71 ) -> std::result::Result<Self, D::Error> {
72 let s = String::deserialize(deserializer)?;
73 Hash::from_hex(&s).map_err(serde::de::Error::custom)
74 }
75}
76
77#[derive(Debug)]
79pub enum AreevError {
80 NotFound(Hash),
81 Format(String),
82 Validation(String),
83 Serialization(String),
84 ToolRenderUnsupported(String),
85 Storage(String),
86 StoreBusy(String),
89 TlsUnavailable(String),
95 ReadOnly(String),
103 ReadOnlyOpenFailed(String),
113 SupersessionConflict(Hash),
114 SupersessionChainTooDeep(Hash),
120 AnnIndexUnsupported(String),
126 CryptoError(String),
127 AccumulateRetryExhausted,
128 AccumulateInternal(String),
129 AccumulateBackpressureRejected,
130 Internal(String),
131 AuthzDenied(String),
133 AuthzUnknownPrincipal(String),
135 AuthzConfigInvalid(String),
137 AuthzTokenUnrecognized,
140}
141
142impl AreevError {
143 pub fn code(&self) -> &'static str {
149 match self {
150 Self::NotFound(_) => "MEM-E001",
151 Self::SupersessionConflict(_) => "MEM-E002",
152 Self::SupersessionChainTooDeep(_) => "STO-E006",
153 Self::AnnIndexUnsupported(_) => "STO-E007",
154 Self::ToolRenderUnsupported(_) => "MEM-E110",
155 Self::Format(_) => "FMT-E001",
156 Self::Serialization(_) => "FMT-E002",
157 Self::Validation(_) => "VAL-E001",
158 Self::Storage(_) => "STO-E001",
159 Self::StoreBusy(_) => "STO-E002",
160 Self::TlsUnavailable(_) => "STO-E003",
161 Self::ReadOnly(_) => "STO-E004",
162 Self::ReadOnlyOpenFailed(_) => "STO-E005",
163 Self::CryptoError(_) => "CRY-E001",
164 Self::AccumulateRetryExhausted => "CAL-E083",
167 Self::AccumulateInternal(_) => "CAL-E084",
168 Self::AccumulateBackpressureRejected => "CAL-E085",
169 Self::Internal(_) => "SYS-E001",
170 Self::AuthzDenied(_) => "AUT-E001",
171 Self::AuthzUnknownPrincipal(_) => "AUT-E002",
172 Self::AuthzConfigInvalid(_) => "AUT-E003",
173 Self::AuthzTokenUnrecognized => "AUT-E004",
174 }
175 }
176}
177
178impl std::fmt::Display for AreevError {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 match self {
183 Self::NotFound(h) => write!(f, "MEM-E001: grain not found: {h}"),
184 Self::SupersessionConflict(h) => write!(f, "MEM-E002: already superseded: {h}"),
185 Self::SupersessionChainTooDeep(h) => write!(
186 f,
187 "STO-E006: supersession chain from {h} did not terminate within the bounded walk — the supersedes links may be cyclic or corrupt"
188 ),
189 Self::AnnIndexUnsupported(m) => write!(
190 f,
191 "STO-E007: no approximate vector index on this backend: {m}"
192 ),
193 Self::ToolRenderUnsupported(m) => write!(f, "MEM-E110: tool render unsupported: {m}"),
194 Self::Format(m) => write!(f, "FMT-E001: format error: {m}"),
195 Self::Serialization(m) => write!(f, "FMT-E002: serialization error: {m}"),
196 Self::Validation(m) => write!(f, "VAL-E001: validation error: {m}"),
197 Self::Storage(m) => write!(f, "STO-E001: storage error: {m}"),
198 Self::StoreBusy(m) => write!(f, "STO-E002: store busy: {m}"),
199 Self::TlsUnavailable(m) => write!(f, "STO-E003: {m}"),
200 Self::ReadOnly(m) => write!(f, "STO-E004: refusing write on a read-only memory: {m}"),
201 Self::ReadOnlyOpenFailed(m) => write!(f, "STO-E005: {m}"),
202 Self::CryptoError(m) => write!(f, "CRY-E001: crypto error: {m}"),
203 Self::AccumulateRetryExhausted => write!(f, "CAL-E083: ACCUMULATE retry budget exhausted"),
204 Self::AccumulateInternal(m) => write!(f, "CAL-E084: ACCUMULATE internal failure: {m}"),
205 Self::AccumulateBackpressureRejected => write!(f, "CAL-E085: ACCUMULATE backpressure: inflight cap exceeded"),
206 Self::Internal(m) => write!(f, "SYS-E001: internal error: {m}"),
207 Self::AuthzDenied(m) => write!(f, "AUT-E001: authorization denied: {m}"),
208 Self::AuthzUnknownPrincipal(p) => write!(f, "AUT-E002: unknown principal: {p}"),
209 Self::AuthzConfigInvalid(m) => write!(f, "AUT-E003: {m}"),
210 Self::AuthzTokenUnrecognized => write!(f, "AUT-E004: token not recognized"),
211 }
212 }
213}
214
215impl std::error::Error for AreevError {}
216
217pub type Result<T> = std::result::Result<T, AreevError>;
218
219#[cfg(test)]
220mod error_code_tests {
221 use super::*;
222
223 fn all_variants() -> Vec<AreevError> {
225 let h = Hash::from_bytes(&[0u8; 32]);
226 vec![
227 AreevError::NotFound(h),
228 AreevError::SupersessionConflict(h),
229 AreevError::SupersessionChainTooDeep(h),
230 AreevError::AnnIndexUnsupported("x".into()),
231 AreevError::ToolRenderUnsupported("x".into()),
232 AreevError::Format("x".into()),
233 AreevError::Serialization("x".into()),
234 AreevError::Validation("x".into()),
235 AreevError::Storage("x".into()),
236 AreevError::StoreBusy("x".into()),
237 AreevError::TlsUnavailable("x".into()),
238 AreevError::ReadOnly("x".into()),
239 AreevError::ReadOnlyOpenFailed("x".into()),
240 AreevError::CryptoError("x".into()),
241 AreevError::AccumulateRetryExhausted,
242 AreevError::AccumulateInternal("x".into()),
243 AreevError::AccumulateBackpressureRejected,
244 AreevError::Internal("x".into()),
245 AreevError::AuthzDenied("x".into()),
246 AreevError::AuthzUnknownPrincipal("x".into()),
247 AreevError::AuthzConfigInvalid("x".into()),
248 AreevError::AuthzTokenUnrecognized,
249 ]
250 }
251
252 #[test]
255 fn code_prefixes_every_display() {
256 for e in all_variants() {
257 let msg = e.to_string();
258 let code = e.code();
259 assert!(
260 msg.starts_with(&format!("{code}: ")),
261 "`{msg}` must start with its code `{code}`"
262 );
263 }
264 }
265
266 #[test]
269 fn codes_follow_the_repo_standard() {
270 for e in all_variants() {
271 let c = e.code();
272 let (domain, num) = c.split_once("-E").unwrap_or_else(|| panic!("bad code: {c}"));
273 assert_eq!(domain.len(), 3, "{c}: domain must be 3 letters");
274 assert!(domain.chars().all(|ch| ch.is_ascii_uppercase()), "{c}: domain uppercase");
275 assert!(!num.is_empty() && num.chars().all(|ch| ch.is_ascii_digit()), "{c}: numeric suffix");
276 }
277 }
278}