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