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 fn code(&self) -> &'static str {
96 match self {
97 Self::UniqueViolation { .. } => "UNIQUE_VIOLATION",
98 Self::ForeignKeyMissing { .. } => "FK_MISSING",
99 Self::PermissionDenied => "PERMISSION_DENIED",
100 Self::RateLimited { .. } => "RATE_LIMITED",
101 Self::SchemaViolation { .. } => "SCHEMA_VIOLATION",
102 Self::Custom { .. } => "CUSTOM",
103 Self::IntegrityViolation => "INTEGRITY_VIOLATION",
104 Self::Retry { .. } => "RETRY",
105 }
106 }
107}
108
109impl std::fmt::Display for CompensationHint {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 Self::UniqueViolation {
113 field,
114 conflicting_value,
115 } => write!(
116 f,
117 "UNIQUE({field}): value '{conflicting_value}' already exists"
118 ),
119 Self::ForeignKeyMissing { referenced_id } => {
120 write!(f, "FK_MISSING: referenced ID '{referenced_id}' not found")
121 }
122 Self::PermissionDenied => write!(f, "PERMISSION_DENIED"),
123 Self::RateLimited { retry_after_ms } => {
124 write!(f, "RATE_LIMITED: retry after {retry_after_ms}ms")
125 }
126 Self::SchemaViolation { field, reason } => {
127 write!(f, "SCHEMA({field}): {reason}")
128 }
129 Self::Custom {
130 constraint, detail, ..
131 } => write!(f, "CUSTOM({constraint}): {detail}"),
132 Self::IntegrityViolation => write!(f, "INTEGRITY_VIOLATION: CRC32C mismatch"),
133 Self::Retry { retry_after_ms } => {
134 write!(f, "RETRY: retry after {retry_after_ms}ms")
135 }
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn compensation_codes() {
146 assert_eq!(
147 CompensationHint::UniqueViolation {
148 field: "email".into(),
149 conflicting_value: "a@b.com".into()
150 }
151 .code(),
152 "UNIQUE_VIOLATION"
153 );
154 assert_eq!(
155 CompensationHint::PermissionDenied.code(),
156 "PERMISSION_DENIED"
157 );
158 assert_eq!(
159 CompensationHint::RateLimited {
160 retry_after_ms: 5000
161 }
162 .code(),
163 "RATE_LIMITED"
164 );
165 }
166
167 #[test]
168 fn compensation_display() {
169 let hint = CompensationHint::UniqueViolation {
170 field: "username".into(),
171 conflicting_value: "alice".into(),
172 };
173 assert!(hint.to_string().contains("alice"));
174 assert!(hint.to_string().contains("username"));
175 }
176
177 #[test]
178 fn retry_code_and_display() {
179 let hint = CompensationHint::Retry {
180 retry_after_ms: 1000,
181 };
182 assert_eq!(hint.code(), "RETRY");
183 assert!(hint.to_string().contains("1000"));
184 }
185
186 #[test]
187 fn msgpack_roundtrip() {
188 let hint = CompensationHint::ForeignKeyMissing {
189 referenced_id: "user-42".into(),
190 };
191 let bytes = zerompk::to_msgpack_vec(&hint).unwrap();
192 let decoded: CompensationHint = zerompk::from_msgpack(&bytes).unwrap();
193 assert_eq!(hint, decoded);
194 }
195}