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)]
25pub enum CompensationHint {
26 UniqueViolation {
28 field: String,
30 conflicting_value: String,
32 },
33
34 ForeignKeyMissing {
36 referenced_id: String,
38 },
39
40 PermissionDenied,
43
44 RateLimited {
46 retry_after_ms: u64,
48 },
49
50 SchemaViolation {
52 field: String,
54 reason: String,
56 },
57
58 Custom {
60 constraint: String,
62 detail: String,
64 },
65
66 IntegrityViolation,
69}
70
71impl CompensationHint {
72 pub fn code(&self) -> &'static str {
74 match self {
75 Self::UniqueViolation { .. } => "UNIQUE_VIOLATION",
76 Self::ForeignKeyMissing { .. } => "FK_MISSING",
77 Self::PermissionDenied => "PERMISSION_DENIED",
78 Self::RateLimited { .. } => "RATE_LIMITED",
79 Self::SchemaViolation { .. } => "SCHEMA_VIOLATION",
80 Self::Custom { .. } => "CUSTOM",
81 Self::IntegrityViolation => "INTEGRITY_VIOLATION",
82 }
83 }
84}
85
86impl std::fmt::Display for CompensationHint {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 Self::UniqueViolation {
90 field,
91 conflicting_value,
92 } => write!(
93 f,
94 "UNIQUE({field}): value '{conflicting_value}' already exists"
95 ),
96 Self::ForeignKeyMissing { referenced_id } => {
97 write!(f, "FK_MISSING: referenced ID '{referenced_id}' not found")
98 }
99 Self::PermissionDenied => write!(f, "PERMISSION_DENIED"),
100 Self::RateLimited { retry_after_ms } => {
101 write!(f, "RATE_LIMITED: retry after {retry_after_ms}ms")
102 }
103 Self::SchemaViolation { field, reason } => {
104 write!(f, "SCHEMA({field}): {reason}")
105 }
106 Self::Custom {
107 constraint, detail, ..
108 } => write!(f, "CUSTOM({constraint}): {detail}"),
109 Self::IntegrityViolation => write!(f, "INTEGRITY_VIOLATION: CRC32C mismatch"),
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn compensation_codes() {
120 assert_eq!(
121 CompensationHint::UniqueViolation {
122 field: "email".into(),
123 conflicting_value: "a@b.com".into()
124 }
125 .code(),
126 "UNIQUE_VIOLATION"
127 );
128 assert_eq!(
129 CompensationHint::PermissionDenied.code(),
130 "PERMISSION_DENIED"
131 );
132 assert_eq!(
133 CompensationHint::RateLimited {
134 retry_after_ms: 5000
135 }
136 .code(),
137 "RATE_LIMITED"
138 );
139 }
140
141 #[test]
142 fn compensation_display() {
143 let hint = CompensationHint::UniqueViolation {
144 field: "username".into(),
145 conflicting_value: "alice".into(),
146 };
147 assert!(hint.to_string().contains("alice"));
148 assert!(hint.to_string().contains("username"));
149 }
150
151 #[test]
152 fn msgpack_roundtrip() {
153 let hint = CompensationHint::ForeignKeyMissing {
154 referenced_id: "user-42".into(),
155 };
156 let bytes = rmp_serde::to_vec_named(&hint).unwrap();
157 let decoded: CompensationHint = rmp_serde::from_slice(&bytes).unwrap();
158 assert_eq!(hint, decoded);
159 }
160}