term_guard/core/
builder_extensions.rs

1//! Extended builder API for unified constraints.
2//!
3//! This module provides the new unified constraint API for the Check builder,
4//! with improved ergonomics and consistency while maintaining backward compatibility.
5
6use crate::constraints::{
7    Assertion, FormatOptions, FormatType, StatisticType, UniquenessOptions, UniquenessType,
8};
9use crate::core::{CheckBuilder, ConstraintOptions, LogicalOperator};
10use crate::prelude::*;
11
12/// Options for completeness constraints with fluent builder pattern.
13#[derive(Debug, Clone)]
14pub struct CompletenessOptions {
15    threshold: Option<f64>,
16    operator: LogicalOperator,
17    null_is_failure: bool,
18}
19
20impl CompletenessOptions {
21    /// Creates options that require 100% completeness.
22    pub fn full() -> Self {
23        Self {
24            threshold: Some(1.0),
25            operator: LogicalOperator::All,
26            null_is_failure: true,
27        }
28    }
29
30    /// Creates options with a specific threshold.
31    pub fn threshold(threshold: f64) -> Self {
32        Self {
33            threshold: Some(threshold),
34            operator: LogicalOperator::All,
35            null_is_failure: true,
36        }
37    }
38
39    /// Creates options where at least N columns must be complete.
40    pub fn at_least(n: usize) -> Self {
41        Self {
42            threshold: None,
43            operator: LogicalOperator::AtLeast(n),
44            null_is_failure: true,
45        }
46    }
47
48    /// Creates options where any column being complete satisfies the constraint.
49    pub fn any() -> Self {
50        Self {
51            threshold: None,
52            operator: LogicalOperator::Any,
53            null_is_failure: true,
54        }
55    }
56
57    /// Sets the logical operator for multi-column constraints.
58    pub fn with_operator(mut self, operator: LogicalOperator) -> Self {
59        self.operator = operator;
60        self
61    }
62
63    /// Sets whether null values should be considered failures.
64    pub fn null_handling(mut self, null_is_failure: bool) -> Self {
65        self.null_is_failure = null_is_failure;
66        self
67    }
68
69    /// Converts to ConstraintOptions.
70    pub fn into_constraint_options(self) -> ConstraintOptions {
71        let mut options = ConstraintOptions::new()
72            .with_operator(self.operator)
73            .with_flag("null_is_failure", self.null_is_failure);
74
75        if let Some(threshold) = self.threshold {
76            options = options.with_threshold(threshold);
77        }
78
79        options
80    }
81}
82
83/// Options for statistical constraints with fluent builder pattern.
84#[derive(Debug, Clone)]
85pub struct StatisticalOptions {
86    statistics: Vec<(StatisticType, Assertion)>,
87}
88
89impl Default for StatisticalOptions {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl StatisticalOptions {
96    /// Creates a new statistical options builder.
97    pub fn new() -> Self {
98        Self {
99            statistics: Vec::new(),
100        }
101    }
102
103    /// Adds a minimum value constraint.
104    pub fn min(mut self, assertion: Assertion) -> Self {
105        self.statistics.push((StatisticType::Min, assertion));
106        self
107    }
108
109    /// Adds a maximum value constraint.
110    pub fn max(mut self, assertion: Assertion) -> Self {
111        self.statistics.push((StatisticType::Max, assertion));
112        self
113    }
114
115    /// Adds a mean/average constraint.
116    pub fn mean(mut self, assertion: Assertion) -> Self {
117        self.statistics.push((StatisticType::Mean, assertion));
118        self
119    }
120
121    /// Adds a sum constraint.
122    pub fn sum(mut self, assertion: Assertion) -> Self {
123        self.statistics.push((StatisticType::Sum, assertion));
124        self
125    }
126
127    /// Adds a standard deviation constraint.
128    pub fn standard_deviation(mut self, assertion: Assertion) -> Self {
129        self.statistics
130            .push((StatisticType::StandardDeviation, assertion));
131        self
132    }
133
134    /// Adds a variance constraint.
135    pub fn variance(mut self, assertion: Assertion) -> Self {
136        self.statistics.push((StatisticType::Variance, assertion));
137        self
138    }
139
140    /// Adds a median constraint.
141    pub fn median(mut self, assertion: Assertion) -> Self {
142        self.statistics.push((StatisticType::Median, assertion));
143        self
144    }
145
146    /// Adds a percentile constraint.
147    pub fn percentile(mut self, percentile: f64, assertion: Assertion) -> Self {
148        self.statistics
149            .push((StatisticType::Percentile(percentile), assertion));
150        self
151    }
152
153    /// Returns true if multiple statistics are configured.
154    pub fn is_multi(&self) -> bool {
155        self.statistics.len() > 1
156    }
157
158    /// Returns the configured statistics.
159    pub fn into_statistics(self) -> Vec<(StatisticType, Assertion)> {
160        self.statistics
161    }
162}
163
164// Note: ConstraintBuilder was removed as it doesn't integrate well with the
165// existing builder pattern. Use the individual methods on CheckBuilder instead.
166
167/// Extension methods for CheckBuilder providing the new unified API.
168impl CheckBuilder {
169    /// Adds statistical constraints using the new unified API.
170    ///
171    /// # Examples
172    ///
173    /// ```rust
174    /// use term_guard::core::{Check, builder_extensions::StatisticalOptions};
175    /// use term_guard::constraints::Assertion;
176    ///
177    /// # use term_guard::prelude::*;
178    /// # fn example() -> Result<Check> {
179    /// // Single statistic
180    /// let check = Check::builder("age_stats")
181    ///     .statistics(
182    ///         "age",
183    ///         StatisticalOptions::new()
184    ///             .min(Assertion::GreaterThanOrEqual(0.0))
185    ///             .max(Assertion::LessThan(150.0))
186    ///     )?
187    ///     .build();
188    ///
189    /// // Multiple statistics optimized in one query
190    /// let check = Check::builder("response_time_stats")
191    ///     .statistics(
192    ///         "response_time",
193    ///         StatisticalOptions::new()
194    ///             .min(Assertion::GreaterThanOrEqual(0.0))
195    ///             .max(Assertion::LessThan(5000.0))
196    ///             .mean(Assertion::Between(100.0, 1000.0))
197    ///             .percentile(0.95, Assertion::LessThan(2000.0))
198    ///     )?
199    ///     .build();
200    /// # Ok(check)
201    /// # }
202    /// ```
203    pub fn statistics(
204        self,
205        column: impl Into<String>,
206        options: StatisticalOptions,
207    ) -> Result<Self> {
208        let column_str = column.into();
209        let stats = options.into_statistics();
210
211        // Use the existing statistic method
212        let mut result = self;
213        for (stat_type, assertion) in stats {
214            result = result.statistic(column_str.clone(), stat_type, assertion);
215        }
216
217        Ok(result)
218    }
219
220    /// Adds multiple constraints using a fluent builder API.
221    ///
222    /// Note: This is an advanced API. For simple cases, use the individual builder methods.
223    ///
224    /// # Examples
225    ///
226    /// ```rust
227    /// use term_guard::core::Check;
228    /// use term_guard::core::builder_extensions::{CompletenessOptions, StatisticalOptions};
229    /// use term_guard::constraints::{FormatType, FormatOptions, UniquenessType, UniquenessOptions, Assertion};
230    ///
231    /// # use term_guard::prelude::*;
232    /// let check = Check::builder("user_validation")
233    ///     // Use individual methods for constraints
234    ///     .completeness("user_id", CompletenessOptions::full().into_constraint_options())
235    ///     .completeness("email", CompletenessOptions::threshold(0.95).into_constraint_options())
236    ///     .has_format("email", FormatType::Email, 0.95, FormatOptions::default())
237    ///     .uniqueness(
238    ///         vec!["email"],
239    ///         UniquenessType::FullUniqueness { threshold: 1.0 },
240    ///         UniquenessOptions::default()
241    ///     )
242    ///     .build();
243    /// ```
244    pub fn with_constraints<F>(self, build_fn: F) -> Self
245    where
246        F: FnOnce(Self) -> Self,
247    {
248        build_fn(self)
249    }
250}
251
252/// Convenience methods for common validation patterns.
253impl CheckBuilder {
254    /// Adds a primary key validation (non-null and unique).
255    ///
256    /// This is a convenience method that combines completeness and uniqueness constraints.
257    ///
258    /// # Examples
259    ///
260    /// ```rust
261    /// use term_guard::core::Check;
262    ///
263    /// # use term_guard::prelude::*;
264    /// # fn example() -> Result<Check> {
265    /// let check = Check::builder("primary_key")
266    ///     .primary_key(vec!["user_id"])
267    ///     .build();
268    ///
269    /// // Composite primary key
270    /// let check = Check::builder("composite_pk")
271    ///     .primary_key(vec!["tenant_id", "user_id"])
272    ///     .build();
273    /// # Ok(check)
274    /// # }
275    /// ```
276    pub fn primary_key<I, S>(self, columns: I) -> Self
277    where
278        I: IntoIterator<Item = S> + Clone,
279        S: Into<String>,
280    {
281        let columns_vec: Vec<String> = columns.clone().into_iter().map(Into::into).collect();
282
283        self.completeness(
284            columns_vec.clone(),
285            CompletenessOptions::full().into_constraint_options(),
286        )
287        .uniqueness(
288            columns_vec,
289            UniquenessType::FullUniqueness { threshold: 1.0 },
290            UniquenessOptions::default(),
291        )
292    }
293
294    /// Adds email validation with common settings.
295    ///
296    /// # Examples
297    ///
298    /// ```rust
299    /// use term_guard::core::Check;
300    ///
301    /// # use term_guard::prelude::*;
302    /// # fn example() -> Result<Check> {
303    /// let check = Check::builder("email_validation")
304    ///     .email("email_address", 0.95)
305    ///     .build();
306    /// # Ok(check)
307    /// # }
308    /// ```
309    pub fn email(self, column: impl Into<String>, threshold: f64) -> Self {
310        self.has_format(
311            column,
312            FormatType::Email,
313            threshold,
314            FormatOptions::new()
315                .trim_before_check(true)
316                .null_is_valid(false),
317        )
318    }
319
320    /// Adds URL validation with common settings.
321    ///
322    /// # Examples
323    ///
324    /// ```rust
325    /// use term_guard::core::Check;
326    ///
327    /// # use term_guard::prelude::*;
328    /// # fn example() -> Result<Check> {
329    /// let check = Check::builder("url_validation")
330    ///     .url("website", 0.90)
331    ///     .build();
332    /// # Ok(check)
333    /// # }
334    /// ```
335    pub fn url(self, column: impl Into<String>, threshold: f64) -> Self {
336        self.has_format(
337            column,
338            FormatType::Url {
339                allow_localhost: false,
340            },
341            threshold,
342            FormatOptions::new().trim_before_check(true),
343        )
344    }
345
346    /// Adds phone number validation.
347    ///
348    /// # Examples
349    ///
350    /// ```rust
351    /// use term_guard::core::Check;
352    ///
353    /// # use term_guard::prelude::*;
354    /// # fn example() -> Result<Check> {
355    /// let check = Check::builder("phone_validation")
356    ///     .phone("contact_phone", 0.90, Some("US"))
357    ///     .build();
358    /// # Ok(check)
359    /// # }
360    /// ```
361    pub fn phone(
362        self,
363        column: impl Into<String>,
364        threshold: f64,
365        country_code: Option<&str>,
366    ) -> Self {
367        let format_type = FormatType::Phone {
368            country: country_code.map(|s| s.to_string()),
369        };
370
371        self.has_format(
372            column,
373            format_type,
374            threshold,
375            FormatOptions::new().trim_before_check(true),
376        )
377    }
378
379    /// Adds Social Security Number (SSN) pattern validation.
380    ///
381    /// Validates that values in the specified column match US Social Security Number
382    /// patterns. Accepts both hyphenated (XXX-XX-XXXX) and non-hyphenated (XXXXXXXXX)
383    /// formats. Automatically excludes known invalid SSNs (e.g., 000-xx-xxxx, xxx-00-xxxx,
384    /// 666-xx-xxxx, 9xx-xx-xxxx).
385    ///
386    /// # Examples
387    ///
388    /// ```rust
389    /// use term_guard::core::Check;
390    ///
391    /// # use term_guard::prelude::*;
392    /// # fn example() -> Result<Check> {
393    /// let check = Check::builder("ssn_validation")
394    ///     .contains_ssn("ssn_column", 0.95)
395    ///     .build();
396    /// # Ok(check)
397    /// # }
398    /// ```
399    pub fn contains_ssn(self, column: impl Into<String>, threshold: f64) -> Self {
400        self.has_format(
401            column,
402            FormatType::SocialSecurityNumber,
403            threshold,
404            FormatOptions::new().trim_before_check(true),
405        )
406    }
407
408    /// Adds value range validation (min and max).
409    ///
410    /// # Examples
411    ///
412    /// ```rust
413    /// use term_guard::core::Check;
414    ///
415    /// # use term_guard::prelude::*;
416    /// # fn example() -> Result<Check> {
417    /// let check = Check::builder("age_range")
418    ///     .value_range("age", 0.0, 150.0)?
419    ///     .build();
420    /// # Ok(check)
421    /// # }
422    /// ```
423    pub fn value_range(self, column: impl Into<String>, min: f64, max: f64) -> Result<Self> {
424        self.statistics(
425            column,
426            StatisticalOptions::new()
427                .min(Assertion::GreaterThanOrEqual(min))
428                .max(Assertion::LessThanOrEqual(max)),
429        )
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436    use crate::core::Check;
437
438    #[test]
439    fn test_completeness_options() {
440        let full = CompletenessOptions::full();
441        let options = full.into_constraint_options();
442        assert_eq!(options.threshold_or(0.0), 1.0);
443        assert!(options.flag("null_is_failure"));
444
445        let threshold = CompletenessOptions::threshold(0.95);
446        let options = threshold.into_constraint_options();
447        assert_eq!(options.threshold_or(0.0), 0.95);
448
449        let at_least = CompletenessOptions::at_least(2);
450        let options = at_least.into_constraint_options();
451        assert!(matches!(
452            options.operator_or(LogicalOperator::All),
453            LogicalOperator::AtLeast(2)
454        ));
455    }
456
457    #[test]
458    fn test_statistical_options() {
459        let options = StatisticalOptions::new()
460            .min(Assertion::GreaterThan(0.0))
461            .max(Assertion::LessThan(100.0))
462            .mean(Assertion::Between(25.0, 75.0));
463
464        assert!(options.is_multi());
465        let stats = options.into_statistics();
466        assert_eq!(stats.len(), 3);
467    }
468
469    #[test]
470    fn test_new_builder_api() {
471        // Test single column completeness
472        let check = Check::builder("test")
473            .completeness(
474                "user_id",
475                CompletenessOptions::full().into_constraint_options(),
476            )
477            .build();
478        assert_eq!(check.constraints().len(), 1);
479
480        // Test multiple column completeness
481        let check = Check::builder("test")
482            .completeness(
483                vec!["email", "phone"],
484                CompletenessOptions::at_least(1).into_constraint_options(),
485            )
486            .build();
487        assert_eq!(check.constraints().len(), 1);
488
489        // Test format constraint
490        let check = Check::builder("test")
491            .has_format("email", FormatType::Email, 0.95, FormatOptions::default())
492            .build();
493        assert_eq!(check.constraints().len(), 1);
494
495        // Test convenience methods
496        let check = Check::builder("test").email("email_field", 0.95).build();
497        assert_eq!(check.constraints().len(), 1);
498    }
499}