rustapi_validate/v2/
group.rs

1//! Validation groups for conditional validation.
2
3use serde::{Deserialize, Serialize};
4
5/// Validation groups for applying different rules based on operation type.
6///
7/// ## Example
8///
9/// ```rust,ignore
10/// use rustapi_validate::v2::prelude::*;
11///
12/// struct User {
13///     id: Option<i64>,
14///     email: String,
15/// }
16///
17/// impl User {
18///     fn validate_for_group(&self, group: ValidationGroup) -> Result<(), ValidationErrors> {
19///         let mut errors = ValidationErrors::new();
20///         
21///         // Email is always required
22///         if let Err(e) = EmailRule::default().validate(&self.email) {
23///             errors.add("email", e);
24///         }
25///         
26///         // ID is required only for updates
27///         if group == ValidationGroup::Update && self.id.is_none() {
28///             errors.add("id", RuleError::new("required", "ID is required for updates"));
29///         }
30///         
31///         errors.into_result()
32///     }
33/// }
34/// ```
35#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum ValidationGroup {
38    /// Validation rules for creating new records
39    Create,
40    /// Validation rules for updating existing records
41    Update,
42    /// Custom validation group with a name
43    Custom(String),
44    /// Default group - applies to all operations
45    #[default]
46    Default,
47}
48
49impl ValidationGroup {
50    /// Create a custom validation group.
51    pub fn custom(name: impl Into<String>) -> Self {
52        Self::Custom(name.into())
53    }
54
55    /// Check if this group matches another group.
56    ///
57    /// Default group matches everything.
58    pub fn matches(&self, other: &ValidationGroup) -> bool {
59        match (self, other) {
60            (ValidationGroup::Default, _) => true,
61            (_, ValidationGroup::Default) => true,
62            (a, b) => a == b,
63        }
64    }
65
66    /// Get the group name as a string.
67    pub fn name(&self) -> &str {
68        match self {
69            ValidationGroup::Create => "create",
70            ValidationGroup::Update => "update",
71            ValidationGroup::Custom(name) => name,
72            ValidationGroup::Default => "default",
73        }
74    }
75}
76
77impl From<&str> for ValidationGroup {
78    fn from(s: &str) -> Self {
79        match s.to_lowercase().as_str() {
80            "create" => ValidationGroup::Create,
81            "update" => ValidationGroup::Update,
82            "default" => ValidationGroup::Default,
83            other => ValidationGroup::Custom(other.to_string()),
84        }
85    }
86}
87
88impl std::fmt::Display for ValidationGroup {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        write!(f, "{}", self.name())
91    }
92}
93
94/// A validation rule with an associated group.
95#[derive(Debug, Clone)]
96pub struct GroupedRule<R> {
97    /// The validation rule
98    pub rule: R,
99    /// The group this rule belongs to
100    pub group: ValidationGroup,
101}
102
103impl<R> GroupedRule<R> {
104    /// Create a new grouped rule.
105    pub fn new(rule: R, group: ValidationGroup) -> Self {
106        Self { rule, group }
107    }
108
109    /// Create a rule for the Create group.
110    pub fn for_create(rule: R) -> Self {
111        Self::new(rule, ValidationGroup::Create)
112    }
113
114    /// Create a rule for the Update group.
115    pub fn for_update(rule: R) -> Self {
116        Self::new(rule, ValidationGroup::Update)
117    }
118
119    /// Create a rule for the Default group (applies to all).
120    pub fn for_default(rule: R) -> Self {
121        Self::new(rule, ValidationGroup::Default)
122    }
123
124    /// Check if this rule should be applied for the given group.
125    pub fn applies_to(&self, group: &ValidationGroup) -> bool {
126        self.group.matches(group)
127    }
128}
129
130/// A collection of grouped validation rules for a field.
131#[derive(Debug, Clone, Default)]
132pub struct GroupedRules<R> {
133    rules: Vec<GroupedRule<R>>,
134}
135
136impl<R> GroupedRules<R> {
137    /// Create a new empty collection.
138    pub fn new() -> Self {
139        Self { rules: Vec::new() }
140    }
141
142    /// Add a rule with a group.
143    pub fn add(mut self, rule: R, group: ValidationGroup) -> Self {
144        self.rules.push(GroupedRule::new(rule, group));
145        self
146    }
147
148    /// Add a rule for the Create group.
149    pub fn on_create(self, rule: R) -> Self {
150        self.add(rule, ValidationGroup::Create)
151    }
152
153    /// Add a rule for the Update group.
154    pub fn on_update(self, rule: R) -> Self {
155        self.add(rule, ValidationGroup::Update)
156    }
157
158    /// Add a rule that applies to all groups.
159    pub fn always(self, rule: R) -> Self {
160        self.add(rule, ValidationGroup::Default)
161    }
162
163    /// Get rules that apply to a specific group.
164    pub fn for_group<'a>(&'a self, group: &'a ValidationGroup) -> impl Iterator<Item = &'a R> + 'a {
165        self.rules
166            .iter()
167            .filter(move |gr| gr.applies_to(group))
168            .map(|gr| &gr.rule)
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn group_from_str() {
178        assert_eq!(ValidationGroup::from("create"), ValidationGroup::Create);
179        assert_eq!(ValidationGroup::from("update"), ValidationGroup::Update);
180        assert_eq!(ValidationGroup::from("default"), ValidationGroup::Default);
181        assert_eq!(
182            ValidationGroup::from("custom_group"),
183            ValidationGroup::Custom("custom_group".to_string())
184        );
185    }
186
187    #[test]
188    fn group_matches() {
189        assert!(ValidationGroup::Default.matches(&ValidationGroup::Create));
190        assert!(ValidationGroup::Create.matches(&ValidationGroup::Default));
191        assert!(ValidationGroup::Create.matches(&ValidationGroup::Create));
192        assert!(!ValidationGroup::Create.matches(&ValidationGroup::Update));
193    }
194
195    #[test]
196    fn group_name() {
197        assert_eq!(ValidationGroup::Create.name(), "create");
198        assert_eq!(ValidationGroup::Update.name(), "update");
199        assert_eq!(ValidationGroup::Default.name(), "default");
200        assert_eq!(ValidationGroup::Custom("test".to_string()).name(), "test");
201    }
202
203    #[test]
204    fn group_serialization() {
205        let group = ValidationGroup::Create;
206        let json = serde_json::to_string(&group).unwrap();
207        assert_eq!(json, "\"create\"");
208
209        let parsed: ValidationGroup = serde_json::from_str(&json).unwrap();
210        assert_eq!(parsed, ValidationGroup::Create);
211    }
212
213    #[test]
214    fn grouped_rule_applies_to() {
215        let create_rule = GroupedRule::for_create("rule1");
216        let update_rule = GroupedRule::for_update("rule2");
217        let default_rule = GroupedRule::for_default("rule3");
218
219        assert!(create_rule.applies_to(&ValidationGroup::Create));
220        assert!(!create_rule.applies_to(&ValidationGroup::Update));
221        assert!(create_rule.applies_to(&ValidationGroup::Default));
222
223        assert!(!update_rule.applies_to(&ValidationGroup::Create));
224        assert!(update_rule.applies_to(&ValidationGroup::Update));
225        assert!(update_rule.applies_to(&ValidationGroup::Default));
226
227        assert!(default_rule.applies_to(&ValidationGroup::Create));
228        assert!(default_rule.applies_to(&ValidationGroup::Update));
229        assert!(default_rule.applies_to(&ValidationGroup::Default));
230    }
231
232    #[test]
233    fn grouped_rules_for_group() {
234        let rules = GroupedRules::new()
235            .on_create("create_only")
236            .on_update("update_only")
237            .always("always");
238
239        let create_rules: Vec<_> = rules.for_group(&ValidationGroup::Create).collect();
240        assert_eq!(create_rules.len(), 2);
241        assert!(create_rules.contains(&&"create_only"));
242        assert!(create_rules.contains(&&"always"));
243
244        let update_rules: Vec<_> = rules.for_group(&ValidationGroup::Update).collect();
245        assert_eq!(update_rules.len(), 2);
246        assert!(update_rules.contains(&&"update_only"));
247        assert!(update_rules.contains(&&"always"));
248    }
249}