nodedb_types/sync/
compensation.rs1use serde::{Deserialize, Serialize};
11
12#[derive(
17 Debug,
18 Clone,
19 PartialEq,
20 Eq,
21 Serialize,
22 Deserialize,
23 rkyv::Archive,
24 rkyv::Serialize,
25 rkyv::Deserialize,
26 zerompk::ToMessagePack,
27 zerompk::FromMessagePack,
28)]
29#[serde(rename_all = "snake_case")]
30#[non_exhaustive]
31pub enum CompensationHint {
32 #[serde(rename = "unique_violation")]
34 UniqueViolation {
35 field: String,
37 conflicting_value: String,
39 },
40
41 #[serde(rename = "foreign_key_missing")]
43 ForeignKeyMissing {
44 referenced_id: String,
46 },
47
48 #[serde(rename = "permission_denied")]
51 PermissionDenied,
52
53 #[serde(rename = "rate_limited")]
55 RateLimited {
56 retry_after_ms: u64,
58 },
59
60 #[serde(rename = "schema_violation")]
62 SchemaViolation {
63 field: String,
65 reason: String,
67 },
68
69 #[serde(rename = "custom")]
71 Custom {
72 constraint: String,
74 detail: String,
76 },
77
78 #[serde(rename = "integrity_violation")]
81 IntegrityViolation,
82
83 #[serde(rename = "retry")]
87 Retry {
88 retry_after_ms: u64,
90 },
91}
92
93impl CompensationHint {
94 pub const PEER_ID_COLLISION: &'static str = "peer_id_collision";
104
105 pub fn is_peer_id_collision(&self) -> bool {
107 matches!(
108 self,
109 Self::Custom { constraint, .. } if constraint == Self::PEER_ID_COLLISION
110 )
111 }
112
113 pub fn code(&self) -> &'static str {
115 match self {
116 Self::UniqueViolation { .. } => "UNIQUE_VIOLATION",
117 Self::ForeignKeyMissing { .. } => "FK_MISSING",
118 Self::PermissionDenied => "PERMISSION_DENIED",
119 Self::RateLimited { .. } => "RATE_LIMITED",
120 Self::SchemaViolation { .. } => "SCHEMA_VIOLATION",
121 Self::Custom { .. } => "CUSTOM",
122 Self::IntegrityViolation => "INTEGRITY_VIOLATION",
123 Self::Retry { .. } => "RETRY",
124 }
125 }
126}
127
128impl std::fmt::Display for CompensationHint {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 match self {
131 Self::UniqueViolation {
132 field,
133 conflicting_value,
134 } => write!(
135 f,
136 "UNIQUE({field}): value '{conflicting_value}' already exists"
137 ),
138 Self::ForeignKeyMissing { referenced_id } => {
139 write!(f, "FK_MISSING: referenced ID '{referenced_id}' not found")
140 }
141 Self::PermissionDenied => write!(f, "PERMISSION_DENIED"),
142 Self::RateLimited { retry_after_ms } => {
143 write!(f, "RATE_LIMITED: retry after {retry_after_ms}ms")
144 }
145 Self::SchemaViolation { field, reason } => {
146 write!(f, "SCHEMA({field}): {reason}")
147 }
148 Self::Custom {
149 constraint, detail, ..
150 } => write!(f, "CUSTOM({constraint}): {detail}"),
151 Self::IntegrityViolation => write!(f, "INTEGRITY_VIOLATION: CRC32C mismatch"),
152 Self::Retry { retry_after_ms } => {
153 write!(f, "RETRY: retry after {retry_after_ms}ms")
154 }
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn compensation_codes() {
165 assert_eq!(
166 CompensationHint::UniqueViolation {
167 field: "email".into(),
168 conflicting_value: "a@b.com".into()
169 }
170 .code(),
171 "UNIQUE_VIOLATION"
172 );
173 assert_eq!(
174 CompensationHint::PermissionDenied.code(),
175 "PERMISSION_DENIED"
176 );
177 assert_eq!(
178 CompensationHint::RateLimited {
179 retry_after_ms: 5000
180 }
181 .code(),
182 "RATE_LIMITED"
183 );
184 }
185
186 #[test]
187 fn compensation_display() {
188 let hint = CompensationHint::UniqueViolation {
189 field: "username".into(),
190 conflicting_value: "alice".into(),
191 };
192 assert!(hint.to_string().contains("alice"));
193 assert!(hint.to_string().contains("username"));
194 }
195
196 #[test]
197 fn retry_code_and_display() {
198 let hint = CompensationHint::Retry {
199 retry_after_ms: 1000,
200 };
201 assert_eq!(hint.code(), "RETRY");
202 assert!(hint.to_string().contains("1000"));
203 }
204
205 #[test]
206 fn msgpack_roundtrip() {
207 let hint = CompensationHint::ForeignKeyMissing {
208 referenced_id: "user-42".into(),
209 };
210 let bytes = zerompk::to_msgpack_vec(&hint).unwrap();
211 let decoded: CompensationHint = zerompk::from_msgpack(&bytes).unwrap();
212 assert_eq!(hint, decoded);
213 }
214}