nodedb_types/sync/
violation.rs1use serde::{Deserialize, Serialize};
8
9#[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 ViolationType {
26 RlsPolicyViolation { policy_name: String },
28 UniqueViolation { field: String, value: String },
30 ForeignKeyMissing { referenced_id: String },
32 PermissionDenied,
34 RateLimited,
36 TokenExpired,
38 SchemaViolation { field: String, reason: String },
40 ConstraintViolation { detail: String },
42}
43
44impl std::fmt::Display for ViolationType {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::RlsPolicyViolation { policy_name } => {
48 write!(f, "rls_policy:{policy_name}")
49 }
50 Self::UniqueViolation { field, value } => {
51 write!(f, "unique:{field}={value}")
52 }
53 Self::ForeignKeyMissing { referenced_id } => {
54 write!(f, "fk_missing:{referenced_id}")
55 }
56 Self::PermissionDenied => write!(f, "permission_denied"),
57 Self::RateLimited => write!(f, "rate_limited"),
58 Self::TokenExpired => write!(f, "token_expired"),
59 Self::SchemaViolation { field, reason } => {
60 write!(f, "schema:{field}={reason}")
61 }
62 Self::ConstraintViolation { detail } => write!(f, "constraint:{detail}"),
63 }
64 }
65}
66
67impl ViolationType {
68 pub fn to_compensation_hint(&self) -> super::compensation::CompensationHint {
73 use super::compensation::CompensationHint;
74 match self {
75 Self::UniqueViolation { field, value } => CompensationHint::UniqueViolation {
76 field: field.clone(),
77 conflicting_value: value.clone(),
78 },
79 Self::ForeignKeyMissing { referenced_id } => CompensationHint::ForeignKeyMissing {
80 referenced_id: referenced_id.clone(),
81 },
82 Self::RateLimited => CompensationHint::RateLimited {
83 retry_after_ms: 5000,
84 },
85 Self::RlsPolicyViolation { .. } | Self::PermissionDenied | Self::TokenExpired => {
87 CompensationHint::PermissionDenied
88 }
89 Self::SchemaViolation { field, reason } => CompensationHint::SchemaViolation {
90 field: field.clone(),
91 reason: reason.clone(),
92 },
93 Self::ConstraintViolation { detail } => CompensationHint::Custom {
94 constraint: "constraint".into(),
95 detail: detail.clone(),
96 },
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn violation_display() {
107 assert_eq!(
108 ViolationType::PermissionDenied.to_string(),
109 "permission_denied"
110 );
111 assert_eq!(ViolationType::RateLimited.to_string(), "rate_limited");
112 assert_eq!(
113 ViolationType::UniqueViolation {
114 field: "email".into(),
115 value: "x@y.com".into()
116 }
117 .to_string(),
118 "unique:email=x@y.com"
119 );
120 }
121
122 #[test]
123 fn rls_violation_maps_to_permission_denied() {
124 let v = ViolationType::RlsPolicyViolation {
125 policy_name: "user_write_own".into(),
126 };
127 let hint = v.to_compensation_hint();
128 assert!(matches!(
130 hint,
131 super::super::compensation::CompensationHint::PermissionDenied
132 ));
133 }
134
135 #[test]
136 fn unique_violation_preserves_details() {
137 let v = ViolationType::UniqueViolation {
138 field: "username".into(),
139 value: "alice".into(),
140 };
141 let hint = v.to_compensation_hint();
142 match hint {
143 super::super::compensation::CompensationHint::UniqueViolation {
144 field,
145 conflicting_value,
146 } => {
147 assert_eq!(field, "username");
148 assert_eq!(conflicting_value, "alice");
149 }
150 _ => panic!("expected UniqueViolation hint"),
151 }
152 }
153
154 #[test]
155 fn token_expired_maps_to_permission_denied() {
156 let hint = ViolationType::TokenExpired.to_compensation_hint();
157 assert!(matches!(
158 hint,
159 super::super::compensation::CompensationHint::PermissionDenied
160 ));
161 }
162}