use serde::{Deserialize, Serialize};
#[derive(
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub enum CompensationHint {
UniqueViolation {
field: String,
conflicting_value: String,
},
ForeignKeyMissing {
referenced_id: String,
},
PermissionDenied,
RateLimited {
retry_after_ms: u64,
},
SchemaViolation {
field: String,
reason: String,
},
Custom {
constraint: String,
detail: String,
},
IntegrityViolation,
}
impl CompensationHint {
pub fn code(&self) -> &'static str {
match self {
Self::UniqueViolation { .. } => "UNIQUE_VIOLATION",
Self::ForeignKeyMissing { .. } => "FK_MISSING",
Self::PermissionDenied => "PERMISSION_DENIED",
Self::RateLimited { .. } => "RATE_LIMITED",
Self::SchemaViolation { .. } => "SCHEMA_VIOLATION",
Self::Custom { .. } => "CUSTOM",
Self::IntegrityViolation => "INTEGRITY_VIOLATION",
}
}
}
impl std::fmt::Display for CompensationHint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UniqueViolation {
field,
conflicting_value,
} => write!(
f,
"UNIQUE({field}): value '{conflicting_value}' already exists"
),
Self::ForeignKeyMissing { referenced_id } => {
write!(f, "FK_MISSING: referenced ID '{referenced_id}' not found")
}
Self::PermissionDenied => write!(f, "PERMISSION_DENIED"),
Self::RateLimited { retry_after_ms } => {
write!(f, "RATE_LIMITED: retry after {retry_after_ms}ms")
}
Self::SchemaViolation { field, reason } => {
write!(f, "SCHEMA({field}): {reason}")
}
Self::Custom {
constraint, detail, ..
} => write!(f, "CUSTOM({constraint}): {detail}"),
Self::IntegrityViolation => write!(f, "INTEGRITY_VIOLATION: CRC32C mismatch"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compensation_codes() {
assert_eq!(
CompensationHint::UniqueViolation {
field: "email".into(),
conflicting_value: "a@b.com".into()
}
.code(),
"UNIQUE_VIOLATION"
);
assert_eq!(
CompensationHint::PermissionDenied.code(),
"PERMISSION_DENIED"
);
assert_eq!(
CompensationHint::RateLimited {
retry_after_ms: 5000
}
.code(),
"RATE_LIMITED"
);
}
#[test]
fn compensation_display() {
let hint = CompensationHint::UniqueViolation {
field: "username".into(),
conflicting_value: "alice".into(),
};
assert!(hint.to_string().contains("alice"));
assert!(hint.to_string().contains("username"));
}
#[test]
fn msgpack_roundtrip() {
let hint = CompensationHint::ForeignKeyMissing {
referenced_id: "user-42".into(),
};
let bytes = rmp_serde::to_vec_named(&hint).unwrap();
let decoded: CompensationHint = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(hint, decoded);
}
}