reinhardt_rest/metadata/
patterns.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ValidationPattern {
18 pub pattern: String,
20 pub description: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub examples: Option<Vec<String>>,
25}
26
27impl ValidationPattern {
28 pub fn new(pattern: impl Into<String>, description: impl Into<String>) -> Self {
42 Self {
43 pattern: pattern.into(),
44 description: description.into(),
45 examples: None,
46 }
47 }
48
49 pub fn with_examples(
64 pattern: impl Into<String>,
65 description: impl Into<String>,
66 examples: Vec<impl Into<String>>,
67 ) -> Self {
68 Self {
69 pattern: pattern.into(),
70 description: description.into(),
71 examples: Some(examples.into_iter().map(|e| e.into()).collect()),
72 }
73 }
74
75 pub fn email() -> Self {
89 Self::with_examples(
90 r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
91 "Email address",
92 vec!["user@example.com", "test.user@example.co.uk"],
93 )
94 }
95
96 pub fn url() -> Self {
109 Self::with_examples(
110 r"^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$",
111 "URL",
112 vec!["https://example.com", "http://example.com/path"],
113 )
114 }
115
116 pub fn uuid() -> Self {
128 Self::with_examples(
129 r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
130 "UUID",
131 vec!["123e4567-e89b-12d3-a456-426614174000"],
132 )
133 }
134
135 pub fn alphanumeric() -> Self {
149 Self::with_examples(
150 r"^[a-zA-Z0-9]+$",
151 "Alphanumeric characters only",
152 vec!["abc123", "TestUser"],
153 )
154 }
155
156 pub fn slug() -> Self {
170 Self::with_examples(
171 r"^[a-z0-9]+(?:-[a-z0-9]+)*$",
172 "URL slug (lowercase, hyphens only)",
173 vec!["my-blog-post", "article-123"],
174 )
175 }
176
177 pub fn phone() -> Self {
190 Self::with_examples(
191 r"^\+[1-9]\d{0,3}-\d{1,4}-\d{1,4}-\d{1,9}$",
192 "Phone number (international format with country code)",
193 vec!["+1-234-567-8900", "+81-90-1234-5678"],
194 )
195 }
196
197 pub fn hex_color() -> Self {
211 Self::with_examples(
212 r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$",
213 "Hexadecimal color code",
214 vec!["#FF5733", "#fff"],
215 )
216 }
217
218 pub fn ipv4() -> Self {
232 Self::with_examples(
233 r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
234 "IPv4 address",
235 vec!["192.168.1.1", "10.0.0.1"],
236 )
237 }
238
239 pub fn date() -> Self {
257 Self::with_examples(
258 r"^\d{4}-\d{2}-\d{2}$",
259 "Date in YYYY-MM-DD format (format validation only)",
260 vec!["2023-12-25", "2024-01-01"],
261 )
262 }
263
264 pub fn time() -> Self {
278 Self::with_examples(
279 r"^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$",
280 "Time in HH:MM:SS format",
281 vec!["14:30:00", "09:05:30"],
282 )
283 }
284
285 pub fn is_valid(&self, value: &str) -> bool {
297 regex::Regex::new(&self.pattern)
298 .map(|re| re.is_match(value))
299 .unwrap_or(false)
300 }
301
302 pub fn as_openapi_pattern(&self) -> &str {
313 &self.pattern
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_email_pattern() {
323 let pattern = ValidationPattern::email();
324
325 assert!(pattern.is_valid("user@example.com"));
327 assert!(pattern.is_valid("test.user@example.com"));
328 assert!(pattern.is_valid("user+tag@example.co.uk"));
329 assert!(pattern.is_valid("user_name@example.com"));
330
331 assert!(!pattern.is_valid("invalid-email"));
333 assert!(!pattern.is_valid("@example.com"));
334 assert!(!pattern.is_valid("user@"));
335 assert!(!pattern.is_valid("user@.com"));
336 }
337
338 #[test]
339 fn test_url_pattern() {
340 let pattern = ValidationPattern::url();
341
342 assert!(pattern.is_valid("https://example.com"));
344 assert!(pattern.is_valid("http://example.com"));
345 assert!(pattern.is_valid("https://example.com/path"));
346 assert!(pattern.is_valid("https://sub.example.com"));
347
348 assert!(!pattern.is_valid("not-a-url"));
350 assert!(!pattern.is_valid("ftp://example.com"));
351 assert!(!pattern.is_valid("example.com"));
352 }
353
354 #[test]
355 fn test_uuid_pattern() {
356 let pattern = ValidationPattern::uuid();
357
358 assert!(pattern.is_valid("123e4567-e89b-12d3-a456-426614174000"));
360 assert!(pattern.is_valid("550e8400-e29b-41d4-a716-446655440000"));
361
362 assert!(!pattern.is_valid("not-a-uuid"));
364 assert!(!pattern.is_valid("123e4567-e89b-12d3-a456"));
365 assert!(!pattern.is_valid("123e4567e89b12d3a456426614174000"));
366 }
367
368 #[test]
369 fn test_alphanumeric_pattern() {
370 let pattern = ValidationPattern::alphanumeric();
371
372 assert!(pattern.is_valid("abc123"));
374 assert!(pattern.is_valid("TestUser"));
375 assert!(pattern.is_valid("ABC"));
376 assert!(pattern.is_valid("123"));
377
378 assert!(!pattern.is_valid("user@name"));
380 assert!(!pattern.is_valid("user-name"));
381 assert!(!pattern.is_valid("user_name"));
382 assert!(!pattern.is_valid("user name"));
383 }
384
385 #[test]
386 fn test_slug_pattern() {
387 let pattern = ValidationPattern::slug();
388
389 assert!(pattern.is_valid("my-blog-post"));
391 assert!(pattern.is_valid("article-123"));
392 assert!(pattern.is_valid("simple"));
393
394 assert!(!pattern.is_valid("My Blog Post"));
396 assert!(!pattern.is_valid("article_123"));
397 assert!(!pattern.is_valid("-start-hyphen"));
398 assert!(!pattern.is_valid("end-hyphen-"));
399 }
400
401 #[test]
402 fn test_phone_pattern() {
403 let pattern = ValidationPattern::phone();
404
405 assert!(pattern.is_valid("+1-234-567-8900"));
407 assert!(pattern.is_valid("+81-90-1234-5678"));
408 assert!(pattern.is_valid("+44-20-7946-0958"));
409
410 assert!(!pattern.is_valid("123-456-7890")); assert!(!pattern.is_valid("+1234567890")); assert!(!pattern.is_valid("1-234-567-8900")); }
415
416 #[test]
417 fn test_hex_color_pattern() {
418 let pattern = ValidationPattern::hex_color();
419
420 assert!(pattern.is_valid("#FF5733"));
422 assert!(pattern.is_valid("#fff"));
423 assert!(pattern.is_valid("#000000"));
424 assert!(pattern.is_valid("#ABC"));
425
426 assert!(!pattern.is_valid("FF5733")); assert!(!pattern.is_valid("#GG5733")); assert!(!pattern.is_valid("#FF57")); }
431
432 #[test]
433 fn test_ipv4_pattern() {
434 let pattern = ValidationPattern::ipv4();
435
436 assert!(pattern.is_valid("192.168.1.1"));
438 assert!(pattern.is_valid("10.0.0.1"));
439 assert!(pattern.is_valid("255.255.255.255"));
440 assert!(pattern.is_valid("0.0.0.0"));
441
442 assert!(!pattern.is_valid("256.1.1.1")); assert!(!pattern.is_valid("192.168.1")); assert!(!pattern.is_valid("192.168.1.1.1")); }
447
448 #[test]
449 fn test_date_pattern() {
450 let pattern = ValidationPattern::date();
451
452 assert!(pattern.is_valid("2023-12-25"));
454 assert!(pattern.is_valid("2024-01-01"));
455 assert!(pattern.is_valid("1999-01-31"));
456
457 assert!(!pattern.is_valid("23-12-25")); assert!(!pattern.is_valid("2023/12/25")); assert!(pattern.is_valid("2023-13-01")); }
465
466 #[test]
467 fn test_time_pattern() {
468 let pattern = ValidationPattern::time();
469
470 assert!(pattern.is_valid("14:30:00"));
472 assert!(pattern.is_valid("09:05:30"));
473 assert!(pattern.is_valid("00:00:00"));
474 assert!(pattern.is_valid("23:59:59"));
475
476 assert!(!pattern.is_valid("25:00:00")); assert!(!pattern.is_valid("14:30")); assert!(!pattern.is_valid("14:60:00")); assert!(!pattern.is_valid("14:30:60")); }
482
483 #[test]
484 fn test_custom_pattern() {
485 let pattern = ValidationPattern::new(r"^\d{3}-\d{4}$", "Phone extension");
486
487 assert!(pattern.is_valid("123-4567"));
488 assert!(!pattern.is_valid("1234567"));
489 assert!(!pattern.is_valid("abc-defg"));
490 }
491
492 #[test]
493 fn test_pattern_with_examples() {
494 let pattern = ValidationPattern::with_examples(
495 r"^\d{3}$",
496 "Three digit code",
497 vec!["123", "456", "789"],
498 );
499
500 assert_eq!(pattern.examples.as_ref().unwrap().len(), 3);
501 assert!(
502 pattern
503 .examples
504 .as_ref()
505 .unwrap()
506 .contains(&"123".to_string())
507 );
508 }
509
510 #[test]
511 fn test_as_openapi_pattern() {
512 let pattern = ValidationPattern::email();
513 let openapi_pattern = pattern.as_openapi_pattern();
514
515 assert!(!openapi_pattern.is_empty());
516 assert!(openapi_pattern.starts_with('^'));
517 }
518
519 #[test]
520 fn test_pattern_serialization() {
521 let pattern = ValidationPattern::email();
522 let json = serde_json::to_string(&pattern).unwrap();
523
524 assert!(json.contains("pattern"));
525 assert!(json.contains("description"));
526 assert!(json.contains("examples"));
527 }
528}