skp_validator_rules/collection/
unique_items.rs

1//! Unique items validation rule for collections.
2
3use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4use std::collections::HashSet;
5use std::hash::Hash;
6
7/// Unique items validation rule - all items in a collection must be unique.
8///
9/// # Example
10///
11/// ```rust
12/// use skp_validator_rules::collection::unique_items::UniqueItemsRule;
13/// use skp_validator_core::{Rule, ValidationContext};
14///
15/// let rule = UniqueItemsRule::new();
16/// let ctx = ValidationContext::default();
17///
18/// assert!(rule.validate(&vec!["a", "b", "c"], &ctx).is_ok());
19/// assert!(rule.validate(&vec!["a", "b", "a"], &ctx).is_err());
20/// ```
21#[derive(Debug, Clone, Default)]
22pub struct UniqueItemsRule {
23    /// Custom error message
24    pub message: Option<String>,
25}
26
27impl UniqueItemsRule {
28    /// Create a new unique_items rule.
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Set custom error message.
34    pub fn message(mut self, msg: impl Into<String>) -> Self {
35        self.message = Some(msg.into());
36        self
37    }
38
39    fn get_message(&self) -> String {
40        self.message.clone().unwrap_or_else(|| "All items must be unique".to_string())
41    }
42}
43
44impl<T: Eq + Hash> Rule<Vec<T>> for UniqueItemsRule {
45    fn validate(&self, value: &Vec<T>, _ctx: &ValidationContext) -> ValidationResult<()> {
46        let mut seen = HashSet::new();
47        let mut duplicates = 0;
48
49        for item in value {
50            if !seen.insert(item) {
51                duplicates += 1;
52            }
53        }
54
55        if duplicates == 0 {
56            Ok(())
57        } else {
58            Err(ValidationErrors::from_iter([
59                ValidationError::root("unique_items", self.get_message())
60                    .with_param("duplicate_count", duplicates as i64)
61            ]))
62        }
63    }
64
65    fn name(&self) -> &'static str {
66        "unique_items"
67    }
68
69    fn default_message(&self) -> String {
70        self.get_message()
71    }
72}
73
74impl<T: Eq + Hash> Rule<[T]> for UniqueItemsRule {
75    fn validate(&self, value: &[T], _ctx: &ValidationContext) -> ValidationResult<()> {
76        let mut seen = HashSet::new();
77        let mut duplicates = 0;
78
79        for item in value {
80            if !seen.insert(item) {
81                duplicates += 1;
82            }
83        }
84
85        if duplicates == 0 {
86            Ok(())
87        } else {
88            Err(ValidationErrors::from_iter([
89                ValidationError::root("unique_items", self.get_message())
90                    .with_param("duplicate_count", duplicates as i64)
91            ]))
92        }
93    }
94
95    fn name(&self) -> &'static str {
96        "unique_items"
97    }
98
99    fn default_message(&self) -> String {
100        self.get_message()
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_unique_strings() {
110        let rule = UniqueItemsRule::new();
111        let ctx = ValidationContext::default();
112
113        assert!(rule.validate(&vec!["a", "b", "c"], &ctx).is_ok());
114        assert!(rule.validate(&vec!["a", "b", "a"], &ctx).is_err());
115    }
116
117    #[test]
118    fn test_unique_numbers() {
119        let rule = UniqueItemsRule::new();
120        let ctx = ValidationContext::default();
121
122        assert!(rule.validate(&vec![1, 2, 3, 4, 5], &ctx).is_ok());
123        assert!(rule.validate(&vec![1, 2, 3, 2, 5], &ctx).is_err());
124    }
125
126    #[test]
127    fn test_empty_is_valid() {
128        let rule = UniqueItemsRule::new();
129        let ctx = ValidationContext::default();
130
131        assert!(rule.validate(&Vec::<i32>::new(), &ctx).is_ok());
132    }
133
134    #[test]
135    fn test_single_item() {
136        let rule = UniqueItemsRule::new();
137        let ctx = ValidationContext::default();
138
139        assert!(rule.validate(&vec![1], &ctx).is_ok());
140    }
141}