term_guard/core/
logical.rs

1//! Logical operators and column specifications for unified constraint APIs.
2//!
3//! This module provides the core infrastructure for combining constraint results
4//! across multiple columns using logical operators like AND, OR, and threshold-based
5//! operators.
6
7use std::fmt;
8
9/// Logical operators for combining multiple boolean results.
10///
11/// These operators define how to evaluate constraint results across multiple columns
12/// or multiple constraint evaluations.
13///
14/// # Examples
15///
16/// ```rust
17/// use term_guard::core::LogicalOperator;
18///
19/// // All columns must satisfy the constraint
20/// let all = LogicalOperator::All;
21///
22/// // At least one column must satisfy
23/// let any = LogicalOperator::Any;
24///
25/// // Exactly 2 columns must satisfy
26/// let exactly_two = LogicalOperator::Exactly(2);
27///
28/// // At least 3 columns must satisfy
29/// let at_least_three = LogicalOperator::AtLeast(3);
30/// ```
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum LogicalOperator {
33    /// All values must satisfy the condition (AND logic)
34    All,
35    /// At least one value must satisfy the condition (OR logic)
36    Any,
37    /// Exactly N values must satisfy the condition
38    Exactly(usize),
39    /// At least N values must satisfy the condition
40    AtLeast(usize),
41    /// At most N values must satisfy the condition
42    AtMost(usize),
43}
44
45impl LogicalOperator {
46    /// Evaluates a slice of boolean results according to this operator.
47    ///
48    /// # Arguments
49    ///
50    /// * `results` - A slice of boolean values to evaluate
51    ///
52    /// # Returns
53    ///
54    /// `true` if the results satisfy the operator's condition, `false` otherwise
55    ///
56    /// # Examples
57    ///
58    /// ```rust
59    /// use term_guard::core::LogicalOperator;
60    ///
61    /// let results = vec![true, false, true];
62    ///
63    /// assert!(LogicalOperator::Any.evaluate(&results));
64    /// assert!(!LogicalOperator::All.evaluate(&results));
65    /// assert!(LogicalOperator::Exactly(2).evaluate(&results));
66    /// assert!(LogicalOperator::AtLeast(1).evaluate(&results));
67    /// assert!(LogicalOperator::AtMost(2).evaluate(&results));
68    /// ```
69    pub fn evaluate(&self, results: &[bool]) -> bool {
70        if results.is_empty() {
71            return match self {
72                LogicalOperator::All => true,  // Vacuous truth
73                LogicalOperator::Any => false, // No elements satisfy
74                LogicalOperator::Exactly(n) => *n == 0,
75                LogicalOperator::AtLeast(n) => *n == 0,
76                LogicalOperator::AtMost(_) => true, // 0 <= any n
77            };
78        }
79
80        let true_count = results.iter().filter(|&&x| x).count();
81
82        match self {
83            LogicalOperator::All => true_count == results.len(),
84            LogicalOperator::Any => true_count > 0,
85            LogicalOperator::Exactly(n) => true_count == *n,
86            LogicalOperator::AtLeast(n) => true_count >= *n,
87            LogicalOperator::AtMost(n) => true_count <= *n,
88        }
89    }
90
91    /// Returns a human-readable description of the operator.
92    pub fn description(&self) -> String {
93        match self {
94            LogicalOperator::All => "all".to_string(),
95            LogicalOperator::Any => "any".to_string(),
96            LogicalOperator::Exactly(n) => format!("exactly {n}"),
97            LogicalOperator::AtLeast(n) => format!("at least {n}"),
98            LogicalOperator::AtMost(n) => format!("at most {n}"),
99        }
100    }
101}
102
103impl fmt::Display for LogicalOperator {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        write!(f, "{}", self.description())
106    }
107}
108
109/// Specification for columns in a constraint.
110///
111/// This enum allows constraints to work with either a single column or multiple columns,
112/// providing a unified interface for both cases.
113///
114/// # Examples
115///
116/// ```rust
117/// use term_guard::core::ColumnSpec;
118///
119/// // Single column
120/// let single = ColumnSpec::Single("user_id".to_string());
121///
122/// // Multiple columns
123/// let multiple = ColumnSpec::Multiple(vec!["email".to_string(), "phone".to_string()]);
124///
125/// // Convert from various types
126/// let from_str = ColumnSpec::from("user_id");
127/// let from_vec = ColumnSpec::from(vec!["col1", "col2"]);
128/// ```
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum ColumnSpec {
131    /// A single column
132    Single(String),
133    /// Multiple columns
134    Multiple(Vec<String>),
135}
136
137impl ColumnSpec {
138    /// Returns the columns as a vector, regardless of whether this is a single or multiple column spec.
139    pub fn as_vec(&self) -> Vec<&str> {
140        match self {
141            ColumnSpec::Single(col) => vec![col.as_str()],
142            ColumnSpec::Multiple(cols) => cols.iter().map(|s| s.as_str()).collect(),
143        }
144    }
145
146    /// Returns the number of columns in this specification.
147    pub fn len(&self) -> usize {
148        match self {
149            ColumnSpec::Single(_) => 1,
150            ColumnSpec::Multiple(cols) => cols.len(),
151        }
152    }
153
154    /// Returns true if this specification contains no columns.
155    pub fn is_empty(&self) -> bool {
156        match self {
157            ColumnSpec::Single(_) => false,
158            ColumnSpec::Multiple(cols) => cols.is_empty(),
159        }
160    }
161
162    /// Returns true if this is a single column specification.
163    pub fn is_single(&self) -> bool {
164        matches!(self, ColumnSpec::Single(_))
165    }
166
167    /// Returns true if this is a multiple column specification.
168    pub fn is_multiple(&self) -> bool {
169        matches!(self, ColumnSpec::Multiple(_))
170    }
171
172    /// Converts to a multiple column specification, even if currently single.
173    pub fn to_multiple(self) -> Vec<String> {
174        match self {
175            ColumnSpec::Single(col) => vec![col],
176            ColumnSpec::Multiple(cols) => cols,
177        }
178    }
179}
180
181impl From<String> for ColumnSpec {
182    fn from(s: String) -> Self {
183        ColumnSpec::Single(s)
184    }
185}
186
187impl From<&str> for ColumnSpec {
188    fn from(s: &str) -> Self {
189        ColumnSpec::Single(s.to_string())
190    }
191}
192
193impl From<Vec<String>> for ColumnSpec {
194    fn from(v: Vec<String>) -> Self {
195        match v.len() {
196            1 => {
197                // Safe to use expect here because we've checked the length is 1
198                #[allow(clippy::expect_used)]
199                ColumnSpec::Single(
200                    v.into_iter()
201                        .next()
202                        .expect("Vector with length 1 should have one element"),
203                )
204            }
205            _ => ColumnSpec::Multiple(v),
206        }
207    }
208}
209
210impl From<Vec<&str>> for ColumnSpec {
211    fn from(v: Vec<&str>) -> Self {
212        let strings: Vec<String> = v.into_iter().map(|s| s.to_string()).collect();
213        strings.into()
214    }
215}
216
217impl<const N: usize> From<[&str; N]> for ColumnSpec {
218    fn from(arr: [&str; N]) -> Self {
219        let vec: Vec<String> = arr.iter().map(|s| s.to_string()).collect();
220        vec.into()
221    }
222}
223
224/// Builder pattern for constraint options.
225///
226/// This trait provides a fluent interface for configuring constraints
227/// with various options.
228pub trait ConstraintOptionsBuilder: Sized {
229    /// Sets the logical operator for combining results.
230    fn with_operator(self, operator: LogicalOperator) -> Self;
231
232    /// Sets the threshold value.
233    fn with_threshold(self, threshold: f64) -> Self;
234
235    /// Enables or disables a boolean option.
236    fn with_option(self, name: &str, value: bool) -> Self;
237}
238
239/// Result of evaluating a logical expression.
240///
241/// This struct contains both the overall boolean result and detailed information
242/// about individual evaluations.
243#[derive(Debug, Clone)]
244pub struct LogicalResult {
245    /// The overall result of the logical evaluation
246    pub result: bool,
247    /// Individual results that were combined
248    pub individual_results: Vec<(String, bool)>,
249    /// The operator used for combination
250    pub operator: LogicalOperator,
251    /// Optional detailed message
252    pub message: Option<String>,
253}
254
255impl LogicalResult {
256    /// Creates a new logical result.
257    pub fn new(
258        result: bool,
259        individual_results: Vec<(String, bool)>,
260        operator: LogicalOperator,
261    ) -> Self {
262        Self {
263            result,
264            individual_results,
265            operator,
266            message: None,
267        }
268    }
269
270    /// Creates a logical result with a message.
271    pub fn with_message(mut self, message: String) -> Self {
272        self.message = Some(message);
273        self
274    }
275
276    /// Returns the columns that passed the evaluation.
277    pub fn passed_columns(&self) -> Vec<&str> {
278        self.individual_results
279            .iter()
280            .filter(|(_, passed)| *passed)
281            .map(|(col, _)| col.as_str())
282            .collect()
283    }
284
285    /// Returns the columns that failed the evaluation.
286    pub fn failed_columns(&self) -> Vec<&str> {
287        self.individual_results
288            .iter()
289            .filter(|(_, passed)| !*passed)
290            .map(|(col, _)| col.as_str())
291            .collect()
292    }
293
294    /// Returns the pass rate (ratio of passed to total).
295    pub fn pass_rate(&self) -> f64 {
296        if self.individual_results.is_empty() {
297            return 0.0;
298        }
299        let passed = self.individual_results.iter().filter(|(_, p)| *p).count();
300        passed as f64 / self.individual_results.len() as f64
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_logical_operator_all() {
310        let op = LogicalOperator::All;
311
312        assert!(op.evaluate(&[])); // Vacuous truth
313        assert!(op.evaluate(&[true]));
314        assert!(!op.evaluate(&[false]));
315        assert!(op.evaluate(&[true, true, true]));
316        assert!(!op.evaluate(&[true, false, true]));
317    }
318
319    #[test]
320    fn test_logical_operator_any() {
321        let op = LogicalOperator::Any;
322
323        assert!(!op.evaluate(&[])); // No elements satisfy
324        assert!(op.evaluate(&[true]));
325        assert!(!op.evaluate(&[false]));
326        assert!(op.evaluate(&[true, false, false]));
327        assert!(!op.evaluate(&[false, false, false]));
328    }
329
330    #[test]
331    fn test_logical_operator_exactly() {
332        let op = LogicalOperator::Exactly(2);
333
334        assert!(!op.evaluate(&[]));
335        assert!(!op.evaluate(&[true]));
336        assert!(!op.evaluate(&[true, true, true]));
337        assert!(op.evaluate(&[true, true, false]));
338        assert!(op.evaluate(&[true, false, true]));
339
340        // Edge case: Exactly(0)
341        let op_zero = LogicalOperator::Exactly(0);
342        assert!(op_zero.evaluate(&[]));
343        assert!(!op_zero.evaluate(&[true]));
344        assert!(op_zero.evaluate(&[false, false]));
345    }
346
347    #[test]
348    fn test_logical_operator_at_least() {
349        let op = LogicalOperator::AtLeast(2);
350
351        assert!(!op.evaluate(&[]));
352        assert!(!op.evaluate(&[true]));
353        assert!(op.evaluate(&[true, true]));
354        assert!(op.evaluate(&[true, true, true]));
355        assert!(op.evaluate(&[true, true, false]));
356        assert!(!op.evaluate(&[true, false, false]));
357    }
358
359    #[test]
360    fn test_logical_operator_at_most() {
361        let op = LogicalOperator::AtMost(2);
362
363        assert!(op.evaluate(&[]));
364        assert!(op.evaluate(&[true]));
365        assert!(op.evaluate(&[true, true]));
366        assert!(!op.evaluate(&[true, true, true]));
367        assert!(op.evaluate(&[true, false, false]));
368        assert!(op.evaluate(&[false, false, false]));
369    }
370
371    #[test]
372    fn test_column_spec_single() {
373        let spec = ColumnSpec::Single("user_id".to_string());
374
375        assert_eq!(spec.len(), 1);
376        assert!(!spec.is_empty());
377        assert!(spec.is_single());
378        assert!(!spec.is_multiple());
379        assert_eq!(spec.as_vec(), vec!["user_id"]);
380    }
381
382    #[test]
383    fn test_column_spec_multiple() {
384        let spec = ColumnSpec::Multiple(vec!["email".to_string(), "phone".to_string()]);
385
386        assert_eq!(spec.len(), 2);
387        assert!(!spec.is_empty());
388        assert!(!spec.is_single());
389        assert!(spec.is_multiple());
390        assert_eq!(spec.as_vec(), vec!["email", "phone"]);
391    }
392
393    #[test]
394    fn test_column_spec_conversions() {
395        // From &str
396        let spec = ColumnSpec::from("test");
397        assert!(matches!(spec, ColumnSpec::Single(s) if s == "test"));
398
399        // From String
400        let spec = ColumnSpec::from("test".to_string());
401        assert!(matches!(spec, ColumnSpec::Single(s) if s == "test"));
402
403        // From Vec with single element
404        let spec = ColumnSpec::from(vec!["test"]);
405        assert!(matches!(spec, ColumnSpec::Single(s) if s == "test"));
406
407        // From Vec with multiple elements
408        let spec = ColumnSpec::from(vec!["test1", "test2"]);
409        assert!(matches!(spec, ColumnSpec::Multiple(v) if v.len() == 2));
410
411        // From array
412        let spec = ColumnSpec::from(["a", "b", "c"]);
413        assert!(matches!(spec, ColumnSpec::Multiple(v) if v.len() == 3));
414    }
415
416    #[test]
417    fn test_logical_result() {
418        let individual = vec![
419            ("col1".to_string(), true),
420            ("col2".to_string(), false),
421            ("col3".to_string(), true),
422        ];
423
424        let result = LogicalResult::new(true, individual, LogicalOperator::AtLeast(2));
425
426        assert_eq!(result.passed_columns(), vec!["col1", "col3"]);
427        assert_eq!(result.failed_columns(), vec!["col2"]);
428        assert_eq!(result.pass_rate(), 2.0 / 3.0);
429    }
430}