skp_validator_rules/string/
contains.rs1use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4
5#[derive(Debug, Clone)]
20pub struct ContainsRule {
21 pub substring: String,
23 pub case_insensitive: bool,
25 pub message: Option<String>,
27}
28
29impl ContainsRule {
30 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 pub fn case_insensitive(mut self) -> Self {
41 self.case_insensitive = true;
42 self
43 }
44
45 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#[derive(Debug, Clone)]
105pub struct PrefixRule {
106 pub prefix: String,
108 pub case_insensitive: bool,
110 pub message: Option<String>,
112}
113
114impl PrefixRule {
115 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 pub fn case_insensitive(mut self) -> Self {
126 self.case_insensitive = true;
127 self
128 }
129
130 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#[derive(Debug, Clone)]
189pub struct SuffixRule {
190 pub suffix: String,
192 pub case_insensitive: bool,
194 pub message: Option<String>,
196}
197
198impl SuffixRule {
199 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 pub fn case_insensitive(mut self) -> Self {
210 self.case_insensitive = true;
211 self
212 }
213
214 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}