use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Hash([u8; 32]);
impl Hash {
pub fn from_bytes(bytes: &[u8; 32]) -> Self {
Hash(*bytes)
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 32 {
return Err(DejaDbError::Format(format!(
"hash requires 32 bytes, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes[..32]);
Ok(Hash(arr))
}
pub fn from_hex(hex_str: &str) -> Result<Self> {
let bytes = hex::decode(hex_str)
.map_err(|e| DejaDbError::Format(format!("invalid hex hash: {}", e)))?;
if bytes.len() != 32 {
return Err(DejaDbError::Format(format!(
"hash must be 32 bytes, got {}",
bytes.len()
)));
}
Ok(Self::from_bytes(&bytes.try_into().unwrap()))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
}
impl fmt::Debug for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Hash({})", &self.to_hex()[..16])
}
}
impl fmt::Display for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}
impl serde::Serialize for Hash {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_hex())
}
}
impl<'de> serde::Deserialize<'de> for Hash {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Hash::from_hex(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug)]
pub enum DejaDbError {
NotFound(Hash),
Format(String),
Validation(String),
Serialization(String),
ToolRenderUnsupported(String),
Storage(String),
SupersessionConflict(Hash),
CryptoError(String),
AccumulateRetryExhausted,
AccumulateInternal(String),
AccumulateBackpressureRejected,
Internal(String),
}
impl DejaDbError {
pub fn code(&self) -> &'static str {
match self {
Self::NotFound(_) => "MEM-E001",
Self::SupersessionConflict(_) => "MEM-E002",
Self::ToolRenderUnsupported(_) => "MEM-E110",
Self::Format(_) => "FMT-E001",
Self::Serialization(_) => "FMT-E002",
Self::Validation(_) => "VAL-E001",
Self::Storage(_) => "STO-E001",
Self::CryptoError(_) => "CRY-E001",
Self::AccumulateRetryExhausted => "CAL-E083",
Self::AccumulateInternal(_) => "CAL-E084",
Self::AccumulateBackpressureRejected => "CAL-E085",
Self::Internal(_) => "SYS-E001",
}
}
}
impl std::fmt::Display for DejaDbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(h) => write!(f, "MEM-E001: grain not found: {h}"),
Self::SupersessionConflict(h) => write!(f, "MEM-E002: already superseded: {h}"),
Self::ToolRenderUnsupported(m) => write!(f, "MEM-E110: tool render unsupported: {m}"),
Self::Format(m) => write!(f, "FMT-E001: format error: {m}"),
Self::Serialization(m) => write!(f, "FMT-E002: serialization error: {m}"),
Self::Validation(m) => write!(f, "VAL-E001: validation error: {m}"),
Self::Storage(m) => write!(f, "STO-E001: storage error: {m}"),
Self::CryptoError(m) => write!(f, "CRY-E001: crypto error: {m}"),
Self::AccumulateRetryExhausted => write!(f, "CAL-E083: ACCUMULATE retry budget exhausted"),
Self::AccumulateInternal(m) => write!(f, "CAL-E084: ACCUMULATE internal failure: {m}"),
Self::AccumulateBackpressureRejected => write!(f, "CAL-E085: ACCUMULATE backpressure: inflight cap exceeded"),
Self::Internal(m) => write!(f, "SYS-E001: internal error: {m}"),
}
}
}
impl std::error::Error for DejaDbError {}
pub type Result<T> = std::result::Result<T, DejaDbError>;
#[cfg(test)]
mod error_code_tests {
use super::*;
fn all_variants() -> Vec<DejaDbError> {
let h = Hash::from_bytes(&[0u8; 32]);
vec![
DejaDbError::NotFound(h),
DejaDbError::SupersessionConflict(h),
DejaDbError::ToolRenderUnsupported("x".into()),
DejaDbError::Format("x".into()),
DejaDbError::Serialization("x".into()),
DejaDbError::Validation("x".into()),
DejaDbError::Storage("x".into()),
DejaDbError::CryptoError("x".into()),
DejaDbError::AccumulateRetryExhausted,
DejaDbError::AccumulateInternal("x".into()),
DejaDbError::AccumulateBackpressureRejected,
DejaDbError::Internal("x".into()),
]
}
#[test]
fn code_prefixes_every_display() {
for e in all_variants() {
let msg = e.to_string();
let code = e.code();
assert!(
msg.starts_with(&format!("{code}: ")),
"`{msg}` must start with its code `{code}`"
);
}
}
#[test]
fn codes_follow_the_repo_standard() {
for e in all_variants() {
let c = e.code();
let (domain, num) = c.split_once("-E").unwrap_or_else(|| panic!("bad code: {c}"));
assert_eq!(domain.len(), 3, "{c}: domain must be 3 letters");
assert!(domain.chars().all(|ch| ch.is_ascii_uppercase()), "{c}: domain uppercase");
assert!(!num.is_empty() && num.chars().all(|ch| ch.is_ascii_digit()), "{c}: numeric suffix");
}
}
}