nodedb_types/sync/
compensation.rs1use serde::{Deserialize, Serialize};
9
10#[derive(
15 Debug,
16 Clone,
17 PartialEq,
18 Eq,
19 Serialize,
20 Deserialize,
21 rkyv::Archive,
22 rkyv::Serialize,
23 rkyv::Deserialize,
24 zerompk::ToMessagePack,
25 zerompk::FromMessagePack,
26)]
27pub enum CompensationHint {
28 UniqueViolation {
30 field: String,
32 conflicting_value: String,
34 },
35
36 ForeignKeyMissing {
38 referenced_id: String,
40 },
41
42 PermissionDenied,
45
46 RateLimited {
48 retry_after_ms: u64,
50 },
51
52 SchemaViolation {
54 field: String,
56 reason: String,
58 },
59
60 Custom {
62 constraint: String,
64 detail: String,
66 },
67
68 IntegrityViolation,
71}
72
73impl CompensationHint {
74 pub fn code(&self) -> &'static str {
76 match self {
77 Self::UniqueViolation { .. } => "UNIQUE_VIOLATION",
78 Self::ForeignKeyMissing { .. } => "FK_MISSING",
79 Self::PermissionDenied => "PERMISSION_DENIED",
80 Self::RateLimited { .. } => "RATE_LIMITED",
81 Self::SchemaViolation { .. } => "SCHEMA_VIOLATION",
82 Self::Custom { .. } => "CUSTOM",
83 Self::IntegrityViolation => "INTEGRITY_VIOLATION",
84 }
85 }
86}
87
88impl std::fmt::Display for CompensationHint {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 Self::UniqueViolation {
92 field,
93 conflicting_value,
94 } => write!(
95 f,
96 "UNIQUE({field}): value '{conflicting_value}' already exists"
97 ),
98 Self::ForeignKeyMissing { referenced_id } => {
99 write!(f, "FK_MISSING: referenced ID '{referenced_id}' not found")
100 }
101 Self::PermissionDenied => write!(f, "PERMISSION_DENIED"),
102 Self::RateLimited { retry_after_ms } => {
103 write!(f, "RATE_LIMITED: retry after {retry_after_ms}ms")
104 }
105 Self::SchemaViolation { field, reason } => {
106 write!(f, "SCHEMA({field}): {reason}")
107 }
108 Self::Custom {
109 constraint, detail, ..
110 } => write!(f, "CUSTOM({constraint}): {detail}"),
111 Self::IntegrityViolation => write!(f, "INTEGRITY_VIOLATION: CRC32C mismatch"),
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn compensation_codes() {
122 assert_eq!(
123 CompensationHint::UniqueViolation {
124 field: "email".into(),
125 conflicting_value: "a@b.com".into()
126 }
127 .code(),
128 "UNIQUE_VIOLATION"
129 );
130 assert_eq!(
131 CompensationHint::PermissionDenied.code(),
132 "PERMISSION_DENIED"
133 );
134 assert_eq!(
135 CompensationHint::RateLimited {
136 retry_after_ms: 5000
137 }
138 .code(),
139 "RATE_LIMITED"
140 );
141 }
142
143 #[test]
144 fn compensation_display() {
145 let hint = CompensationHint::UniqueViolation {
146 field: "username".into(),
147 conflicting_value: "alice".into(),
148 };
149 assert!(hint.to_string().contains("alice"));
150 assert!(hint.to_string().contains("username"));
151 }
152
153 #[test]
154 fn msgpack_roundtrip() {
155 let hint = CompensationHint::ForeignKeyMissing {
156 referenced_id: "user-42".into(),
157 };
158 let bytes = zerompk::to_msgpack_vec(&hint).unwrap();
159 let decoded: CompensationHint = zerompk::from_msgpack(&bytes).unwrap();
160 assert_eq!(hint, decoded);
161 }
162}