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)]
27#[serde(rename_all = "snake_case")]
28#[non_exhaustive]
29pub enum ViolationType {
30 #[serde(rename = "rls_policy_violation")]
32 RlsPolicyViolation { policy_name: String },
33 #[serde(rename = "unique_violation")]
35 UniqueViolation { field: String, value: String },
36 #[serde(rename = "foreign_key_missing")]
38 ForeignKeyMissing { referenced_id: String },
39 #[serde(rename = "permission_denied")]
41 PermissionDenied,
42 #[serde(rename = "rate_limited")]
44 RateLimited,
45 #[serde(rename = "token_expired")]
47 TokenExpired,
48 #[serde(rename = "schema_violation")]
50 SchemaViolation { field: String, reason: String },
51 #[serde(rename = "constraint_violation")]
53 ConstraintViolation { detail: String },
54}
55
56impl std::fmt::Display for ViolationType {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match self {
59 Self::RlsPolicyViolation { policy_name } => {
60 write!(f, "rls_policy:{policy_name}")
61 }
62 Self::UniqueViolation { field, value } => {
63 write!(f, "unique:{field}={value}")
64 }
65 Self::ForeignKeyMissing { referenced_id } => {
66 write!(f, "fk_missing:{referenced_id}")
67 }
68 Self::PermissionDenied => write!(f, "permission_denied"),
69 Self::RateLimited => write!(f, "rate_limited"),
70 Self::TokenExpired => write!(f, "token_expired"),
71 Self::SchemaViolation { field, reason } => {
72 write!(f, "schema:{field}={reason}")
73 }
74 Self::ConstraintViolation { detail } => write!(f, "constraint:{detail}"),
75 }
76 }
77}
78
79impl ViolationType {
80 pub fn to_compensation_hint(&self) -> super::compensation::CompensationHint {
85 use super::compensation::CompensationHint;
86 match self {
87 Self::UniqueViolation { field, value } => CompensationHint::UniqueViolation {
88 field: field.clone(),
89 conflicting_value: value.clone(),
90 },
91 Self::ForeignKeyMissing { referenced_id } => CompensationHint::ForeignKeyMissing {
92 referenced_id: referenced_id.clone(),
93 },
94 Self::RateLimited => CompensationHint::RateLimited {
95 retry_after_ms: 5000,
96 },
97 Self::RlsPolicyViolation { .. } | Self::PermissionDenied | Self::TokenExpired => {
99 CompensationHint::PermissionDenied
100 }
101 Self::SchemaViolation { field, reason } => CompensationHint::SchemaViolation {
102 field: field.clone(),
103 reason: reason.clone(),
104 },
105 Self::ConstraintViolation { detail } => CompensationHint::Custom {
106 constraint: "constraint".into(),
107 detail: detail.clone(),
108 },
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn violation_display() {
119 assert_eq!(
120 ViolationType::PermissionDenied.to_string(),
121 "permission_denied"
122 );
123 assert_eq!(ViolationType::RateLimited.to_string(), "rate_limited");
124 assert_eq!(
125 ViolationType::UniqueViolation {
126 field: "email".into(),
127 value: "x@y.com".into()
128 }
129 .to_string(),
130 "unique:email=x@y.com"
131 );
132 }
133
134 #[test]
135 fn rls_violation_maps_to_permission_denied() {
136 let v = ViolationType::RlsPolicyViolation {
137 policy_name: "user_write_own".into(),
138 };
139 let hint = v.to_compensation_hint();
140 assert!(matches!(
142 hint,
143 super::super::compensation::CompensationHint::PermissionDenied
144 ));
145 }
146
147 #[test]
148 fn unique_violation_preserves_details() {
149 let v = ViolationType::UniqueViolation {
150 field: "username".into(),
151 value: "alice".into(),
152 };
153 let hint = v.to_compensation_hint();
154 match hint {
155 super::super::compensation::CompensationHint::UniqueViolation {
156 field,
157 conflicting_value,
158 } => {
159 assert_eq!(field, "username");
160 assert_eq!(conflicting_value, "alice");
161 }
162 _ => panic!("expected UniqueViolation hint"),
163 }
164 }
165
166 #[test]
167 fn token_expired_maps_to_permission_denied() {
168 let hint = ViolationType::TokenExpired.to_compensation_hint();
169 assert!(matches!(
170 hint,
171 super::super::compensation::CompensationHint::PermissionDenied
172 ));
173 }
174}