term_guard/core/check.rs
1//! Check type and builder for grouping constraints.
2#![allow(deprecated)] // Allow deprecated constraints for backward compatibility
3#![allow(clippy::expect_used)] // Allow expect() in builder methods as they would require API changes
4//!
5//! This module provides the [`Check`] type and [`CheckBuilder`] for creating validation checks.
6//! Starting with v0.2.0, we provide a new unified API through the `builder_extensions` module
7//! that offers improved consistency and performance.
8//!
9//! ## New Unified API Example (v0.2.0+)
10//!
11//! ```rust
12//! use term_guard::core::{Check, Level};
13//! use term_guard::core::builder_extensions::{CompletenessOptions, StatisticalOptions};
14//! use term_guard::constraints::{Assertion, FormatType, FormatOptions};
15//!
16//! # use term_guard::prelude::*;
17//! # fn example() -> Result<Check> {
18//! let check = Check::builder("user_validation")
19//! .level(Level::Error)
20//! // Completeness with options
21//! .completeness("user_id", CompletenessOptions::full().into_constraint_options())
22//! .completeness("email", CompletenessOptions::threshold(0.95).into_constraint_options())
23//! // Format validation
24//! .has_format("email", FormatType::Email, 0.95, FormatOptions::default())
25//! // Combined statistics (single query)
26//! .statistics(
27//! "age",
28//! StatisticalOptions::new()
29//! .min(Assertion::GreaterThanOrEqual(18.0))
30//! .max(Assertion::LessThan(100.0))
31//! .mean(Assertion::Between(25.0, 65.0))
32//! )?
33//! // Convenience method
34//! .primary_key(vec!["user_id"])
35//! .build();
36//! # Ok(check)
37//! # }
38//! ```
39
40use super::{constraint::BoxedConstraint, Constraint, Level};
41use crate::constraints::{
42 ApproxCountDistinctConstraint, Assertion, ColumnCountConstraint, CorrelationConstraint,
43 CustomSqlConstraint, DataTypeConstraint, FormatConstraint, FormatOptions, FormatType,
44 HistogramAssertion, HistogramConstraint, NullHandling, QuantileConstraint, SizeConstraint,
45 UniquenessConstraint, UniquenessOptions, UniquenessType,
46};
47use std::sync::Arc;
48
49/// A validation check containing one or more constraints.
50///
51/// A `Check` groups related constraints together and assigns them a severity level.
52/// Checks are the building blocks of validation suites. When a check runs, all its
53/// constraints are evaluated, and the check fails if any constraint fails.
54///
55/// # Examples
56///
57/// ## Basic Check
58///
59/// ```rust
60/// use term_guard::core::{Check, Level};
61///
62/// let check = Check::builder("user_data_quality")
63/// .level(Level::Error)
64/// .description("Validates user data quality")
65/// .build();
66/// ```
67///
68/// ## Check with Constraints
69///
70/// ```rust
71/// use term_guard::core::{Check, Level, ConstraintOptions};
72/// use term_guard::constraints::{Assertion, UniquenessType, StatisticType, FormatType, FormatOptions};
73///
74/// let check = Check::builder("customer_validation")
75/// .level(Level::Error)
76/// .description("Ensure customer data integrity")
77/// // Completeness checks using unified API
78/// .completeness("customer_id", ConstraintOptions::new().with_threshold(1.0))
79/// .completeness("email", ConstraintOptions::new().with_threshold(0.99))
80/// // Uniqueness checks using unified API
81/// .validates_uniqueness(vec!["customer_id"], 1.0)
82/// .validates_uniqueness(vec!["email", "region"], 1.0)
83/// // Pattern validation using format
84/// .has_format("phone", FormatType::Regex(r"^\+?\d{3}-\d{3}-\d{4}$".to_string()), 0.95, FormatOptions::default())
85/// // Range checks using statistic
86/// .statistic("age", StatisticType::Min, Assertion::GreaterThanOrEqual(18.0))
87/// .statistic("age", StatisticType::Max, Assertion::LessThanOrEqual(120.0))
88/// .build();
89/// ```
90///
91/// ## Data Quality Check
92///
93/// ```rust
94/// use term_guard::core::{Check, Level};
95/// use term_guard::constraints::{Assertion, StatisticType};
96///
97/// let check = Check::builder("data_types_and_formats")
98/// .level(Level::Warning)
99/// // Ensure consistent data types using the unified API
100/// .has_consistent_data_type("order_date", 0.99)
101/// .has_consistent_data_type("product_id", 0.95)
102/// // String length validation
103/// .has_min_length("password", 8)
104/// .has_max_length("username", 20)
105/// // Check for PII
106/// .validates_credit_card("comments", 0.0, true) // Should be 0%
107/// .validates_email("email_field", 0.98)
108/// // Statistical checks
109/// .statistic("order_value", StatisticType::Mean, Assertion::Between(50.0, 500.0))
110/// .statistic("response_time", StatisticType::StandardDeviation, Assertion::LessThan(100.0))
111/// .build();
112/// ```
113///
114/// ## Enhanced Format Validation
115///
116/// ```rust
117/// use term_guard::core::{Check, Level};
118/// use term_guard::constraints::FormatOptions;
119///
120/// let check = Check::builder("enhanced_format_validation")
121/// .level(Level::Error)
122/// // Basic format validation
123/// .validates_email("email", 0.95)
124/// .validates_url("website", 0.90, false)
125/// .validates_phone("phone", 0.85, Some("US"))
126/// // Enhanced format validation with options
127/// .validates_email_with_options(
128/// "secondary_email",
129/// 0.80,
130/// FormatOptions::lenient() // Case insensitive, trimming, nulls allowed
131/// )
132/// .validates_url_with_options(
133/// "dev_url",
134/// 0.75,
135/// true, // allow localhost
136/// FormatOptions::case_insensitive().trim_before_check(true)
137/// )
138/// .validates_regex_with_options(
139/// "product_code",
140/// r"^[A-Z]{2}\d{4}$",
141/// 0.98,
142/// FormatOptions::strict() // Case sensitive, no nulls, no trimming
143/// )
144/// .build();
145/// ```
146#[derive(Debug, Clone)]
147pub struct Check {
148 /// The name of the check
149 name: String,
150 /// The severity level of the check
151 level: Level,
152 /// Optional description of what this check validates
153 description: Option<String>,
154 /// The constraints that make up this check
155 constraints: Vec<Arc<dyn Constraint>>,
156}
157
158impl Check {
159 /// Creates a new builder for constructing a check.
160 ///
161 /// # Arguments
162 ///
163 /// * `name` - The name of the check
164 ///
165 /// # Examples
166 ///
167 /// ```rust
168 /// use term_guard::core::Check;
169 ///
170 /// let builder = Check::builder("data_quality");
171 /// ```
172 pub fn builder(name: impl Into<String>) -> CheckBuilder {
173 CheckBuilder::new(name)
174 }
175
176 /// Returns the name of the check.
177 pub fn name(&self) -> &str {
178 &self.name
179 }
180
181 /// Returns the severity level of the check.
182 pub fn level(&self) -> Level {
183 self.level
184 }
185
186 /// Returns the description of the check if available.
187 pub fn description(&self) -> Option<&str> {
188 self.description.as_deref()
189 }
190
191 /// Returns the constraints in this check.
192 pub fn constraints(&self) -> &[Arc<dyn Constraint>] {
193 &self.constraints
194 }
195}
196
197/// Builder for constructing `Check` instances.
198///
199/// # Examples
200///
201/// ```rust
202/// use term_guard::core::{Check, Level};
203///
204/// let check = Check::builder("completeness_check")
205/// .level(Level::Error)
206/// .description("Ensures all required fields are present")
207/// .build();
208/// ```
209#[derive(Debug)]
210pub struct CheckBuilder {
211 name: String,
212 level: Level,
213 description: Option<String>,
214 constraints: Vec<Arc<dyn Constraint>>,
215}
216
217impl CheckBuilder {
218 /// Creates a new check builder with the given name.
219 pub fn new(name: impl Into<String>) -> Self {
220 Self {
221 name: name.into(),
222 level: Level::default(),
223 description: None,
224 constraints: Vec::new(),
225 }
226 }
227
228 /// Sets the severity level for the check.
229 ///
230 /// # Arguments
231 ///
232 /// * `level` - The severity level
233 ///
234 /// # Examples
235 ///
236 /// ```rust
237 /// use term_guard::core::{Check, Level};
238 ///
239 /// let check = Check::builder("critical_check")
240 /// .level(Level::Error)
241 /// .build();
242 /// ```
243 pub fn level(mut self, level: Level) -> Self {
244 self.level = level;
245 self
246 }
247
248 /// Sets the description for the check.
249 ///
250 /// # Arguments
251 ///
252 /// * `description` - A description of what this check validates
253 pub fn description(mut self, description: impl Into<String>) -> Self {
254 self.description = Some(description.into());
255 self
256 }
257
258 /// Adds a constraint to the check.
259 ///
260 /// # Arguments
261 ///
262 /// * `constraint` - The constraint to add
263 pub fn constraint(mut self, constraint: impl Constraint + 'static) -> Self {
264 self.constraints.push(Arc::new(constraint));
265 self
266 }
267
268 /// Adds a boxed constraint to the check.
269 ///
270 /// # Arguments
271 ///
272 /// * `constraint` - The boxed constraint to add
273 pub fn boxed_constraint(mut self, constraint: BoxedConstraint) -> Self {
274 self.constraints.push(Arc::from(constraint));
275 self
276 }
277
278 /// Adds multiple constraints to the check.
279 ///
280 /// # Arguments
281 ///
282 /// * `constraints` - An iterator of constraints to add
283 pub fn constraints<I>(mut self, constraints: I) -> Self
284 where
285 I: IntoIterator<Item = BoxedConstraint>,
286 {
287 self.constraints
288 .extend(constraints.into_iter().map(Arc::from));
289 self
290 }
291
292 // Builder methods
293
294 /// Adds a constraint that checks the dataset size (row count).
295 ///
296 /// # Arguments
297 ///
298 /// * `assertion` - The assertion to evaluate against the row count
299 ///
300 /// # Examples
301 ///
302 /// ```rust
303 /// use term_guard::core::{Check, Level};
304 /// use term_guard::constraints::Assertion;
305 ///
306 /// let check = Check::builder("size_validation")
307 /// .level(Level::Error)
308 /// .has_size(Assertion::GreaterThan(1000.0))
309 /// .build();
310 /// ```
311 pub fn has_size(mut self, assertion: Assertion) -> Self {
312 self.constraints
313 .push(Arc::new(SizeConstraint::new(assertion)));
314 self
315 }
316
317 /// Adds a constraint that checks the number of columns in the dataset.
318 ///
319 /// This constraint validates that the dataset has the expected number of columns
320 /// by examining the schema.
321 ///
322 /// # Arguments
323 ///
324 /// * `assertion` - The assertion to evaluate against the column count
325 ///
326 /// # Examples
327 ///
328 /// ```rust
329 /// use term_guard::core::{Check, Level};
330 /// use term_guard::constraints::Assertion;
331 ///
332 /// let check = Check::builder("schema_validation")
333 /// .level(Level::Error)
334 /// .has_column_count(Assertion::Equals(15.0))
335 /// .has_column_count(Assertion::GreaterThanOrEqual(10.0))
336 /// .build();
337 /// ```
338 pub fn has_column_count(mut self, assertion: Assertion) -> Self {
339 self.constraints
340 .push(Arc::new(ColumnCountConstraint::new(assertion)));
341 self
342 }
343
344 /// Adds a constraint that checks the approximate count of distinct values in a column.
345 ///
346 /// Uses DataFusion's APPROX_DISTINCT function which provides an approximate count
347 /// using HyperLogLog algorithm. This is much faster than exact COUNT(DISTINCT)
348 /// for large datasets while maintaining accuracy within 2-3% error margin.
349 ///
350 /// # Arguments
351 ///
352 /// * `column` - The column to count distinct values in
353 /// * `assertion` - The assertion to evaluate against the approximate distinct count
354 ///
355 /// # Examples
356 ///
357 /// ```rust
358 /// use term_guard::core::{Check, Level};
359 /// use term_guard::constraints::Assertion;
360 ///
361 /// let check = Check::builder("cardinality_validation")
362 /// .level(Level::Warning)
363 /// // High cardinality check (e.g., user IDs)
364 /// .has_approx_count_distinct("user_id", Assertion::GreaterThan(1000000.0))
365 /// // Low cardinality check (e.g., country codes)
366 /// .has_approx_count_distinct("country_code", Assertion::LessThan(200.0))
367 /// .build();
368 /// ```
369 pub fn has_approx_count_distinct(
370 mut self,
371 column: impl Into<String>,
372 assertion: Assertion,
373 ) -> Self {
374 self.constraints
375 .push(Arc::new(ApproxCountDistinctConstraint::new(
376 column, assertion,
377 )));
378 self
379 }
380
381 /// Adds a constraint that checks an approximate quantile of a column.
382 ///
383 /// # Arguments
384 ///
385 /// * `column` - The column to check
386 /// * `quantile` - The quantile to compute (0.0 to 1.0)
387 /// * `assertion` - The assertion to evaluate against the quantile value
388 ///
389 /// # Examples
390 ///
391 /// ```rust
392 /// use term_guard::core::{Check, Level};
393 /// use term_guard::constraints::Assertion;
394 ///
395 /// let check = Check::builder("quantile_validation")
396 /// .level(Level::Warning)
397 /// .has_approx_quantile("response_time", 0.95, Assertion::LessThan(1000.0))
398 /// .build();
399 /// ```
400 ///
401 /// # Panics
402 ///
403 /// Panics if quantile is not between 0.0 and 1.0
404 pub fn has_approx_quantile(
405 mut self,
406 column: impl Into<String>,
407 quantile: f64,
408 assertion: Assertion,
409 ) -> Self {
410 self.constraints.push(Arc::new(
411 QuantileConstraint::percentile(column, quantile, assertion)
412 .expect("Invalid quantile parameters"),
413 ));
414 self
415 }
416
417 /// Adds a constraint that checks the mutual information between two columns.
418 ///
419 /// # Arguments
420 ///
421 /// * `column1` - The first column
422 /// * `column2` - The second column
423 /// * `assertion` - The assertion to evaluate against the mutual information
424 ///
425 /// # Examples
426 ///
427 /// ```rust
428 /// use term_guard::core::{Check, Level};
429 /// use term_guard::constraints::Assertion;
430 ///
431 /// let check = Check::builder("mi_validation")
432 /// .level(Level::Info)
433 /// .has_mutual_information("feature1", "feature2", Assertion::GreaterThan(0.5))
434 /// .build();
435 /// ```
436 pub fn has_mutual_information(
437 mut self,
438 column1: impl Into<String>,
439 column2: impl Into<String>,
440 assertion: Assertion,
441 ) -> Self {
442 self.constraints.push(Arc::new(
443 CorrelationConstraint::mutual_information(column1, column2, 10, assertion)
444 .expect("Invalid mutual information parameters"),
445 ));
446 self
447 }
448
449 /// Adds a constraint that checks the correlation between two columns.
450 ///
451 /// # Arguments
452 ///
453 /// * `column1` - The first column
454 /// * `column2` - The second column
455 /// * `assertion` - The assertion to evaluate against the correlation
456 ///
457 /// # Examples
458 ///
459 /// ```rust
460 /// use term_guard::core::{Check, Level};
461 /// use term_guard::constraints::Assertion;
462 ///
463 /// let check = Check::builder("correlation_validation")
464 /// .level(Level::Warning)
465 /// .has_correlation("height", "weight", Assertion::GreaterThan(0.7))
466 /// .build();
467 /// ```
468 pub fn has_correlation(
469 mut self,
470 column1: impl Into<String>,
471 column2: impl Into<String>,
472 assertion: Assertion,
473 ) -> Self {
474 self.constraints.push(Arc::new(
475 CorrelationConstraint::pearson(column1, column2, assertion)
476 .expect("Invalid correlation parameters"),
477 ));
478 self
479 }
480
481 /// Adds a constraint that checks minimum string length.
482 ///
483 /// **Note**: Consider using the new `length()` method for more flexibility.
484 ///
485 /// # Arguments
486 ///
487 /// * `column` - The column to check
488 /// * `min_length` - The minimum acceptable string length
489 ///
490 /// # Examples
491 ///
492 /// ```rust
493 /// use term_guard::core::{Check, Level};
494 /// use term_guard::constraints::LengthAssertion;
495 ///
496 /// // Using the convenience method:
497 /// let check = Check::builder("password_validation")
498 /// .level(Level::Error)
499 /// .has_min_length("password", 8)
500 /// .build();
501 ///
502 /// // Or using the unified length API:
503 /// let check = Check::builder("password_validation")
504 /// .level(Level::Error)
505 /// .length("password", LengthAssertion::Min(8))
506 /// .build();
507 /// ```
508 pub fn has_min_length(mut self, column: impl Into<String>, min_length: usize) -> Self {
509 use crate::constraints::LengthConstraint;
510 self.constraints
511 .push(Arc::new(LengthConstraint::min(column, min_length)));
512 self
513 }
514
515 /// Adds a constraint that checks maximum string length.
516 ///
517 /// # Arguments
518 ///
519 /// * `column` - The column to check
520 /// * `max_length` - The maximum acceptable string length
521 ///
522 /// # Examples
523 ///
524 /// ```rust
525 /// use term_guard::core::{Check, Level};
526 ///
527 /// let check = Check::builder("username_validation")
528 /// .level(Level::Error)
529 /// .has_max_length("username", 20)
530 /// .build();
531 /// ```
532 pub fn has_max_length(mut self, column: impl Into<String>, max_length: usize) -> Self {
533 use crate::constraints::LengthConstraint;
534 self.constraints
535 .push(Arc::new(LengthConstraint::max(column, max_length)));
536 self
537 }
538
539 /// Adds a constraint that checks string length is between bounds (inclusive).
540 ///
541 /// # Arguments
542 ///
543 /// * `column` - The column to check
544 /// * `min_length` - The minimum acceptable string length
545 /// * `max_length` - The maximum acceptable string length
546 ///
547 /// # Examples
548 ///
549 /// ```rust
550 /// use term_guard::core::{Check, Level};
551 ///
552 /// let check = Check::builder("description_validation")
553 /// .level(Level::Warning)
554 /// .has_length_between("description", 10, 500)
555 /// .build();
556 /// ```
557 pub fn has_length_between(
558 mut self,
559 column: impl Into<String>,
560 min_length: usize,
561 max_length: usize,
562 ) -> Self {
563 use crate::constraints::LengthConstraint;
564 self.constraints.push(Arc::new(LengthConstraint::between(
565 column, min_length, max_length,
566 )));
567 self
568 }
569
570 /// Adds a constraint that checks string has exact length.
571 ///
572 /// # Arguments
573 ///
574 /// * `column` - The column to check
575 /// * `length` - The required exact string length
576 ///
577 /// # Examples
578 ///
579 /// ```rust
580 /// use term_guard::core::{Check, Level};
581 ///
582 /// let check = Check::builder("code_validation")
583 /// .level(Level::Error)
584 /// .has_exact_length("verification_code", 6)
585 /// .build();
586 /// ```
587 pub fn has_exact_length(mut self, column: impl Into<String>, length: usize) -> Self {
588 use crate::constraints::LengthConstraint;
589 self.constraints
590 .push(Arc::new(LengthConstraint::exactly(column, length)));
591 self
592 }
593
594 /// Adds a constraint that checks strings are not empty.
595 ///
596 /// # Arguments
597 ///
598 /// * `column` - The column to check
599 ///
600 /// # Examples
601 ///
602 /// ```rust
603 /// use term_guard::core::{Check, Level};
604 ///
605 /// let check = Check::builder("name_validation")
606 /// .level(Level::Error)
607 /// .is_not_empty("name")
608 /// .build();
609 /// ```
610 pub fn is_not_empty(mut self, column: impl Into<String>) -> Self {
611 use crate::constraints::LengthConstraint;
612 self.constraints
613 .push(Arc::new(LengthConstraint::not_empty(column)));
614 self
615 }
616
617 /// Adds a constraint that checks data type consistency.
618 ///
619 /// This analyzes the actual data types present in a column and reports on consistency,
620 /// helping identify columns with mixed types.
621 ///
622 /// # Arguments
623 ///
624 /// * `column` - The column to check
625 /// * `threshold` - The minimum ratio of values that must have the most common type (0.0 to 1.0)
626 ///
627 /// # Examples
628 ///
629 /// ```rust
630 /// use term_guard::core::{Check, Level};
631 ///
632 /// let check = Check::builder("consistency_validation")
633 /// .level(Level::Warning)
634 /// .has_consistent_data_type("user_id", 0.95)
635 /// .build();
636 /// ```
637 ///
638 /// # Panics
639 ///
640 /// Panics if threshold is not between 0.0 and 1.0
641 pub fn has_consistent_data_type(mut self, column: impl Into<String>, threshold: f64) -> Self {
642 self.constraints.push(Arc::new(
643 DataTypeConstraint::type_consistency(column, threshold)
644 .expect("Invalid data type consistency parameters"),
645 ));
646 self
647 }
648
649 /// Adds a constraint that evaluates a custom SQL expression.
650 ///
651 /// This allows users to define custom validation logic using SQL expressions.
652 /// The expression should evaluate to a boolean value for each row.
653 /// For safety, the expression cannot contain data-modifying operations.
654 ///
655 /// # Arguments
656 ///
657 /// * `sql_expression` - The SQL expression to evaluate (must return boolean)
658 /// * `hint` - Optional hint message to provide context when the constraint fails
659 ///
660 /// # Examples
661 ///
662 /// ```rust
663 /// use term_guard::core::{Check, Level};
664 ///
665 /// let check = Check::builder("business_rules")
666 /// .level(Level::Error)
667 /// .satisfies("price > 0 AND price < 1000000", Some("Price must be positive and reasonable"))
668 /// .satisfies("order_date <= ship_date", Some("Orders cannot ship before being placed"))
669 /// .build();
670 /// ```
671 ///
672 /// # Panics
673 ///
674 /// Panics if the SQL expression contains dangerous operations like DROP, DELETE, UPDATE, etc.
675 pub fn satisfies(
676 mut self,
677 sql_expression: impl Into<String>,
678 hint: Option<impl Into<String>>,
679 ) -> Self {
680 self.constraints.push(Arc::new(
681 CustomSqlConstraint::new(sql_expression, hint).expect("Invalid SQL expression"),
682 ));
683 self
684 }
685
686 /// Adds a constraint that analyzes value distribution and applies custom assertions.
687 ///
688 /// This constraint computes a histogram of value frequencies in the specified column
689 /// and allows custom assertion functions to validate distribution characteristics.
690 ///
691 /// # Arguments
692 ///
693 /// * `column` - The column to analyze
694 /// * `assertion` - The assertion function to apply to the histogram
695 ///
696 /// # Examples
697 ///
698 /// ```rust
699 /// use term_guard::core::{Check, Level};
700 /// use term_guard::constraints::Histogram;
701 /// use std::sync::Arc;
702 ///
703 /// let check = Check::builder("distribution_validation")
704 /// .level(Level::Warning)
705 /// // No single value should dominate
706 /// .has_histogram("status", Arc::new(|hist: &Histogram| {
707 /// hist.most_common_ratio() < 0.5
708 /// }))
709 /// // Check expected number of categories
710 /// .has_histogram("category", Arc::new(|hist| {
711 /// hist.bucket_count() >= 5 && hist.bucket_count() <= 10
712 /// }))
713 /// .build();
714 /// ```
715 pub fn has_histogram(
716 mut self,
717 column: impl Into<String>,
718 assertion: HistogramAssertion,
719 ) -> Self {
720 self.constraints
721 .push(Arc::new(HistogramConstraint::new(column, assertion)));
722 self
723 }
724
725 /// Adds a constraint that analyzes value distribution with a custom description.
726 ///
727 /// This is similar to `has_histogram` but allows providing a description of what
728 /// the assertion checks, which is useful for error messages.
729 ///
730 /// # Arguments
731 ///
732 /// * `column` - The column to analyze
733 /// * `assertion` - The assertion function to apply to the histogram
734 /// * `description` - A description of what the assertion checks
735 ///
736 /// # Examples
737 ///
738 /// ```rust
739 /// use term_guard::core::{Check, Level};
740 /// use term_guard::constraints::Histogram;
741 /// use std::sync::Arc;
742 ///
743 /// let check = Check::builder("distribution_validation")
744 /// .level(Level::Error)
745 /// .has_histogram_with_description(
746 /// "age",
747 /// Arc::new(|hist: &Histogram| hist.is_roughly_uniform(2.0)),
748 /// "age distribution is roughly uniform"
749 /// )
750 /// .build();
751 /// ```
752 pub fn has_histogram_with_description(
753 mut self,
754 column: impl Into<String>,
755 assertion: HistogramAssertion,
756 description: impl Into<String>,
757 ) -> Self {
758 self.constraints
759 .push(Arc::new(HistogramConstraint::new_with_description(
760 column,
761 assertion,
762 description,
763 )));
764 self
765 }
766
767 // ========================================================================
768 // NEW UNIFIED FORMAT VALIDATION METHODS
769 // ========================================================================
770
771 /// Adds a general format validation constraint with full configuration options.
772 ///
773 /// This is the most flexible format validation method, supporting all format types
774 /// and configuration options through the unified FormatConstraint API.
775 ///
776 /// # Arguments
777 ///
778 /// * `column` - The column to validate
779 /// * `format` - The format type to validate against
780 /// * `threshold` - The minimum ratio of values that must match (0.0 to 1.0)
781 /// * `options` - Configuration options for the validation
782 ///
783 /// # Examples
784 ///
785 /// ```rust
786 /// use term_guard::core::{Check, Level};
787 /// use term_guard::constraints::{FormatType, FormatOptions};
788 ///
789 /// let check = Check::builder("format_validation")
790 /// .level(Level::Error)
791 /// // Custom regex with case-insensitive matching
792 /// .has_format(
793 /// "phone",
794 /// FormatType::Regex(r"^\+?\d{3}-\d{3}-\d{4}$".to_string()),
795 /// 0.95,
796 /// FormatOptions::default().case_sensitive(false).trim_before_check(true)
797 /// )
798 /// // Email validation with custom options
799 /// .has_format(
800 /// "email",
801 /// FormatType::Email,
802 /// 0.99,
803 /// FormatOptions::default().null_is_valid(true)
804 /// )
805 /// // UUID validation
806 /// .has_format(
807 /// "user_id",
808 /// FormatType::UUID,
809 /// 1.0,
810 /// FormatOptions::default()
811 /// )
812 /// .build();
813 /// ```
814 ///
815 /// # Errors
816 ///
817 /// Returns error if column name is invalid, threshold is out of range,
818 /// or regex pattern is invalid.
819 pub fn has_format(
820 mut self,
821 column: impl Into<String>,
822 format: FormatType,
823 threshold: f64,
824 options: FormatOptions,
825 ) -> Self {
826 self.constraints.push(Arc::new(
827 FormatConstraint::new(column, format, threshold, options)
828 .expect("Invalid column, format, threshold, or options"),
829 ));
830 self
831 }
832
833 /// Adds a regex pattern validation constraint.
834 ///
835 /// This is a convenience method for `has_format()` with `FormatType::Regex`.
836 ///
837 /// # Arguments
838 ///
839 /// * `column` - The column to validate
840 /// * `pattern` - The regular expression pattern
841 /// * `threshold` - The minimum ratio of values that must match (0.0 to 1.0)
842 ///
843 /// # Examples
844 ///
845 /// ```rust
846 /// use term_guard::core::{Check, Level};
847 ///
848 /// let check = Check::builder("regex_validation")
849 /// .level(Level::Error)
850 /// .validates_regex("phone", r"^\+?\d{3}-\d{3}-\d{4}$", 0.95)
851 /// .validates_regex("product_code", r"^[A-Z]{2}\d{6}$", 1.0)
852 /// .build();
853 /// ```
854 ///
855 /// # Errors
856 ///
857 /// Returns error if column name is invalid, threshold is out of range,
858 /// or regex pattern is invalid.
859 pub fn validates_regex(
860 mut self,
861 column: impl Into<String>,
862 pattern: impl Into<String>,
863 threshold: f64,
864 ) -> Self {
865 self.constraints.push(Arc::new(
866 FormatConstraint::regex(column, pattern, threshold)
867 .expect("Invalid column, pattern, or threshold"),
868 ));
869 self
870 }
871
872 /// Adds an email address validation constraint.
873 ///
874 /// This is a convenience method for `has_format()` with `FormatType::Email`.
875 ///
876 /// # Arguments
877 ///
878 /// * `column` - The column to validate
879 /// * `threshold` - The minimum ratio of values that must be valid emails (0.0 to 1.0)
880 ///
881 /// # Examples
882 ///
883 /// ```rust
884 /// use term_guard::core::{Check, Level};
885 ///
886 /// let check = Check::builder("email_validation")
887 /// .level(Level::Error)
888 /// .validates_email("primary_email", 0.99)
889 /// .validates_email("secondary_email", 0.80)
890 /// .build();
891 /// ```
892 ///
893 /// # Errors
894 ///
895 /// Returns error if column name is invalid or threshold is out of range.
896 pub fn validates_email(mut self, column: impl Into<String>, threshold: f64) -> Self {
897 self.constraints.push(Arc::new(
898 FormatConstraint::email(column, threshold).expect("Invalid column or threshold"),
899 ));
900 self
901 }
902
903 /// Adds a URL validation constraint.
904 ///
905 /// This is a convenience method for `has_format()` with `FormatType::Url`.
906 ///
907 /// # Arguments
908 ///
909 /// * `column` - The column to validate
910 /// * `threshold` - The minimum ratio of values that must be valid URLs (0.0 to 1.0)
911 /// * `allow_localhost` - Whether to allow localhost URLs
912 ///
913 /// # Examples
914 ///
915 /// ```rust
916 /// use term_guard::core::{Check, Level};
917 ///
918 /// let check = Check::builder("url_validation")
919 /// .level(Level::Error)
920 /// .validates_url("website", 0.90, false)
921 /// .validates_url("dev_endpoint", 0.80, true)
922 /// .build();
923 /// ```
924 ///
925 /// # Errors
926 ///
927 /// Returns error if column name is invalid or threshold is out of range.
928 pub fn validates_url(
929 mut self,
930 column: impl Into<String>,
931 threshold: f64,
932 allow_localhost: bool,
933 ) -> Self {
934 self.constraints.push(Arc::new(
935 FormatConstraint::url(column, threshold, allow_localhost)
936 .expect("Invalid column or threshold"),
937 ));
938 self
939 }
940
941 /// Adds a credit card number detection constraint.
942 ///
943 /// This is a convenience method for `has_format()` with `FormatType::CreditCard`.
944 /// Note: For PII detection, you typically want a low threshold (e.g., 0.01 or 0.05)
945 /// to catch any potential credit card numbers.
946 ///
947 /// # Arguments
948 ///
949 /// * `column` - The column to validate
950 /// * `threshold` - The maximum ratio of values that can be credit card numbers (0.0 to 1.0)
951 /// * `detect_only` - If true, optimizes for detection; if false, for validation
952 ///
953 /// # Examples
954 ///
955 /// ```rust
956 /// use term_guard::core::{Check, Level};
957 ///
958 /// let check = Check::builder("pii_detection")
959 /// .level(Level::Error)
960 /// // PII detection - should find very few or no credit cards
961 /// .validates_credit_card("comments", 0.01, true)
962 /// .validates_credit_card("description", 0.0, true)
963 /// // Credit card validation - most values should be valid credit cards
964 /// .validates_credit_card("payment_info", 0.95, false)
965 /// .build();
966 /// ```
967 ///
968 /// # Errors
969 ///
970 /// Returns error if column name is invalid or threshold is out of range.
971 pub fn validates_credit_card(
972 mut self,
973 column: impl Into<String>,
974 threshold: f64,
975 detect_only: bool,
976 ) -> Self {
977 self.constraints.push(Arc::new(
978 FormatConstraint::credit_card(column, threshold, detect_only)
979 .expect("Invalid column or threshold"),
980 ));
981 self
982 }
983
984 /// Adds a phone number validation constraint.
985 ///
986 /// This is a convenience method for `has_format()` with `FormatType::Phone`.
987 ///
988 /// # Arguments
989 ///
990 /// * `column` - The column to validate
991 /// * `threshold` - The minimum ratio of values that must be valid phone numbers (0.0 to 1.0)
992 /// * `country` - Optional country code for country-specific validation (e.g., "US", "CA")
993 ///
994 /// # Examples
995 ///
996 /// ```rust
997 /// use term_guard::core::{Check, Level};
998 ///
999 /// let check = Check::builder("phone_validation")
1000 /// .level(Level::Error)
1001 /// .validates_phone("phone", 0.95, Some("US"))
1002 /// .validates_phone("international_phone", 0.90, None)
1003 /// .build();
1004 /// ```
1005 ///
1006 /// # Errors
1007 ///
1008 /// Returns error if column name is invalid or threshold is out of range.
1009 pub fn validates_phone(
1010 mut self,
1011 column: impl Into<String>,
1012 threshold: f64,
1013 country: Option<&str>,
1014 ) -> Self {
1015 self.constraints.push(Arc::new(
1016 FormatConstraint::phone(column, threshold, country.map(|s| s.to_string()))
1017 .expect("Invalid column or threshold"),
1018 ));
1019 self
1020 }
1021
1022 /// Adds a postal code validation constraint.
1023 ///
1024 /// This is a convenience method for `has_format()` with `FormatType::PostalCode`.
1025 ///
1026 /// # Arguments
1027 ///
1028 /// * `column` - The column to validate
1029 /// * `threshold` - The minimum ratio of values that must be valid postal codes (0.0 to 1.0)
1030 /// * `country` - Country code for country-specific validation (e.g., "US", "CA", "UK")
1031 ///
1032 /// # Examples
1033 ///
1034 /// ```rust
1035 /// use term_guard::core::{Check, Level};
1036 ///
1037 /// let check = Check::builder("postal_code_validation")
1038 /// .level(Level::Error)
1039 /// .validates_postal_code("zip_code", 0.98, "US")
1040 /// .validates_postal_code("postal_code", 0.95, "CA")
1041 /// .build();
1042 /// ```
1043 ///
1044 /// # Errors
1045 ///
1046 /// Returns error if column name is invalid or threshold is out of range.
1047 pub fn validates_postal_code(
1048 mut self,
1049 column: impl Into<String>,
1050 threshold: f64,
1051 country: &str,
1052 ) -> Self {
1053 self.constraints.push(Arc::new(
1054 FormatConstraint::postal_code(column, threshold, country)
1055 .expect("Invalid column or threshold"),
1056 ));
1057 self
1058 }
1059
1060 /// Adds a UUID validation constraint.
1061 ///
1062 /// This is a convenience method for `has_format()` with `FormatType::UUID`.
1063 ///
1064 /// # Arguments
1065 ///
1066 /// * `column` - The column to validate
1067 /// * `threshold` - The minimum ratio of values that must be valid UUIDs (0.0 to 1.0)
1068 ///
1069 /// # Examples
1070 ///
1071 /// ```rust
1072 /// use term_guard::core::{Check, Level};
1073 ///
1074 /// let check = Check::builder("uuid_validation")
1075 /// .level(Level::Error)
1076 /// .validates_uuid("user_id", 1.0)
1077 /// .validates_uuid("session_id", 0.99)
1078 /// .build();
1079 /// ```
1080 ///
1081 /// # Errors
1082 ///
1083 /// Returns error if column name is invalid or threshold is out of range.
1084 pub fn validates_uuid(mut self, column: impl Into<String>, threshold: f64) -> Self {
1085 self.constraints.push(Arc::new(
1086 FormatConstraint::uuid(column, threshold).expect("Invalid column or threshold"),
1087 ));
1088 self
1089 }
1090
1091 /// Adds an IPv4 address validation constraint.
1092 ///
1093 /// This is a convenience method for `has_format()` with `FormatType::IPv4`.
1094 ///
1095 /// # Arguments
1096 ///
1097 /// * `column` - The column to validate
1098 /// * `threshold` - The minimum ratio of values that must be valid IPv4 addresses (0.0 to 1.0)
1099 ///
1100 /// # Examples
1101 ///
1102 /// ```rust
1103 /// use term_guard::core::{Check, Level};
1104 ///
1105 /// let check = Check::builder("ip_validation")
1106 /// .level(Level::Error)
1107 /// .validates_ipv4("client_ip", 0.98)
1108 /// .validates_ipv4("server_ip", 1.0)
1109 /// .build();
1110 /// ```
1111 ///
1112 /// # Errors
1113 ///
1114 /// Returns error if column name is invalid or threshold is out of range.
1115 pub fn validates_ipv4(mut self, column: impl Into<String>, threshold: f64) -> Self {
1116 self.constraints.push(Arc::new(
1117 FormatConstraint::ipv4(column, threshold).expect("Invalid column or threshold"),
1118 ));
1119 self
1120 }
1121
1122 /// Adds an IPv6 address validation constraint.
1123 ///
1124 /// This is a convenience method for `has_format()` with `FormatType::IPv6`.
1125 ///
1126 /// # Arguments
1127 ///
1128 /// * `column` - The column to validate
1129 /// * `threshold` - The minimum ratio of values that must be valid IPv6 addresses (0.0 to 1.0)
1130 ///
1131 /// # Examples
1132 ///
1133 /// ```rust
1134 /// use term_guard::core::{Check, Level};
1135 ///
1136 /// let check = Check::builder("ipv6_validation")
1137 /// .level(Level::Error)
1138 /// .validates_ipv6("client_ipv6", 0.95)
1139 /// .build();
1140 /// ```
1141 ///
1142 /// # Errors
1143 ///
1144 /// Returns error if column name is invalid or threshold is out of range.
1145 pub fn validates_ipv6(mut self, column: impl Into<String>, threshold: f64) -> Self {
1146 self.constraints.push(Arc::new(
1147 FormatConstraint::ipv6(column, threshold).expect("Invalid column or threshold"),
1148 ));
1149 self
1150 }
1151
1152 /// Adds a JSON format validation constraint.
1153 ///
1154 /// This is a convenience method for `has_format()` with `FormatType::Json`.
1155 ///
1156 /// # Arguments
1157 ///
1158 /// * `column` - The column to validate
1159 /// * `threshold` - The minimum ratio of values that must be valid JSON (0.0 to 1.0)
1160 ///
1161 /// # Examples
1162 ///
1163 /// ```rust
1164 /// use term_guard::core::{Check, Level};
1165 ///
1166 /// let check = Check::builder("json_validation")
1167 /// .level(Level::Error)
1168 /// .validates_json("metadata", 0.99)
1169 /// .validates_json("config", 1.0)
1170 /// .build();
1171 /// ```
1172 ///
1173 /// # Errors
1174 ///
1175 /// Returns error if column name is invalid or threshold is out of range.
1176 pub fn validates_json(mut self, column: impl Into<String>, threshold: f64) -> Self {
1177 self.constraints.push(Arc::new(
1178 FormatConstraint::json(column, threshold).expect("Invalid column or threshold"),
1179 ));
1180 self
1181 }
1182
1183 /// Adds an ISO 8601 datetime validation constraint.
1184 ///
1185 /// This is a convenience method for `has_format()` with `FormatType::Iso8601DateTime`.
1186 ///
1187 /// # Arguments
1188 ///
1189 /// * `column` - The column to validate
1190 /// * `threshold` - The minimum ratio of values that must be valid ISO 8601 datetimes (0.0 to 1.0)
1191 ///
1192 /// # Examples
1193 ///
1194 /// ```rust
1195 /// use term_guard::core::{Check, Level};
1196 ///
1197 /// let check = Check::builder("datetime_validation")
1198 /// .level(Level::Error)
1199 /// .validates_iso8601_datetime("order_date", 1.0)
1200 /// .validates_iso8601_datetime("modified_date", 0.98)
1201 /// .build();
1202 /// ```
1203 ///
1204 /// # Errors
1205 ///
1206 /// Returns error if column name is invalid or threshold is out of range.
1207 pub fn validates_iso8601_datetime(mut self, column: impl Into<String>, threshold: f64) -> Self {
1208 self.constraints.push(Arc::new(
1209 FormatConstraint::iso8601_datetime(column, threshold)
1210 .expect("Invalid column or threshold"),
1211 ));
1212 self
1213 }
1214
1215 // ========================================================================
1216 // ENHANCED FORMAT VALIDATION METHODS WITH OPTIONS
1217 // ========================================================================
1218
1219 /// Adds an enhanced email validation constraint with configurable options.
1220 ///
1221 /// This method provides more control than `validates_email()` by supporting
1222 /// case sensitivity, whitespace trimming, and null handling options.
1223 ///
1224 /// # Arguments
1225 ///
1226 /// * `column` - The column to validate
1227 /// * `threshold` - The minimum ratio of values that must be valid emails (0.0 to 1.0)
1228 /// * `options` - Format validation options
1229 ///
1230 /// # Examples
1231 ///
1232 /// ```rust
1233 /// use term_guard::core::{Check, Level};
1234 /// use term_guard::constraints::FormatOptions;
1235 ///
1236 /// let check = Check::builder("enhanced_email_validation")
1237 /// .level(Level::Error)
1238 /// // Case-insensitive email validation with trimming
1239 /// .validates_email_with_options(
1240 /// "email",
1241 /// 0.95,
1242 /// FormatOptions::new()
1243 /// .case_sensitive(false)
1244 /// .trim_before_check(true)
1245 /// .null_is_valid(false)
1246 /// )
1247 /// .build();
1248 /// ```
1249 pub fn validates_email_with_options(
1250 mut self,
1251 column: impl Into<String>,
1252 threshold: f64,
1253 options: FormatOptions,
1254 ) -> Self {
1255 self.constraints.push(Arc::new(
1256 FormatConstraint::new(column, FormatType::Email, threshold, options)
1257 .expect("Invalid column, threshold, or options"),
1258 ));
1259 self
1260 }
1261
1262 /// Adds an enhanced URL validation constraint with configurable options.
1263 ///
1264 /// This method provides more control than `validates_url()` by supporting
1265 /// case sensitivity, whitespace trimming, and null handling options.
1266 ///
1267 /// # Arguments
1268 ///
1269 /// * `column` - The column to validate
1270 /// * `threshold` - The minimum ratio of values that must be valid URLs (0.0 to 1.0)
1271 /// * `allow_localhost` - Whether to allow localhost URLs
1272 /// * `options` - Format validation options
1273 ///
1274 /// # Examples
1275 ///
1276 /// ```rust
1277 /// use term_guard::core::{Check, Level};
1278 /// use term_guard::constraints::FormatOptions;
1279 ///
1280 /// let check = Check::builder("enhanced_url_validation")
1281 /// .level(Level::Error)
1282 /// // Case-insensitive URL validation with trimming
1283 /// .validates_url_with_options(
1284 /// "website",
1285 /// 0.90,
1286 /// true, // allow localhost
1287 /// FormatOptions::new()
1288 /// .case_sensitive(false)
1289 /// .trim_before_check(true)
1290 /// )
1291 /// .build();
1292 /// ```
1293 pub fn validates_url_with_options(
1294 mut self,
1295 column: impl Into<String>,
1296 threshold: f64,
1297 allow_localhost: bool,
1298 options: FormatOptions,
1299 ) -> Self {
1300 self.constraints.push(Arc::new(
1301 FormatConstraint::new(
1302 column,
1303 FormatType::Url { allow_localhost },
1304 threshold,
1305 options,
1306 )
1307 .expect("Invalid column, threshold, or options"),
1308 ));
1309 self
1310 }
1311
1312 /// Adds an enhanced phone number validation constraint with configurable options.
1313 ///
1314 /// This method provides more control than `validates_phone()` by supporting
1315 /// case sensitivity, whitespace trimming, and null handling options.
1316 ///
1317 /// # Arguments
1318 ///
1319 /// * `column` - The column to validate
1320 /// * `threshold` - The minimum ratio of values that must be valid phone numbers (0.0 to 1.0)
1321 /// * `country` - Optional country code for region-specific validation
1322 /// * `options` - Format validation options
1323 ///
1324 /// # Examples
1325 ///
1326 /// ```rust
1327 /// use term_guard::core::{Check, Level};
1328 /// use term_guard::constraints::FormatOptions;
1329 ///
1330 /// let check = Check::builder("enhanced_phone_validation")
1331 /// .level(Level::Error)
1332 /// // US phone validation with trimming
1333 /// .validates_phone_with_options(
1334 /// "phone",
1335 /// 0.95,
1336 /// Some("US".to_string()),
1337 /// FormatOptions::new().trim_before_check(true)
1338 /// )
1339 /// .build();
1340 /// ```
1341 pub fn validates_phone_with_options(
1342 mut self,
1343 column: impl Into<String>,
1344 threshold: f64,
1345 country: Option<String>,
1346 options: FormatOptions,
1347 ) -> Self {
1348 self.constraints.push(Arc::new(
1349 FormatConstraint::new(column, FormatType::Phone { country }, threshold, options)
1350 .expect("Invalid column, threshold, or options"),
1351 ));
1352 self
1353 }
1354
1355 /// Adds an enhanced regex pattern validation constraint with configurable options.
1356 ///
1357 /// This method provides more control than `validates_regex()` by supporting
1358 /// case sensitivity, whitespace trimming, and null handling options.
1359 ///
1360 /// # Arguments
1361 ///
1362 /// * `column` - The column to validate
1363 /// * `pattern` - The regular expression pattern
1364 /// * `threshold` - The minimum ratio of values that must match (0.0 to 1.0)
1365 /// * `options` - Format validation options
1366 ///
1367 /// # Examples
1368 ///
1369 /// ```rust
1370 /// use term_guard::core::{Check, Level};
1371 /// use term_guard::constraints::FormatOptions;
1372 ///
1373 /// let check = Check::builder("enhanced_regex_validation")
1374 /// .level(Level::Error)
1375 /// // Case-insensitive product code validation
1376 /// .validates_regex_with_options(
1377 /// "product_code",
1378 /// r"^[A-Z]{2}\d{4}$",
1379 /// 0.98,
1380 /// FormatOptions::new()
1381 /// .case_sensitive(false)
1382 /// .trim_before_check(true)
1383 /// )
1384 /// .build();
1385 /// ```
1386 pub fn validates_regex_with_options(
1387 mut self,
1388 column: impl Into<String>,
1389 pattern: impl Into<String>,
1390 threshold: f64,
1391 options: FormatOptions,
1392 ) -> Self {
1393 self.constraints.push(Arc::new(
1394 FormatConstraint::new(
1395 column,
1396 FormatType::Regex(pattern.into()),
1397 threshold,
1398 options,
1399 )
1400 .expect("Invalid column, pattern, threshold, or options"),
1401 ));
1402 self
1403 }
1404
1405 // ========================================================================
1406 // NEW UNIFIED API METHODS
1407 // ========================================================================
1408
1409 /// Adds a unified uniqueness constraint with full control over validation type and options.
1410 ///
1411 /// This method provides a comprehensive alternative to `is_unique`, `are_unique`, `has_uniqueness`,
1412 /// `is_primary_key`, `has_distinctness`, and `has_unique_value_ratio` by supporting all uniqueness
1413 /// validation types with flexible configuration options.
1414 ///
1415 /// # Arguments
1416 ///
1417 /// * `columns` - The column(s) to check (single string, vec, or array)
1418 /// * `uniqueness_type` - The type of uniqueness validation to perform
1419 /// * `options` - Configuration options for null handling, case sensitivity, etc.
1420 ///
1421 /// # Examples
1422 ///
1423 /// ```rust
1424 /// use term_guard::core::{Check, Level};
1425 /// use term_guard::constraints::{UniquenessType, UniquenessOptions, NullHandling, Assertion};
1426 ///
1427 /// let check = Check::builder("unified_uniqueness")
1428 /// // Full uniqueness with threshold
1429 /// .uniqueness(
1430 /// vec!["user_id"],
1431 /// UniquenessType::FullUniqueness { threshold: 1.0 },
1432 /// UniquenessOptions::default()
1433 /// )
1434 /// // Primary key validation
1435 /// .uniqueness(
1436 /// vec!["order_id", "line_item_id"],
1437 /// UniquenessType::PrimaryKey,
1438 /// UniquenessOptions::default()
1439 /// )
1440 /// // Distinctness check
1441 /// .uniqueness(
1442 /// vec!["category"],
1443 /// UniquenessType::Distinctness(Assertion::LessThan(0.1)),
1444 /// UniquenessOptions::default()
1445 /// )
1446 /// // Unique value ratio
1447 /// .uniqueness(
1448 /// vec!["transaction_id"],
1449 /// UniquenessType::UniqueValueRatio(Assertion::GreaterThan(0.99)),
1450 /// UniquenessOptions::default()
1451 /// )
1452 /// // Composite uniqueness with null handling
1453 /// .uniqueness(
1454 /// vec!["email", "domain"],
1455 /// UniquenessType::UniqueComposite {
1456 /// threshold: 0.95,
1457 /// null_handling: NullHandling::Exclude,
1458 /// case_sensitive: false
1459 /// },
1460 /// UniquenessOptions::new()
1461 /// .with_null_handling(NullHandling::Exclude)
1462 /// .case_sensitive(false)
1463 /// )
1464 /// .build();
1465 /// ```
1466 ///
1467 /// # Errors
1468 ///
1469 /// Returns error if column names are invalid or thresholds are out of range.
1470 pub fn uniqueness<I, S>(
1471 mut self,
1472 columns: I,
1473 uniqueness_type: UniquenessType,
1474 options: UniquenessOptions,
1475 ) -> Self
1476 where
1477 I: IntoIterator<Item = S>,
1478 S: Into<String>,
1479 {
1480 self.constraints.push(Arc::new(
1481 UniquenessConstraint::new(columns, uniqueness_type, options)
1482 .expect("Invalid columns, uniqueness type, or options"),
1483 ));
1484 self
1485 }
1486
1487 /// Adds a full uniqueness constraint for single or multiple columns.
1488 ///
1489 /// This is a convenience method for `uniqueness()` with `UniquenessType::FullUniqueness`.
1490 ///
1491 /// # Arguments
1492 ///
1493 /// * `columns` - The column(s) to check for uniqueness
1494 /// * `threshold` - The minimum acceptable uniqueness ratio (0.0 to 1.0)
1495 ///
1496 /// # Examples
1497 ///
1498 /// ```rust
1499 /// use term_guard::core::{Check, Level};
1500 ///
1501 /// let check = Check::builder("uniqueness_validation")
1502 /// .level(Level::Error)
1503 /// .validates_uniqueness(vec!["user_id"], 1.0)
1504 /// .validates_uniqueness(vec!["email", "domain"], 0.95)
1505 /// .build();
1506 /// ```
1507 ///
1508 /// # Errors
1509 ///
1510 /// Returns error if column names are invalid or threshold is out of range.
1511 pub fn validates_uniqueness<I, S>(mut self, columns: I, threshold: f64) -> Self
1512 where
1513 I: IntoIterator<Item = S>,
1514 S: Into<String>,
1515 {
1516 self.constraints.push(Arc::new(
1517 UniquenessConstraint::new(
1518 columns,
1519 UniquenessType::FullUniqueness { threshold },
1520 UniquenessOptions::default(),
1521 )
1522 .expect("Invalid columns or threshold"),
1523 ));
1524 self
1525 }
1526
1527 /// Adds a distinctness constraint with assertion-based validation.
1528 ///
1529 /// This is a convenience method for `uniqueness()` with `UniquenessType::Distinctness`.
1530 ///
1531 /// # Arguments
1532 ///
1533 /// * `columns` - The column(s) to check for distinctness
1534 /// * `assertion` - The assertion to apply to the distinctness ratio
1535 ///
1536 /// # Examples
1537 ///
1538 /// ```rust
1539 /// use term_guard::core::{Check, Level};
1540 /// use term_guard::constraints::Assertion;
1541 ///
1542 /// let check = Check::builder("distinctness_validation")
1543 /// .level(Level::Warning)
1544 /// .validates_distinctness(vec!["status"], Assertion::LessThan(0.1))
1545 /// .validates_distinctness(vec!["user_id"], Assertion::GreaterThan(0.95))
1546 /// .build();
1547 /// ```
1548 ///
1549 /// # Errors
1550 ///
1551 /// Returns error if column names are invalid.
1552 pub fn validates_distinctness<I, S>(mut self, columns: I, assertion: Assertion) -> Self
1553 where
1554 I: IntoIterator<Item = S>,
1555 S: Into<String>,
1556 {
1557 self.constraints.push(Arc::new(
1558 UniquenessConstraint::new(
1559 columns,
1560 UniquenessType::Distinctness(assertion),
1561 UniquenessOptions::default(),
1562 )
1563 .expect("Invalid columns"),
1564 ));
1565 self
1566 }
1567
1568 /// Adds a unique value ratio constraint with assertion-based validation.
1569 ///
1570 /// This is a convenience method for `uniqueness()` with `UniquenessType::UniqueValueRatio`.
1571 ///
1572 /// # Arguments
1573 ///
1574 /// * `columns` - The column(s) to check for unique value ratio
1575 /// * `assertion` - The assertion to apply to the unique value ratio
1576 ///
1577 /// # Examples
1578 ///
1579 /// ```rust
1580 /// use term_guard::core::{Check, Level};
1581 /// use term_guard::constraints::Assertion;
1582 ///
1583 /// let check = Check::builder("unique_ratio_validation")
1584 /// .level(Level::Warning)
1585 /// .validates_unique_value_ratio(vec!["transaction_id"], Assertion::GreaterThan(0.99))
1586 /// .validates_unique_value_ratio(vec!["category"], Assertion::LessThan(0.01))
1587 /// .build();
1588 /// ```
1589 ///
1590 /// # Errors
1591 ///
1592 /// Returns error if column names are invalid.
1593 pub fn validates_unique_value_ratio<I, S>(mut self, columns: I, assertion: Assertion) -> Self
1594 where
1595 I: IntoIterator<Item = S>,
1596 S: Into<String>,
1597 {
1598 self.constraints.push(Arc::new(
1599 UniquenessConstraint::new(
1600 columns,
1601 UniquenessType::UniqueValueRatio(assertion),
1602 UniquenessOptions::default(),
1603 )
1604 .expect("Invalid columns"),
1605 ));
1606 self
1607 }
1608
1609 /// Adds a primary key constraint (unique + non-null).
1610 ///
1611 /// This is a convenience method for `uniqueness()` with `UniquenessType::PrimaryKey`.
1612 ///
1613 /// # Arguments
1614 ///
1615 /// * `columns` - The column(s) that form the primary key
1616 ///
1617 /// # Examples
1618 ///
1619 /// ```rust
1620 /// use term_guard::core::{Check, Level};
1621 ///
1622 /// let check = Check::builder("primary_key_validation")
1623 /// .level(Level::Error)
1624 /// .validates_primary_key(vec!["user_id"])
1625 /// .validates_primary_key(vec!["order_id", "line_item_id"])
1626 /// .build();
1627 /// ```
1628 ///
1629 /// # Errors
1630 ///
1631 /// Returns error if column names are invalid.
1632 pub fn validates_primary_key<I, S>(mut self, columns: I) -> Self
1633 where
1634 I: IntoIterator<Item = S>,
1635 S: Into<String>,
1636 {
1637 self.constraints.push(Arc::new(
1638 UniquenessConstraint::new(
1639 columns,
1640 UniquenessType::PrimaryKey,
1641 UniquenessOptions::default(),
1642 )
1643 .expect("Invalid columns"),
1644 ));
1645 self
1646 }
1647
1648 /// Adds a uniqueness constraint that allows NULL values with configurable handling.
1649 ///
1650 /// This is a convenience method for `uniqueness()` with `UniquenessType::UniqueWithNulls`.
1651 ///
1652 /// # Arguments
1653 ///
1654 /// * `columns` - The column(s) to check for uniqueness
1655 /// * `threshold` - The minimum acceptable uniqueness ratio (0.0 to 1.0)
1656 /// * `null_handling` - How to handle NULL values in uniqueness calculations
1657 ///
1658 /// # Examples
1659 ///
1660 /// ```rust
1661 /// use term_guard::core::{Check, Level};
1662 /// use term_guard::constraints::NullHandling;
1663 ///
1664 /// let check = Check::builder("null_handling_validation")
1665 /// .level(Level::Warning)
1666 /// .validates_uniqueness_with_nulls(vec!["optional_id"], 0.9, NullHandling::Exclude)
1667 /// .validates_uniqueness_with_nulls(vec!["reference"], 0.8, NullHandling::Include)
1668 /// .build();
1669 /// ```
1670 ///
1671 /// # Errors
1672 ///
1673 /// Returns error if column names are invalid or threshold is out of range.
1674 pub fn validates_uniqueness_with_nulls<I, S>(
1675 mut self,
1676 columns: I,
1677 threshold: f64,
1678 null_handling: NullHandling,
1679 ) -> Self
1680 where
1681 I: IntoIterator<Item = S>,
1682 S: Into<String>,
1683 {
1684 self.constraints.push(Arc::new(
1685 UniquenessConstraint::new(
1686 columns,
1687 UniquenessType::UniqueWithNulls {
1688 threshold,
1689 null_handling,
1690 },
1691 UniquenessOptions::new().with_null_handling(null_handling),
1692 )
1693 .expect("Invalid columns or threshold"),
1694 ));
1695 self
1696 }
1697
1698 /// Adds a completeness constraint using the unified options pattern.
1699 ///
1700 /// This method provides a more flexible alternative to `is_complete`, `has_completeness`,
1701 /// `are_complete`, and `are_any_complete` by supporting arbitrary logical operators
1702 /// and thresholds.
1703 ///
1704 /// # Arguments
1705 ///
1706 /// * `columns` - The column(s) to check (single string, vec, or array)
1707 /// * `options` - Configuration options including threshold and logical operator
1708 ///
1709 /// # Examples
1710 ///
1711 /// ```rust
1712 /// use term_guard::core::{Check, ConstraintOptions, LogicalOperator};
1713 ///
1714 /// let check = Check::builder("unified_completeness")
1715 /// // Single column with threshold
1716 /// .completeness("email", ConstraintOptions::new().with_threshold(0.95))
1717 /// // Multiple columns - all must be complete
1718 /// .completeness(
1719 /// vec!["first_name", "last_name"],
1720 /// ConstraintOptions::new()
1721 /// .with_operator(LogicalOperator::All)
1722 /// .with_threshold(1.0)
1723 /// )
1724 /// // At least 2 of 4 contact methods must be 90% complete
1725 /// .completeness(
1726 /// vec!["email", "phone", "address", "postal_code"],
1727 /// ConstraintOptions::new()
1728 /// .with_operator(LogicalOperator::AtLeast(2))
1729 /// .with_threshold(0.9)
1730 /// )
1731 /// .build();
1732 /// ```
1733 pub fn completeness(
1734 mut self,
1735 columns: impl Into<crate::core::ColumnSpec>,
1736 options: crate::core::ConstraintOptions,
1737 ) -> Self {
1738 use crate::constraints::CompletenessConstraint;
1739 self.constraints
1740 .push(Arc::new(CompletenessConstraint::new(columns, options)));
1741 self
1742 }
1743
1744 /// Adds a string length constraint using the unified options pattern.
1745 ///
1746 /// This method provides a more flexible alternative to the individual length methods
1747 /// by supporting all length assertion types in a single interface.
1748 ///
1749 /// # Arguments
1750 ///
1751 /// * `column` - The column to check
1752 /// * `assertion` - The length assertion (Min, Max, Between, Exactly, NotEmpty)
1753 ///
1754 /// # Examples
1755 ///
1756 /// ```rust
1757 /// use term_guard::core::Check;
1758 /// use term_guard::constraints::LengthAssertion;
1759 ///
1760 /// let check = Check::builder("length_validation")
1761 /// .length("password", LengthAssertion::Min(8))
1762 /// .length("username", LengthAssertion::Between(3, 20))
1763 /// .length("verification_code", LengthAssertion::Exactly(6))
1764 /// .length("name", LengthAssertion::NotEmpty)
1765 /// .build();
1766 /// ```
1767 pub fn length(
1768 mut self,
1769 column: impl Into<String>,
1770 assertion: crate::constraints::LengthAssertion,
1771 ) -> Self {
1772 use crate::constraints::LengthConstraint;
1773 self.constraints
1774 .push(Arc::new(LengthConstraint::new(column, assertion)));
1775 self
1776 }
1777
1778 /// Adds a statistical constraint using the unified options pattern.
1779 ///
1780 /// This method provides a unified interface for all statistical constraints
1781 /// (min, max, mean, sum, standard deviation) with consistent assertion patterns.
1782 ///
1783 /// # Arguments
1784 ///
1785 /// * `column` - The column to analyze
1786 /// * `statistic` - The type of statistic to compute
1787 /// * `assertion` - The assertion to apply to the statistic
1788 ///
1789 /// # Examples
1790 ///
1791 /// ```rust
1792 /// use term_guard::core::Check;
1793 /// use term_guard::constraints::{StatisticType, Assertion};
1794 ///
1795 /// let check = Check::builder("statistical_validation")
1796 /// .statistic("age", StatisticType::Min, Assertion::GreaterThanOrEqual(0.0))
1797 /// .statistic("age", StatisticType::Max, Assertion::LessThanOrEqual(120.0))
1798 /// .statistic("salary", StatisticType::Mean, Assertion::Between(50000.0, 100000.0))
1799 /// .statistic("response_time", StatisticType::StandardDeviation, Assertion::LessThan(100.0))
1800 /// .build();
1801 /// ```
1802 pub fn statistic(
1803 mut self,
1804 column: impl Into<String>,
1805 statistic: crate::constraints::StatisticType,
1806 assertion: Assertion,
1807 ) -> Self {
1808 use crate::constraints::StatisticalConstraint;
1809 self.constraints.push(Arc::new(
1810 StatisticalConstraint::new(column, statistic, assertion)
1811 .expect("Invalid column name or statistic"),
1812 ));
1813 self
1814 }
1815
1816 /// Adds a minimum value constraint for a column.
1817 ///
1818 /// This is a convenience method for `statistic()` with `StatisticType::Min`.
1819 ///
1820 /// # Examples
1821 ///
1822 /// ```rust
1823 /// use term_guard::core::{Check, Level};
1824 /// use term_guard::constraints::Assertion;
1825 ///
1826 /// let check = Check::builder("age_validation")
1827 /// .level(Level::Error)
1828 /// .has_min("age", Assertion::GreaterThanOrEqual(0.0))
1829 /// .build();
1830 /// ```
1831 pub fn has_min(self, column: impl Into<String>, assertion: Assertion) -> Self {
1832 self.statistic(column, crate::constraints::StatisticType::Min, assertion)
1833 }
1834
1835 /// Adds a maximum value constraint for a column.
1836 ///
1837 /// This is a convenience method for `statistic()` with `StatisticType::Max`.
1838 ///
1839 /// # Examples
1840 ///
1841 /// ```rust
1842 /// use term_guard::core::{Check, Level};
1843 /// use term_guard::constraints::Assertion;
1844 ///
1845 /// let check = Check::builder("age_validation")
1846 /// .level(Level::Error)
1847 /// .has_max("age", Assertion::LessThanOrEqual(120.0))
1848 /// .build();
1849 /// ```
1850 pub fn has_max(self, column: impl Into<String>, assertion: Assertion) -> Self {
1851 self.statistic(column, crate::constraints::StatisticType::Max, assertion)
1852 }
1853
1854 /// Adds a mean (average) value constraint for a column.
1855 ///
1856 /// This is a convenience method for `statistic()` with `StatisticType::Mean`.
1857 ///
1858 /// # Examples
1859 ///
1860 /// ```rust
1861 /// use term_guard::core::{Check, Level};
1862 /// use term_guard::constraints::Assertion;
1863 ///
1864 /// let check = Check::builder("salary_validation")
1865 /// .level(Level::Warning)
1866 /// .has_mean("salary", Assertion::Between(50000.0, 100000.0))
1867 /// .build();
1868 /// ```
1869 pub fn has_mean(self, column: impl Into<String>, assertion: Assertion) -> Self {
1870 self.statistic(column, crate::constraints::StatisticType::Mean, assertion)
1871 }
1872
1873 /// Adds a sum constraint for a column.
1874 ///
1875 /// This is a convenience method for `statistic()` with `StatisticType::Sum`.
1876 ///
1877 /// # Examples
1878 ///
1879 /// ```rust
1880 /// use term_guard::core::{Check, Level};
1881 /// use term_guard::constraints::Assertion;
1882 ///
1883 /// let check = Check::builder("revenue_validation")
1884 /// .level(Level::Error)
1885 /// .has_sum("revenue", Assertion::GreaterThan(1000000.0))
1886 /// .build();
1887 /// ```
1888 pub fn has_sum(self, column: impl Into<String>, assertion: Assertion) -> Self {
1889 self.statistic(column, crate::constraints::StatisticType::Sum, assertion)
1890 }
1891
1892 /// Adds a standard deviation constraint for a column.
1893 ///
1894 /// This is a convenience method for `statistic()` with `StatisticType::StandardDeviation`.
1895 ///
1896 /// # Examples
1897 ///
1898 /// ```rust
1899 /// use term_guard::core::{Check, Level};
1900 /// use term_guard::constraints::Assertion;
1901 ///
1902 /// let check = Check::builder("response_time_validation")
1903 /// .level(Level::Warning)
1904 /// .has_standard_deviation("response_time", Assertion::LessThan(100.0))
1905 /// .build();
1906 /// ```
1907 pub fn has_standard_deviation(self, column: impl Into<String>, assertion: Assertion) -> Self {
1908 self.statistic(
1909 column,
1910 crate::constraints::StatisticType::StandardDeviation,
1911 assertion,
1912 )
1913 }
1914
1915 /// Adds a variance constraint for a column.
1916 ///
1917 /// This is a convenience method for `statistic()` with `StatisticType::Variance`.
1918 ///
1919 /// # Examples
1920 ///
1921 /// ```rust
1922 /// use term_guard::core::{Check, Level};
1923 /// use term_guard::constraints::Assertion;
1924 ///
1925 /// let check = Check::builder("score_validation")
1926 /// .level(Level::Warning)
1927 /// .has_variance("score", Assertion::LessThan(250.0))
1928 /// .build();
1929 /// ```
1930 pub fn has_variance(self, column: impl Into<String>, assertion: Assertion) -> Self {
1931 self.statistic(
1932 column,
1933 crate::constraints::StatisticType::Variance,
1934 assertion,
1935 )
1936 }
1937
1938 /// Adds a constraint using a fluent constraint builder.
1939 ///
1940 /// This method provides the most flexible API for building complex constraints
1941 /// with full access to all unified constraint features.
1942 ///
1943 /// # Arguments
1944 ///
1945 /// * `constraint` - A constraint built using the fluent API
1946 ///
1947 /// # Examples
1948 ///
1949 /// ```rust
1950 /// use term_guard::core::{Check, ConstraintOptions, LogicalOperator};
1951 /// use term_guard::constraints::{CompletenessConstraint, LengthConstraint, LengthAssertion};
1952 ///
1953 /// let check = Check::builder("advanced_validation")
1954 /// .with_constraint(
1955 /// CompletenessConstraint::new(
1956 /// vec!["phone", "email"],
1957 /// ConstraintOptions::new()
1958 /// .with_operator(LogicalOperator::Any)
1959 /// .with_threshold(0.99)
1960 /// )
1961 /// )
1962 /// .with_constraint(
1963 /// LengthConstraint::new("description", LengthAssertion::Between(50, 2000))
1964 /// )
1965 /// .build();
1966 /// ```
1967 pub fn with_constraint(mut self, constraint: impl crate::core::Constraint + 'static) -> Self {
1968 self.constraints.push(Arc::new(constraint));
1969 self
1970 }
1971
1972 // ========================================================================
1973 // CONVENIENCE METHODS FOR COMMON PATTERNS
1974 // ========================================================================
1975
1976 /// Convenience method for requiring any of multiple columns to be complete.
1977 ///
1978 /// Equivalent to `completeness(columns, ConstraintOptions::new().with_operator(LogicalOperator::Any).with_threshold(1.0))`
1979 /// but more concise for this common pattern.
1980 ///
1981 /// # Examples
1982 ///
1983 /// ```rust
1984 /// use term_guard::core::Check;
1985 ///
1986 /// let check = Check::builder("contact_validation")
1987 /// .any_complete(vec!["phone", "email", "address"])
1988 /// .build();
1989 /// ```
1990 pub fn any_complete<I, S>(self, columns: I) -> Self
1991 where
1992 I: IntoIterator<Item = S>,
1993 S: Into<String>,
1994 {
1995 use crate::core::{ConstraintOptions, LogicalOperator};
1996 let cols: Vec<String> = columns.into_iter().map(Into::into).collect();
1997 self.completeness(
1998 cols,
1999 ConstraintOptions::new()
2000 .with_operator(LogicalOperator::Any)
2001 .with_threshold(1.0),
2002 )
2003 }
2004
2005 /// Convenience method for requiring at least N columns to meet a threshold.
2006 ///
2007 /// # Examples
2008 ///
2009 /// ```rust
2010 /// use term_guard::core::Check;
2011 ///
2012 /// let check = Check::builder("contact_validation")
2013 /// .at_least_complete(2, vec!["email", "phone", "address", "postal_code"], 0.9)
2014 /// .build();
2015 /// ```
2016 pub fn at_least_complete<I, S>(self, n: usize, columns: I, threshold: f64) -> Self
2017 where
2018 I: IntoIterator<Item = S>,
2019 S: Into<String>,
2020 {
2021 use crate::core::{ConstraintOptions, LogicalOperator};
2022 let cols: Vec<String> = columns.into_iter().map(Into::into).collect();
2023 self.completeness(
2024 cols,
2025 ConstraintOptions::new()
2026 .with_operator(LogicalOperator::AtLeast(n))
2027 .with_threshold(threshold),
2028 )
2029 }
2030
2031 /// Convenience method for exactly N columns meeting a threshold.
2032 ///
2033 /// # Examples
2034 ///
2035 /// ```rust
2036 /// use term_guard::core::Check;
2037 ///
2038 /// let check = Check::builder("balance_validation")
2039 /// .exactly_complete(1, vec!["primary_phone", "secondary_phone"], 1.0)
2040 /// .build();
2041 /// ```
2042 pub fn exactly_complete<I, S>(self, n: usize, columns: I, threshold: f64) -> Self
2043 where
2044 I: IntoIterator<Item = S>,
2045 S: Into<String>,
2046 {
2047 use crate::core::{ConstraintOptions, LogicalOperator};
2048 let cols: Vec<String> = columns.into_iter().map(Into::into).collect();
2049 self.completeness(
2050 cols,
2051 ConstraintOptions::new()
2052 .with_operator(LogicalOperator::Exactly(n))
2053 .with_threshold(threshold),
2054 )
2055 }
2056
2057 /// Builds the `Check` instance.
2058 ///
2059 /// # Returns
2060 ///
2061 /// The constructed `Check`
2062 pub fn build(self) -> Check {
2063 Check {
2064 name: self.name,
2065 level: self.level,
2066 description: self.description,
2067 constraints: self.constraints,
2068 }
2069 }
2070}
2071
2072#[cfg(test)]
2073mod tests {
2074 use super::*;
2075 use crate::prelude::*;
2076 use async_trait::async_trait;
2077 use datafusion::prelude::*;
2078
2079 #[derive(Debug)]
2080 struct DummyConstraint {
2081 name: String,
2082 }
2083
2084 #[async_trait]
2085 impl Constraint for DummyConstraint {
2086 async fn evaluate(&self, _ctx: &SessionContext) -> Result<crate::core::ConstraintResult> {
2087 Ok(crate::core::ConstraintResult::success())
2088 }
2089
2090 fn name(&self) -> &str {
2091 &self.name
2092 }
2093
2094 fn metadata(&self) -> crate::core::ConstraintMetadata {
2095 crate::core::ConstraintMetadata::new()
2096 }
2097 }
2098
2099 #[test]
2100 fn test_check_builder() {
2101 let check = Check::builder("test_check")
2102 .level(Level::Error)
2103 .description("Test check description")
2104 .constraint(DummyConstraint {
2105 name: "constraint1".to_string(),
2106 })
2107 .build();
2108
2109 assert_eq!(check.name(), "test_check");
2110 assert_eq!(check.level(), Level::Error);
2111 assert_eq!(check.description(), Some("Test check description"));
2112 assert_eq!(check.constraints().len(), 1);
2113 }
2114
2115 #[test]
2116 fn test_check_default_level() {
2117 let check = Check::builder("test_check").build();
2118 assert_eq!(check.level(), Level::Warning);
2119 }
2120
2121 #[test]
2122 fn test_check_builder_completeness() {
2123 use crate::core::ConstraintOptions;
2124
2125 let check = Check::builder("completeness_check")
2126 .level(Level::Error)
2127 .completeness("user_id", ConstraintOptions::new().with_threshold(1.0))
2128 .completeness("email", ConstraintOptions::new().with_threshold(0.95))
2129 .build();
2130
2131 assert_eq!(check.name(), "completeness_check");
2132 assert_eq!(check.level(), Level::Error);
2133 assert_eq!(check.constraints().len(), 2);
2134 }
2135
2136 #[test]
2137 fn test_check_builder_uniqueness() {
2138 let check = Check::builder("uniqueness_check")
2139 .validates_uniqueness(vec!["user_id"], 1.0)
2140 .validates_uniqueness(vec!["first_name", "last_name"], 1.0)
2141 .validates_uniqueness(vec!["email"], 0.99)
2142 .build();
2143
2144 assert_eq!(check.constraints().len(), 3);
2145 }
2146
2147 #[test]
2148 fn test_check_builder_method_chaining() {
2149 use crate::core::ConstraintOptions;
2150
2151 let check = Check::builder("comprehensive_check")
2152 .level(Level::Error)
2153 .description("Comprehensive data quality check")
2154 .completeness("id", ConstraintOptions::new().with_threshold(1.0))
2155 .completeness("name", ConstraintOptions::new().with_threshold(0.9))
2156 .validates_uniqueness(vec!["id"], 1.0)
2157 .validates_uniqueness(vec!["email", "phone"], 1.0)
2158 .build();
2159
2160 assert_eq!(check.name(), "comprehensive_check");
2161 assert_eq!(check.level(), Level::Error);
2162 assert_eq!(
2163 check.description(),
2164 Some("Comprehensive data quality check")
2165 );
2166 assert_eq!(check.constraints().len(), 4);
2167 }
2168
2169 #[test]
2170 fn test_check_builder_multiple_completeness() {
2171 use crate::core::{ConstraintOptions, LogicalOperator};
2172
2173 let check = Check::builder("multi_completeness_check")
2174 .completeness(
2175 vec!["user_id", "email", "name"],
2176 ConstraintOptions::new()
2177 .with_operator(LogicalOperator::All)
2178 .with_threshold(1.0),
2179 )
2180 .any_complete(vec!["phone", "mobile", "fax"])
2181 .build();
2182
2183 assert_eq!(check.constraints().len(), 2);
2184 }
2185
2186 #[test]
2187 #[should_panic(expected = "Threshold must be between 0.0 and 1.0")]
2188 fn test_check_builder_invalid_completeness_threshold() {
2189 use crate::core::ConstraintOptions;
2190
2191 Check::builder("test")
2192 .completeness("column", ConstraintOptions::new().with_threshold(1.5))
2193 .build();
2194 }
2195
2196 #[test]
2197 #[should_panic(expected = "Invalid columns or threshold")]
2198 fn test_check_builder_invalid_uniqueness_threshold() {
2199 Check::builder("test")
2200 .validates_uniqueness(vec!["column"], -0.1)
2201 .build();
2202 }
2203
2204 #[test]
2205 fn test_check_builder_string_length() {
2206 let check = Check::builder("string_length_check")
2207 .has_min_length("password", 8)
2208 .has_max_length("username", 20)
2209 .build();
2210
2211 assert_eq!(check.constraints().len(), 2);
2212 }
2213
2214 #[test]
2215 fn test_unified_completeness_api() {
2216 use crate::core::{ConstraintOptions, LogicalOperator};
2217
2218 let check = Check::builder("unified_completeness_test")
2219 // Single column with threshold
2220 .completeness("email", ConstraintOptions::new().with_threshold(0.95))
2221 // Multiple columns with ANY operator
2222 .completeness(
2223 vec!["phone", "email", "address"],
2224 ConstraintOptions::new()
2225 .with_operator(LogicalOperator::Any)
2226 .with_threshold(1.0),
2227 )
2228 // At least 2 columns with threshold
2229 .completeness(
2230 vec!["a", "b", "c", "d"],
2231 ConstraintOptions::new()
2232 .with_operator(LogicalOperator::AtLeast(2))
2233 .with_threshold(0.9),
2234 )
2235 .build();
2236
2237 assert_eq!(check.constraints().len(), 3);
2238 }
2239
2240 #[test]
2241 fn test_unified_length_api() {
2242 use crate::constraints::LengthAssertion;
2243
2244 let check = Check::builder("unified_length_test")
2245 .length("password", LengthAssertion::Min(8))
2246 .length("username", LengthAssertion::Between(3, 20))
2247 .length("code", LengthAssertion::Exactly(6))
2248 .length("name", LengthAssertion::NotEmpty)
2249 .build();
2250
2251 assert_eq!(check.constraints().len(), 4);
2252 }
2253
2254 #[test]
2255 fn test_unified_statistics_api() {
2256 use crate::constraints::{Assertion, StatisticType};
2257
2258 let check = Check::builder("unified_statistics_test")
2259 .statistic(
2260 "age",
2261 StatisticType::Min,
2262 Assertion::GreaterThanOrEqual(0.0),
2263 )
2264 .statistic("age", StatisticType::Max, Assertion::LessThanOrEqual(120.0))
2265 .statistic(
2266 "salary",
2267 StatisticType::Mean,
2268 Assertion::Between(50000.0, 100000.0),
2269 )
2270 .statistic(
2271 "response_time",
2272 StatisticType::StandardDeviation,
2273 Assertion::LessThan(100.0),
2274 )
2275 .build();
2276
2277 assert_eq!(check.constraints().len(), 4);
2278 }
2279
2280 #[test]
2281 fn test_convenience_methods() {
2282 let check = Check::builder("convenience_test")
2283 .any_complete(vec!["phone", "email", "address"])
2284 .at_least_complete(2, vec!["a", "b", "c", "d"], 0.9)
2285 .exactly_complete(1, vec!["primary", "secondary"], 1.0)
2286 .build();
2287
2288 assert_eq!(check.constraints().len(), 3);
2289 }
2290
2291 #[test]
2292 fn test_with_constraint_method() {
2293 use crate::constraints::{LengthAssertion, LengthConstraint};
2294
2295 let constraint = LengthConstraint::new("test", LengthAssertion::Between(5, 50));
2296 let check = Check::builder("with_constraint_test")
2297 .with_constraint(constraint)
2298 .build();
2299
2300 assert_eq!(check.constraints().len(), 1);
2301 }
2302
2303 #[test]
2304 fn test_enhanced_format_validation_methods() {
2305 let check = Check::builder("enhanced_format_test")
2306 // Enhanced email validation with options
2307 .validates_email_with_options(
2308 "email",
2309 0.95,
2310 FormatOptions::new()
2311 .case_sensitive(false)
2312 .trim_before_check(true)
2313 .null_is_valid(false),
2314 )
2315 // Enhanced URL validation with options
2316 .validates_url_with_options(
2317 "website",
2318 0.90,
2319 true, // allow localhost
2320 FormatOptions::new()
2321 .case_sensitive(false)
2322 .trim_before_check(true),
2323 )
2324 // Enhanced phone validation with options
2325 .validates_phone_with_options(
2326 "phone",
2327 0.95,
2328 Some("US".to_string()),
2329 FormatOptions::new().trim_before_check(true),
2330 )
2331 // Enhanced regex validation with options
2332 .validates_regex_with_options(
2333 "product_code",
2334 r"^[A-Z]{2}\d{4}$",
2335 0.98,
2336 FormatOptions::new()
2337 .case_sensitive(false)
2338 .trim_before_check(true),
2339 )
2340 .build();
2341
2342 assert_eq!(check.constraints().len(), 4);
2343 assert_eq!(check.name(), "enhanced_format_test");
2344 }
2345
2346 #[test]
2347 fn test_enhanced_vs_basic_format_methods() {
2348 // Test that basic and enhanced methods can be used together
2349 let check = Check::builder("mixed_format_test")
2350 // Basic methods
2351 .validates_email("basic_email", 0.90)
2352 .validates_url("basic_url", 0.85, false)
2353 .validates_phone("basic_phone", 0.80, None)
2354 .validates_regex("basic_pattern", r"^\d+$", 0.75)
2355 // Enhanced methods
2356 .validates_email_with_options(
2357 "enhanced_email",
2358 0.95,
2359 FormatOptions::new().case_sensitive(false),
2360 )
2361 .validates_url_with_options(
2362 "enhanced_url",
2363 0.90,
2364 true,
2365 FormatOptions::new().trim_before_check(true),
2366 )
2367 .build();
2368
2369 assert_eq!(check.constraints().len(), 6);
2370 }
2371}