term_guard/core/
constraint.rs1use crate::prelude::*;
4use async_trait::async_trait;
5use datafusion::prelude::*;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt::Debug;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum ConstraintStatus {
14 Success,
16 Failure,
18 Skipped,
20}
21
22impl ConstraintStatus {
23 pub fn is_success(&self) -> bool {
25 matches!(self, ConstraintStatus::Success)
26 }
27
28 pub fn is_failure(&self) -> bool {
30 matches!(self, ConstraintStatus::Failure)
31 }
32
33 pub fn is_skipped(&self) -> bool {
35 matches!(self, ConstraintStatus::Skipped)
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ConstraintResult {
42 pub status: ConstraintStatus,
44 pub metric: Option<f64>,
46 pub message: Option<String>,
48}
49
50impl ConstraintResult {
51 pub fn success() -> Self {
53 Self {
54 status: ConstraintStatus::Success,
55 metric: None,
56 message: None,
57 }
58 }
59
60 pub fn success_with_metric(metric: f64) -> Self {
62 Self {
63 status: ConstraintStatus::Success,
64 metric: Some(metric),
65 message: None,
66 }
67 }
68
69 pub fn failure(message: impl Into<String>) -> Self {
71 Self {
72 status: ConstraintStatus::Failure,
73 metric: None,
74 message: Some(message.into()),
75 }
76 }
77
78 pub fn failure_with_metric(metric: f64, message: impl Into<String>) -> Self {
80 Self {
81 status: ConstraintStatus::Failure,
82 metric: Some(metric),
83 message: Some(message.into()),
84 }
85 }
86
87 pub fn skipped(message: impl Into<String>) -> Self {
89 Self {
90 status: ConstraintStatus::Skipped,
91 metric: None,
92 message: Some(message.into()),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
102pub struct ConstraintMetadata {
103 pub columns: Vec<String>,
105 pub description: Option<String>,
107 #[serde(skip_serializing_if = "HashMap::is_empty")]
109 pub custom: HashMap<String, String>,
110}
111
112impl ConstraintMetadata {
113 pub fn new() -> Self {
115 Self::default()
116 }
117
118 pub fn for_column(column: impl Into<String>) -> Self {
120 Self {
121 columns: vec![column.into()],
122 description: None,
123 custom: HashMap::new(),
124 }
125 }
126
127 pub fn for_columns<I, S>(columns: I) -> Self
129 where
130 I: IntoIterator<Item = S>,
131 S: Into<String>,
132 {
133 Self {
134 columns: columns.into_iter().map(Into::into).collect(),
135 description: None,
136 custom: HashMap::new(),
137 }
138 }
139
140 pub fn with_description(mut self, description: impl Into<String>) -> Self {
142 self.description = Some(description.into());
143 self
144 }
145
146 pub fn with_custom(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
148 self.custom.insert(key.into(), value.into());
149 self
150 }
151}
152
153#[async_trait]
187pub trait Constraint: Debug + Send + Sync {
188 async fn evaluate(&self, ctx: &SessionContext) -> Result<ConstraintResult>;
198
199 fn name(&self) -> &str;
201
202 fn column(&self) -> Option<&str> {
207 None
208 }
209
210 fn description(&self) -> Option<&str> {
215 None
216 }
217
218 fn metadata(&self) -> ConstraintMetadata {
223 ConstraintMetadata::new()
224 }
225}
226
227pub type BoxedConstraint = Box<dyn Constraint>;
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_constraint_metadata_builder() {
236 let metadata = ConstraintMetadata::for_column("user_id")
237 .with_description("Checks user ID validity")
238 .with_custom("severity", "high")
239 .with_custom("category", "identity");
240
241 assert_eq!(metadata.columns, vec!["user_id"]);
242 assert_eq!(
243 metadata.description,
244 Some("Checks user ID validity".to_string())
245 );
246 assert_eq!(metadata.custom.get("severity"), Some(&"high".to_string()));
247 assert_eq!(
248 metadata.custom.get("category"),
249 Some(&"identity".to_string())
250 );
251 }
252
253 #[test]
254 fn test_constraint_metadata_multi_column() {
255 let metadata = ConstraintMetadata::for_columns(vec!["first_name", "last_name"])
256 .with_description("Checks name completeness");
257
258 assert_eq!(metadata.columns, vec!["first_name", "last_name"]);
259 assert_eq!(
260 metadata.description,
261 Some("Checks name completeness".to_string())
262 );
263 }
264
265 #[test]
266 fn test_constraint_result_builders() {
267 let success = ConstraintResult::success();
268 assert_eq!(success.status, ConstraintStatus::Success);
269 assert!(success.metric.is_none());
270 assert!(success.message.is_none());
271
272 let success_with_metric = ConstraintResult::success_with_metric(0.95);
273 assert_eq!(success_with_metric.status, ConstraintStatus::Success);
274 assert_eq!(success_with_metric.metric, Some(0.95));
275
276 let failure = ConstraintResult::failure("Validation failed");
277 assert_eq!(failure.status, ConstraintStatus::Failure);
278 assert_eq!(failure.message, Some("Validation failed".to_string()));
279
280 let failure_with_metric = ConstraintResult::failure_with_metric(0.3, "Below threshold");
281 assert_eq!(failure_with_metric.status, ConstraintStatus::Failure);
282 assert_eq!(failure_with_metric.metric, Some(0.3));
283 assert_eq!(
284 failure_with_metric.message,
285 Some("Below threshold".to_string())
286 );
287
288 let skipped = ConstraintResult::skipped("No data");
289 assert_eq!(skipped.status, ConstraintStatus::Skipped);
290 assert_eq!(skipped.message, Some("No data".to_string()));
291 }
292}