Skip to main content

reinhardt_rest/metadata/
patterns.rs

1//! Regular expression validation patterns for field validation
2
3use serde::{Deserialize, Serialize};
4
5/// Common validation patterns for field validation
6///
7/// # Examples
8///
9/// ```
10/// use reinhardt_rest::metadata::ValidationPattern;
11///
12/// let pattern = ValidationPattern::email();
13/// assert!(pattern.is_valid("user@example.com"));
14/// assert!(!pattern.is_valid("invalid-email"));
15/// ```
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ValidationPattern {
18	/// The regular expression pattern
19	pub pattern: String,
20	/// Description of what the pattern validates
21	pub description: String,
22	/// Example values that match this pattern
23	#[serde(skip_serializing_if = "Option::is_none")]
24	pub examples: Option<Vec<String>>,
25}
26
27impl ValidationPattern {
28	/// Creates a new validation pattern
29	///
30	/// # Examples
31	///
32	/// ```
33	/// use reinhardt_rest::metadata::ValidationPattern;
34	///
35	/// let pattern = ValidationPattern::new(
36	///     r"^\d{3}-\d{4}$",
37	///     "Phone number format (XXX-XXXX)"
38	/// );
39	/// assert_eq!(pattern.pattern, r"^\d{3}-\d{4}$");
40	/// ```
41	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	/// Creates a new validation pattern with examples
50	///
51	/// # Examples
52	///
53	/// ```
54	/// use reinhardt_rest::metadata::ValidationPattern;
55	///
56	/// let pattern = ValidationPattern::with_examples(
57	///     r"^\d{3}-\d{4}$",
58	///     "Phone number format",
59	///     vec!["123-4567", "555-0100"]
60	/// );
61	/// assert_eq!(pattern.examples.as_ref().unwrap().len(), 2);
62	/// ```
63	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	/// Email address pattern
76	///
77	/// # Examples
78	///
79	/// ```
80	/// use reinhardt_rest::metadata::ValidationPattern;
81	///
82	/// let pattern = ValidationPattern::email();
83	/// assert!(pattern.is_valid("user@example.com"));
84	/// assert!(pattern.is_valid("test.user+tag@example.co.uk"));
85	/// assert!(!pattern.is_valid("invalid-email"));
86	/// assert!(!pattern.is_valid("@example.com"));
87	/// ```
88	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	/// URL pattern
97	///
98	/// # Examples
99	///
100	/// ```
101	/// use reinhardt_rest::metadata::ValidationPattern;
102	///
103	/// let pattern = ValidationPattern::url();
104	/// assert!(pattern.is_valid("https://example.com"));
105	/// assert!(pattern.is_valid("http://example.com/path"));
106	/// assert!(!pattern.is_valid("not-a-url"));
107	/// ```
108	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	/// UUID pattern (version 4)
117	///
118	/// # Examples
119	///
120	/// ```
121	/// use reinhardt_rest::metadata::ValidationPattern;
122	///
123	/// let pattern = ValidationPattern::uuid();
124	/// assert!(pattern.is_valid("123e4567-e89b-12d3-a456-426614174000"));
125	/// assert!(!pattern.is_valid("not-a-uuid"));
126	/// ```
127	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	/// Alphanumeric pattern (letters and numbers only)
136	///
137	/// # Examples
138	///
139	/// ```
140	/// use reinhardt_rest::metadata::ValidationPattern;
141	///
142	/// let pattern = ValidationPattern::alphanumeric();
143	/// assert!(pattern.is_valid("abc123"));
144	/// assert!(pattern.is_valid("TestUser123"));
145	/// assert!(!pattern.is_valid("user@name"));
146	/// assert!(!pattern.is_valid("user-name"));
147	/// ```
148	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	/// Slug pattern (URL-friendly identifier)
157	///
158	/// # Examples
159	///
160	/// ```
161	/// use reinhardt_rest::metadata::ValidationPattern;
162	///
163	/// let pattern = ValidationPattern::slug();
164	/// assert!(pattern.is_valid("my-blog-post"));
165	/// assert!(pattern.is_valid("article-123"));
166	/// assert!(!pattern.is_valid("My Blog Post"));
167	/// assert!(!pattern.is_valid("article_123"));
168	/// ```
169	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	/// Phone number pattern (international format)
178	///
179	/// # Examples
180	///
181	/// ```
182	/// use reinhardt_rest::metadata::ValidationPattern;
183	///
184	/// let pattern = ValidationPattern::phone();
185	/// assert!(pattern.is_valid("+1-234-567-8900"));
186	/// assert!(pattern.is_valid("+81-90-1234-5678"));
187	/// assert!(!pattern.is_valid("123-456-7890")); // Missing country code
188	/// ```
189	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	/// Hex color pattern
198	///
199	/// # Examples
200	///
201	/// ```
202	/// use reinhardt_rest::metadata::ValidationPattern;
203	///
204	/// let pattern = ValidationPattern::hex_color();
205	/// assert!(pattern.is_valid("#FF5733"));
206	/// assert!(pattern.is_valid("#fff"));
207	/// assert!(!pattern.is_valid("FF5733")); // Missing #
208	/// assert!(!pattern.is_valid("#GG5733")); // Invalid hex
209	/// ```
210	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	/// IP address pattern (IPv4)
219	///
220	/// # Examples
221	///
222	/// ```
223	/// use reinhardt_rest::metadata::ValidationPattern;
224	///
225	/// let pattern = ValidationPattern::ipv4();
226	/// assert!(pattern.is_valid("192.168.1.1"));
227	/// assert!(pattern.is_valid("10.0.0.1"));
228	/// assert!(!pattern.is_valid("256.1.1.1")); // Invalid range
229	/// assert!(!pattern.is_valid("192.168.1")); // Incomplete
230	/// ```
231	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	/// Date pattern (YYYY-MM-DD)
240	///
241	/// Note: This pattern only validates the format, not the validity of the date itself.
242	/// For example, it will accept "2023-13-01" (invalid month) or "2023-02-30" (invalid day).
243	/// Use a proper date parsing library for full validation.
244	///
245	/// # Examples
246	///
247	/// ```
248	/// use reinhardt_rest::metadata::ValidationPattern;
249	///
250	/// let pattern = ValidationPattern::date();
251	/// assert!(pattern.is_valid("2023-12-25"));
252	/// assert!(pattern.is_valid("2024-01-01"));
253	/// assert!(!pattern.is_valid("23-12-25")); // Wrong format
254	/// assert!(!pattern.is_valid("2023/12/25")); // Wrong separator
255	/// ```
256	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	/// Time pattern (HH:MM:SS)
265	///
266	/// # Examples
267	///
268	/// ```
269	/// use reinhardt_rest::metadata::ValidationPattern;
270	///
271	/// let pattern = ValidationPattern::time();
272	/// assert!(pattern.is_valid("14:30:00"));
273	/// assert!(pattern.is_valid("09:05:30"));
274	/// assert!(!pattern.is_valid("25:00:00")); // Invalid hour
275	/// assert!(!pattern.is_valid("14:30")); // Missing seconds
276	/// ```
277	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	/// Validates if a value matches this pattern
286	///
287	/// # Examples
288	///
289	/// ```
290	/// use reinhardt_rest::metadata::ValidationPattern;
291	///
292	/// let pattern = ValidationPattern::email();
293	/// assert!(pattern.is_valid("user@example.com"));
294	/// assert!(!pattern.is_valid("invalid"));
295	/// ```
296	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	/// Returns the pattern as a string for use in OpenAPI schemas
303	///
304	/// # Examples
305	///
306	/// ```
307	/// use reinhardt_rest::metadata::ValidationPattern;
308	///
309	/// let pattern = ValidationPattern::email();
310	/// assert!(pattern.as_openapi_pattern().starts_with('^'));
311	/// ```
312	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		// Valid emails
326		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		// Invalid emails
332		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		// Valid URLs
343		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		// Invalid URLs
349		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		// Valid UUIDs
359		assert!(pattern.is_valid("123e4567-e89b-12d3-a456-426614174000"));
360		assert!(pattern.is_valid("550e8400-e29b-41d4-a716-446655440000"));
361
362		// Invalid UUIDs
363		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		// Valid alphanumeric
373		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		// Invalid (contains special characters)
379		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		// Valid slugs
390		assert!(pattern.is_valid("my-blog-post"));
391		assert!(pattern.is_valid("article-123"));
392		assert!(pattern.is_valid("simple"));
393
394		// Invalid slugs
395		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		// Valid phone numbers
406		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		// Invalid phone numbers
411		assert!(!pattern.is_valid("123-456-7890")); // Missing +
412		assert!(!pattern.is_valid("+1234567890")); // No separators
413		assert!(!pattern.is_valid("1-234-567-8900")); // Missing +
414	}
415
416	#[test]
417	fn test_hex_color_pattern() {
418		let pattern = ValidationPattern::hex_color();
419
420		// Valid colors
421		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		// Invalid colors
427		assert!(!pattern.is_valid("FF5733")); // Missing #
428		assert!(!pattern.is_valid("#GG5733")); // Invalid hex
429		assert!(!pattern.is_valid("#FF57")); // Wrong length
430	}
431
432	#[test]
433	fn test_ipv4_pattern() {
434		let pattern = ValidationPattern::ipv4();
435
436		// Valid IPs
437		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		// Invalid IPs
443		assert!(!pattern.is_valid("256.1.1.1")); // Out of range
444		assert!(!pattern.is_valid("192.168.1")); // Incomplete
445		assert!(!pattern.is_valid("192.168.1.1.1")); // Too many octets
446	}
447
448	#[test]
449	fn test_date_pattern() {
450		let pattern = ValidationPattern::date();
451
452		// Valid format (not checking date validity)
453		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		// Invalid format
458		assert!(!pattern.is_valid("23-12-25")); // Wrong format
459		assert!(!pattern.is_valid("2023/12/25")); // Wrong separator
460
461		// Note: This pattern only validates format, not date validity
462		// So "2023-13-01" will pass the regex (but is an invalid date)
463		assert!(pattern.is_valid("2023-13-01")); // Invalid month, but valid format
464	}
465
466	#[test]
467	fn test_time_pattern() {
468		let pattern = ValidationPattern::time();
469
470		// Valid times
471		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		// Invalid times
477		assert!(!pattern.is_valid("25:00:00")); // Invalid hour
478		assert!(!pattern.is_valid("14:30")); // Missing seconds
479		assert!(!pattern.is_valid("14:60:00")); // Invalid minute
480		assert!(!pattern.is_valid("14:30:60")); // Invalid second
481	}
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}