use serde::{Deserialize, Serialize};
#[derive(
Debug,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub enum ViolationType {
RlsPolicyViolation { policy_name: String },
UniqueViolation { field: String, value: String },
ForeignKeyMissing { referenced_id: String },
PermissionDenied,
RateLimited,
TokenExpired,
SchemaViolation { field: String, reason: String },
ConstraintViolation { detail: String },
}
impl std::fmt::Display for ViolationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RlsPolicyViolation { policy_name } => {
write!(f, "rls_policy:{policy_name}")
}
Self::UniqueViolation { field, value } => {
write!(f, "unique:{field}={value}")
}
Self::ForeignKeyMissing { referenced_id } => {
write!(f, "fk_missing:{referenced_id}")
}
Self::PermissionDenied => write!(f, "permission_denied"),
Self::RateLimited => write!(f, "rate_limited"),
Self::TokenExpired => write!(f, "token_expired"),
Self::SchemaViolation { field, reason } => {
write!(f, "schema:{field}={reason}")
}
Self::ConstraintViolation { detail } => write!(f, "constraint:{detail}"),
}
}
}
impl ViolationType {
pub fn to_compensation_hint(&self) -> super::compensation::CompensationHint {
use super::compensation::CompensationHint;
match self {
Self::UniqueViolation { field, value } => CompensationHint::UniqueViolation {
field: field.clone(),
conflicting_value: value.clone(),
},
Self::ForeignKeyMissing { referenced_id } => CompensationHint::ForeignKeyMissing {
referenced_id: referenced_id.clone(),
},
Self::RateLimited => CompensationHint::RateLimited {
retry_after_ms: 5000,
},
Self::RlsPolicyViolation { .. } | Self::PermissionDenied | Self::TokenExpired => {
CompensationHint::PermissionDenied
}
Self::SchemaViolation { field, reason } => CompensationHint::SchemaViolation {
field: field.clone(),
reason: reason.clone(),
},
Self::ConstraintViolation { detail } => CompensationHint::Custom {
constraint: "constraint".into(),
detail: detail.clone(),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn violation_display() {
assert_eq!(
ViolationType::PermissionDenied.to_string(),
"permission_denied"
);
assert_eq!(ViolationType::RateLimited.to_string(), "rate_limited");
assert_eq!(
ViolationType::UniqueViolation {
field: "email".into(),
value: "x@y.com".into()
}
.to_string(),
"unique:email=x@y.com"
);
}
#[test]
fn rls_violation_maps_to_permission_denied() {
let v = ViolationType::RlsPolicyViolation {
policy_name: "user_write_own".into(),
};
let hint = v.to_compensation_hint();
assert!(matches!(
hint,
super::super::compensation::CompensationHint::PermissionDenied
));
}
#[test]
fn unique_violation_preserves_details() {
let v = ViolationType::UniqueViolation {
field: "username".into(),
value: "alice".into(),
};
let hint = v.to_compensation_hint();
match hint {
super::super::compensation::CompensationHint::UniqueViolation {
field,
conflicting_value,
} => {
assert_eq!(field, "username");
assert_eq!(conflicting_value, "alice");
}
_ => panic!("expected UniqueViolation hint"),
}
}
#[test]
fn token_expired_maps_to_permission_denied() {
let hint = ViolationType::TokenExpired.to_compensation_hint();
assert!(matches!(
hint,
super::super::compensation::CompensationHint::PermissionDenied
));
}
}