skp_validator_rules/string/
contains.rs

1//! Contains, prefix, and suffix validation rules.
2
3use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4
5/// Contains validation rule - string must contain a substring.
6///
7/// # Example
8///
9/// ```rust
10/// use skp_validator_rules::string::contains::ContainsRule;
11/// use skp_validator_core::{Rule, ValidationContext};
12///
13/// let rule = ContainsRule::new("@");
14/// let ctx = ValidationContext::default();
15///
16/// assert!(rule.validate("test@example.com", &ctx).is_ok());
17/// assert!(rule.validate("testexample.com", &ctx).is_err());
18/// ```
19#[derive(Debug, Clone)]
20pub struct ContainsRule {
21    /// The substring to search for
22    pub substring: String,
23    /// Case insensitive matching
24    pub case_insensitive: bool,
25    /// Custom error message
26    pub message: Option<String>,
27}
28
29impl ContainsRule {
30    /// Create a new contains rule.
31    pub fn new(substring: impl Into<String>) -> Self {
32        Self {
33            substring: substring.into(),
34            case_insensitive: false,
35            message: None,
36        }
37    }
38
39    /// Enable case-insensitive matching.
40    pub fn case_insensitive(mut self) -> Self {
41        self.case_insensitive = true;
42        self
43    }
44
45    /// Set custom error message.
46    pub fn message(mut self, msg: impl Into<String>) -> Self {
47        self.message = Some(msg.into());
48        self
49    }
50
51    fn get_message(&self) -> String {
52        self.message.clone().unwrap_or_else(|| {
53            format!("Must contain '{}'", self.substring)
54        })
55    }
56}
57
58impl Rule<str> for ContainsRule {
59    fn validate(&self, value: &str, _ctx: &ValidationContext) -> ValidationResult<()> {
60        if value.is_empty() {
61            return Ok(());
62        }
63
64        let contains = if self.case_insensitive {
65            value.to_lowercase().contains(&self.substring.to_lowercase())
66        } else {
67            value.contains(&self.substring)
68        };
69
70        if contains {
71            Ok(())
72        } else {
73            Err(ValidationErrors::from_iter([
74                ValidationError::root("contains", self.get_message())
75                    .with_param("substring", self.substring.clone())
76            ]))
77        }
78    }
79
80    fn name(&self) -> &'static str {
81        "contains"
82    }
83
84    fn default_message(&self) -> String {
85        self.get_message()
86    }
87}
88
89impl Rule<String> for ContainsRule {
90    fn validate(&self, value: &String, ctx: &ValidationContext) -> ValidationResult<()> {
91        <Self as Rule<str>>::validate(self, value.as_str(), ctx)
92    }
93
94    fn name(&self) -> &'static str {
95        "contains"
96    }
97
98    fn default_message(&self) -> String {
99        self.get_message()
100    }
101}
102
103/// Prefix validation rule - string must start with a prefix.
104#[derive(Debug, Clone)]
105pub struct PrefixRule {
106    /// The prefix to match
107    pub prefix: String,
108    /// Case insensitive matching
109    pub case_insensitive: bool,
110    /// Custom error message
111    pub message: Option<String>,
112}
113
114impl PrefixRule {
115    /// Create a new prefix rule.
116    pub fn new(prefix: impl Into<String>) -> Self {
117        Self {
118            prefix: prefix.into(),
119            case_insensitive: false,
120            message: None,
121        }
122    }
123
124    /// Enable case-insensitive matching.
125    pub fn case_insensitive(mut self) -> Self {
126        self.case_insensitive = true;
127        self
128    }
129
130    /// Set custom error message.
131    pub fn message(mut self, msg: impl Into<String>) -> Self {
132        self.message = Some(msg.into());
133        self
134    }
135
136    fn get_message(&self) -> String {
137        self.message.clone().unwrap_or_else(|| {
138            format!("Must start with '{}'", self.prefix)
139        })
140    }
141}
142
143impl Rule<str> for PrefixRule {
144    fn validate(&self, value: &str, _ctx: &ValidationContext) -> ValidationResult<()> {
145        if value.is_empty() {
146            return Ok(());
147        }
148
149        let starts_with = if self.case_insensitive {
150            value.to_lowercase().starts_with(&self.prefix.to_lowercase())
151        } else {
152            value.starts_with(&self.prefix)
153        };
154
155        if starts_with {
156            Ok(())
157        } else {
158            Err(ValidationErrors::from_iter([
159                ValidationError::root("prefix", self.get_message())
160            ]))
161        }
162    }
163
164    fn name(&self) -> &'static str {
165        "prefix"
166    }
167
168    fn default_message(&self) -> String {
169        self.get_message()
170    }
171}
172
173impl Rule<String> for PrefixRule {
174    fn validate(&self, value: &String, ctx: &ValidationContext) -> ValidationResult<()> {
175        <Self as Rule<str>>::validate(self, value.as_str(), ctx)
176    }
177
178    fn name(&self) -> &'static str {
179        "prefix"
180    }
181
182    fn default_message(&self) -> String {
183        self.get_message()
184    }
185}
186
187/// Suffix validation rule - string must end with a suffix.
188#[derive(Debug, Clone)]
189pub struct SuffixRule {
190    /// The suffix to match
191    pub suffix: String,
192    /// Case insensitive matching
193    pub case_insensitive: bool,
194    /// Custom error message
195    pub message: Option<String>,
196}
197
198impl SuffixRule {
199    /// Create a new suffix rule.
200    pub fn new(suffix: impl Into<String>) -> Self {
201        Self {
202            suffix: suffix.into(),
203            case_insensitive: false,
204            message: None,
205        }
206    }
207
208    /// Enable case-insensitive matching.
209    pub fn case_insensitive(mut self) -> Self {
210        self.case_insensitive = true;
211        self
212    }
213
214    /// Set custom error message.
215    pub fn message(mut self, msg: impl Into<String>) -> Self {
216        self.message = Some(msg.into());
217        self
218    }
219
220    fn get_message(&self) -> String {
221        self.message.clone().unwrap_or_else(|| {
222            format!("Must end with '{}'", self.suffix)
223        })
224    }
225}
226
227impl Rule<str> for SuffixRule {
228    fn validate(&self, value: &str, _ctx: &ValidationContext) -> ValidationResult<()> {
229        if value.is_empty() {
230            return Ok(());
231        }
232
233        let ends_with = if self.case_insensitive {
234            value.to_lowercase().ends_with(&self.suffix.to_lowercase())
235        } else {
236            value.ends_with(&self.suffix)
237        };
238
239        if ends_with {
240            Ok(())
241        } else {
242            Err(ValidationErrors::from_iter([
243                ValidationError::root("suffix", self.get_message())
244            ]))
245        }
246    }
247
248    fn name(&self) -> &'static str {
249        "suffix"
250    }
251
252    fn default_message(&self) -> String {
253        self.get_message()
254    }
255}
256
257impl Rule<String> for SuffixRule {
258    fn validate(&self, value: &String, ctx: &ValidationContext) -> ValidationResult<()> {
259        <Self as Rule<str>>::validate(self, value.as_str(), ctx)
260    }
261
262    fn name(&self) -> &'static str {
263        "suffix"
264    }
265
266    fn default_message(&self) -> String {
267        self.get_message()
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_contains() {
277        let rule = ContainsRule::new("@");
278        let ctx = ValidationContext::default();
279
280        assert!(rule.validate("test@example.com", &ctx).is_ok());
281        assert!(rule.validate("testexample.com", &ctx).is_err());
282    }
283
284    #[test]
285    fn test_contains_case_insensitive() {
286        let rule = ContainsRule::new("HELLO").case_insensitive();
287        let ctx = ValidationContext::default();
288
289        assert!(rule.validate("say hello world", &ctx).is_ok());
290    }
291
292    #[test]
293    fn test_prefix() {
294        let rule = PrefixRule::new("https://");
295        let ctx = ValidationContext::default();
296
297        assert!(rule.validate("https://example.com", &ctx).is_ok());
298        assert!(rule.validate("http://example.com", &ctx).is_err());
299    }
300
301    #[test]
302    fn test_suffix() {
303        let rule = SuffixRule::new(".pdf");
304        let ctx = ValidationContext::default();
305
306        assert!(rule.validate("document.pdf", &ctx).is_ok());
307        assert!(rule.validate("document.doc", &ctx).is_err());
308    }
309
310    #[test]
311    fn test_suffix_case_insensitive() {
312        let rule = SuffixRule::new(".pdf").case_insensitive();
313        let ctx = ValidationContext::default();
314
315        assert!(rule.validate("document.PDF", &ctx).is_ok());
316    }
317
318    #[test]
319    fn test_empty_is_valid() {
320        let contains = ContainsRule::new("test");
321        let prefix = PrefixRule::new("test");
322        let suffix = SuffixRule::new("test");
323        let ctx = ValidationContext::default();
324
325        assert!(contains.validate("", &ctx).is_ok());
326        assert!(prefix.validate("", &ctx).is_ok());
327        assert!(suffix.validate("", &ctx).is_ok());
328    }
329}