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 value range validation (min and max).
380 ///
381 /// # Examples
382 ///
383 /// ```rust
384 /// use term_guard::core::Check;
385 ///
386 /// # use term_guard::prelude::*;
387 /// # fn example() -> Result<Check> {
388 /// let check = Check::builder("age_range")
389 /// .value_range("age", 0.0, 150.0)?
390 /// .build();
391 /// # Ok(check)
392 /// # }
393 /// ```
394 pub fn value_range(self, column: impl Into<String>, min: f64, max: f64) -> Result<Self> {
395 self.statistics(
396 column,
397 StatisticalOptions::new()
398 .min(Assertion::GreaterThanOrEqual(min))
399 .max(Assertion::LessThanOrEqual(max)),
400 )
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::core::Check;
408
409 #[test]
410 fn test_completeness_options() {
411 let full = CompletenessOptions::full();
412 let options = full.into_constraint_options();
413 assert_eq!(options.threshold_or(0.0), 1.0);
414 assert!(options.flag("null_is_failure"));
415
416 let threshold = CompletenessOptions::threshold(0.95);
417 let options = threshold.into_constraint_options();
418 assert_eq!(options.threshold_or(0.0), 0.95);
419
420 let at_least = CompletenessOptions::at_least(2);
421 let options = at_least.into_constraint_options();
422 assert!(matches!(
423 options.operator_or(LogicalOperator::All),
424 LogicalOperator::AtLeast(2)
425 ));
426 }
427
428 #[test]
429 fn test_statistical_options() {
430 let options = StatisticalOptions::new()
431 .min(Assertion::GreaterThan(0.0))
432 .max(Assertion::LessThan(100.0))
433 .mean(Assertion::Between(25.0, 75.0));
434
435 assert!(options.is_multi());
436 let stats = options.into_statistics();
437 assert_eq!(stats.len(), 3);
438 }
439
440 #[test]
441 fn test_new_builder_api() {
442 // Test single column completeness
443 let check = Check::builder("test")
444 .completeness(
445 "user_id",
446 CompletenessOptions::full().into_constraint_options(),
447 )
448 .build();
449 assert_eq!(check.constraints().len(), 1);
450
451 // Test multiple column completeness
452 let check = Check::builder("test")
453 .completeness(
454 vec!["email", "phone"],
455 CompletenessOptions::at_least(1).into_constraint_options(),
456 )
457 .build();
458 assert_eq!(check.constraints().len(), 1);
459
460 // Test format constraint
461 let check = Check::builder("test")
462 .has_format("email", FormatType::Email, 0.95, FormatOptions::default())
463 .build();
464 assert_eq!(check.constraints().len(), 1);
465
466 // Test convenience methods
467 let check = Check::builder("test").email("email_field", 0.95).build();
468 assert_eq!(check.constraints().len(), 1);
469 }
470}