nodedb_types/sync/
violation.rs1use serde::{Deserialize, Serialize};
10
11#[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 ViolationType {
32 #[serde(rename = "rls_policy_violation")]
34 RlsPolicyViolation { policy_name: String },
35 #[serde(rename = "unique_violation")]
37 UniqueViolation { field: String, value: String },
38 #[serde(rename = "foreign_key_missing")]
40 ForeignKeyMissing { referenced_id: String },
41 #[serde(rename = "permission_denied")]
43 PermissionDenied,
44 #[serde(rename = "rate_limited")]
46 RateLimited,
47 #[serde(rename = "token_expired")]
49 TokenExpired,
50 #[serde(rename = "schema_violation")]
52 SchemaViolation { field: String, reason: String },
53 #[serde(rename = "constraint_violation")]
55 ConstraintViolation { detail: String },
56 #[serde(rename = "constraint_version_pending")]
60 ConstraintVersionPending {
61 collection: String,
62 required: u64,
63 installed: u64,
64 },
65}
66
67pub const CONSTRAINT_RETRY_AFTER_MS: u64 = 1000;
72
73impl std::fmt::Display for ViolationType {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Self::RlsPolicyViolation { policy_name } => {
77 write!(f, "rls_policy:{policy_name}")
78 }
79 Self::UniqueViolation { field, value } => {
80 write!(f, "unique:{field}={value}")
81 }
82 Self::ForeignKeyMissing { referenced_id } => {
83 write!(f, "fk_missing:{referenced_id}")
84 }
85 Self::PermissionDenied => write!(f, "permission_denied"),
86 Self::RateLimited => write!(f, "rate_limited"),
87 Self::TokenExpired => write!(f, "token_expired"),
88 Self::SchemaViolation { field, reason } => {
89 write!(f, "schema:{field}={reason}")
90 }
91 Self::ConstraintViolation { detail } => write!(f, "constraint:{detail}"),
92 Self::ConstraintVersionPending {
93 collection,
94 required,
95 installed,
96 } => {
97 write!(
98 f,
99 "constraint_version_pending:{collection} req={required} installed={installed}"
100 )
101 }
102 }
103 }
104}
105
106impl ViolationType {
107 pub fn to_compensation_hint(&self) -> super::compensation::CompensationHint {
112 use super::compensation::CompensationHint;
113 match self {
114 Self::UniqueViolation { field, value } => CompensationHint::UniqueViolation {
115 field: field.clone(),
116 conflicting_value: value.clone(),
117 },
118 Self::ForeignKeyMissing { referenced_id } => CompensationHint::ForeignKeyMissing {
119 referenced_id: referenced_id.clone(),
120 },
121 Self::RateLimited => CompensationHint::RateLimited {
122 retry_after_ms: 5000,
123 },
124 Self::RlsPolicyViolation { .. } | Self::PermissionDenied | Self::TokenExpired => {
126 CompensationHint::PermissionDenied
127 }
128 Self::SchemaViolation { field, reason } => CompensationHint::SchemaViolation {
129 field: field.clone(),
130 reason: reason.clone(),
131 },
132 Self::ConstraintViolation { detail } => CompensationHint::Custom {
133 constraint: "constraint".into(),
134 detail: detail.clone(),
135 },
136 Self::ConstraintVersionPending { .. } => CompensationHint::Retry {
137 retry_after_ms: CONSTRAINT_RETRY_AFTER_MS,
138 },
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn violation_display() {
149 assert_eq!(
150 ViolationType::PermissionDenied.to_string(),
151 "permission_denied"
152 );
153 assert_eq!(ViolationType::RateLimited.to_string(), "rate_limited");
154 assert_eq!(
155 ViolationType::UniqueViolation {
156 field: "email".into(),
157 value: "x@y.com".into()
158 }
159 .to_string(),
160 "unique:email=x@y.com"
161 );
162 }
163
164 #[test]
165 fn rls_violation_maps_to_permission_denied() {
166 let v = ViolationType::RlsPolicyViolation {
167 policy_name: "user_write_own".into(),
168 };
169 let hint = v.to_compensation_hint();
170 assert!(matches!(
172 hint,
173 super::super::compensation::CompensationHint::PermissionDenied
174 ));
175 }
176
177 #[test]
178 fn unique_violation_preserves_details() {
179 let v = ViolationType::UniqueViolation {
180 field: "username".into(),
181 value: "alice".into(),
182 };
183 let hint = v.to_compensation_hint();
184 match hint {
185 super::super::compensation::CompensationHint::UniqueViolation {
186 field,
187 conflicting_value,
188 } => {
189 assert_eq!(field, "username");
190 assert_eq!(conflicting_value, "alice");
191 }
192 _ => panic!("expected UniqueViolation hint"),
193 }
194 }
195
196 #[test]
197 fn token_expired_maps_to_permission_denied() {
198 let hint = ViolationType::TokenExpired.to_compensation_hint();
199 assert!(matches!(
200 hint,
201 super::super::compensation::CompensationHint::PermissionDenied
202 ));
203 }
204
205 #[test]
206 fn constraint_version_pending_maps_to_retry() {
207 let v = ViolationType::ConstraintVersionPending {
208 collection: "orders".into(),
209 required: 3,
210 installed: 2,
211 };
212 let hint = v.to_compensation_hint();
213 assert!(matches!(
214 hint,
215 super::super::compensation::CompensationHint::Retry { .. }
216 ));
217 }
218}