Skip to main content

reinhardt_di/params/
validation.rs

1//! Validation support for parameter extraction
2//!
3//! This module provides validation capabilities for extracted parameters,
4//! integrating with the `reinhardt-validators` crate.
5//!
6//! # Overview
7//!
8//! Reinhardt provides a powerful validation system that allows you to declaratively
9//! specify constraints on path, query, and form parameters. The validation system
10//! supports:
11//!
12//! - **Length constraints**: `min_length()`, `max_length()`
13//! - **Numeric ranges**: `min_value()`, `max_value()`
14//! - **Pattern matching**: `regex()`
15//! - **Format validation**: `email()`, `url()`
16//! - **Constraint chaining**: Combine multiple constraints with builder pattern
17//!
18//! # Quick Start
19//!
20//! ```rust,no_run
21//! # use reinhardt_di::params::{Path, Query, WithValidation};
22//! # #[tokio::main]
23//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
24//! # let req = ();
25//! # let ctx = ();
26//! // Extract and validate a path parameter
27//! // let id = Path::<i32>::from_request(req, ctx).await?;
28//! // let validated_id = id.min_value(1).max_value(1000);
29//! // validated_id.validate_number(&validated_id.0)?;
30//!
31//! // Extract and validate a query parameter
32//! // let email = Query::<String>::from_request(req, ctx).await?;
33//! // let validated_email = email.min_length(5).max_length(100).email();
34//! // validated_email.validate_string(&validated_email.0)?;
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! # Type Aliases
40//!
41//! For convenience, the module provides type aliases:
42//!
43//! - `ValidatedPath<T>` - Validated path parameters
44//! - `ValidatedQuery<T>` - Validated query parameters
45//! - `ValidatedForm<T>` - Validated form parameters
46//!
47//! # Examples
48//!
49//! ## Numeric Range Validation
50//!
51//! ```rust
52//! # use reinhardt_di::params::{Path, WithValidation};
53//! let age = Path(25);
54//! let validated = age.min_value(0).max_value(120);
55//!
56//! assert!(validated.validate_number(&25).is_ok());
57//! assert!(validated.validate_number(&150).is_err());
58//! assert!(validated.validate_number(&-10).is_err());
59//! ```
60//!
61//! ## Email Validation
62//!
63//! ```rust
64//! # use reinhardt_di::params::{Query, WithValidation};
65//! let email = Query("user@example.com".to_string());
66//! let validated = email.email();
67//!
68//! assert!(validated.validate_string("user@example.com").is_ok());
69//! assert!(validated.validate_string("invalid").is_err());
70//! assert!(validated.validate_string("test@test.com").is_ok());
71//! ```
72//!
73//! ## Combined Constraints
74//!
75//! ```rust
76//! # use reinhardt_di::params::{Path, WithValidation};
77//! let username = Path("alice".to_string());
78//! let validated = username
79//!     .min_length(3)
80//!     .max_length(20)
81//!     .regex(r"^[a-zA-Z0-9_]+$");
82//!
83//! assert!(validated.validate_string(&validated.0).is_ok());
84//! assert!(validated.validate_string("ab").is_err()); // Too short
85//! assert!(validated.validate_string("invalid-chars!").is_err()); // Invalid chars
86//! ```
87//!
88//! # Error Handling
89//!
90//! Validation errors are returned as `ValidationError` from the `reinhardt-validators`
91//! crate, which provides detailed error messages including:
92//!
93//! - The constraint that failed (e.g., "too short", "too large")
94//! - The actual value
95//! - The expected constraint (e.g., minimum, maximum)
96//!
97//! Example error message:
98//! ```text
99//! Validation error for 'email': Length too short: 3 (minimum: 5)
100//! ```
101
102#[cfg(feature = "validation")]
103use reinhardt_core::validators::{ValidationResult, Validator};
104#[cfg(feature = "validation")]
105use std::fmt::{self, Debug};
106use std::ops::Deref;
107
108/// A validated wrapper for extracted parameters
109///
110/// This type wraps an extracted parameter and ensures validation is performed.
111/// It requires the `validation` feature to be enabled.
112///
113/// # Examples
114///
115/// ```rust,no_run
116/// # use reinhardt_di::params::{Path, Validated};
117/// # use reinhardt_core::validators::MinLengthValidator;
118/// # #[tokio::main]
119/// # async fn main() {
120/// // async fn handler(id: Validated<Path<String>, MinLengthValidator>) {
121/// //     // id is guaranteed to meet the validation constraints
122/// //     let value = id.into_inner().0;
123/// // }
124/// # }
125/// ```
126#[cfg(feature = "validation")]
127pub struct Validated<T, V> {
128	inner: T,
129	_validator: std::marker::PhantomData<V>,
130}
131
132#[cfg(feature = "validation")]
133impl<T, V> Validated<T, V> {
134	/// Create a new validated value
135	///
136	/// # Errors
137	///
138	/// Returns an error if validation fails
139	pub fn new<U>(inner: T, validator: &V) -> Result<Self, super::ParamError>
140	where
141		V: Validator<U>,
142		T: AsRef<U>,
143		U: ?Sized,
144	{
145		validator.validate(inner.as_ref()).map_err(|e| {
146			super::ParamError::ValidationError(Box::new(
147				super::ParamErrorContext::new(super::ParamType::Form, e.to_string())
148					.with_field("parameter"),
149			))
150		})?;
151
152		Ok(Self {
153			inner,
154			_validator: std::marker::PhantomData,
155		})
156	}
157
158	/// Unwrap the validated value
159	pub fn into_inner(self) -> T {
160		self.inner
161	}
162}
163
164#[cfg(feature = "validation")]
165impl<T, V> Deref for Validated<T, V> {
166	type Target = T;
167
168	fn deref(&self) -> &Self::Target {
169		&self.inner
170	}
171}
172
173#[cfg(feature = "validation")]
174impl<T: Debug, V> Debug for Validated<T, V> {
175	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176		self.inner.fmt(f)
177	}
178}
179
180// Feature-gated trait is defined at the end of the file for non-validation builds
181
182/// Validation constraints for a parameter
183#[cfg(feature = "validation")]
184pub struct ValidationConstraints<T> {
185	inner: T,
186	min_length: Option<usize>,
187	max_length: Option<usize>,
188	min_value: Option<String>,
189	max_value: Option<String>,
190	regex: Option<String>,
191	email: bool,
192	url: bool,
193}
194
195#[cfg(feature = "validation")]
196impl<T> ValidationConstraints<T> {
197	/// Add another min_length constraint
198	pub fn min_length(mut self, min: usize) -> Self {
199		self.min_length = Some(min);
200		self
201	}
202
203	/// Add another max_length constraint
204	pub fn max_length(mut self, max: usize) -> Self {
205		self.max_length = Some(max);
206		self
207	}
208
209	/// Add another min_value constraint
210	pub fn min_value<V: ToString>(mut self, min: V) -> Self {
211		self.min_value = Some(min.to_string());
212		self
213	}
214
215	/// Add another max_value constraint
216	pub fn max_value<V: ToString>(mut self, max: V) -> Self {
217		self.max_value = Some(max.to_string());
218		self
219	}
220
221	/// Add regex constraint
222	pub fn regex(mut self, pattern: impl Into<String>) -> Self {
223		self.regex = Some(pattern.into());
224		self
225	}
226
227	/// Add email validation
228	pub fn email(mut self) -> Self {
229		self.email = true;
230		self
231	}
232
233	/// Add URL validation
234	pub fn url(mut self) -> Self {
235		self.url = true;
236		self
237	}
238
239	/// Maximum allowed length for user-supplied regex patterns (in bytes).
240	/// Limits regex complexity to prevent ReDoS attacks via excessively large patterns.
241	const MAX_REGEX_PATTERN_LENGTH: usize = 1024;
242
243	/// Validate a string value against the constraints
244	pub fn validate_string(&self, value: &str) -> ValidationResult<()> {
245		// Length constraints
246		if let Some(min) = self.min_length {
247			reinhardt_core::validators::MinLengthValidator::new(min).validate(value)?;
248		}
249		if let Some(max) = self.max_length {
250			reinhardt_core::validators::MaxLengthValidator::new(max).validate(value)?;
251		}
252
253		// Regex constraint with pattern length limit to prevent ReDoS
254		if let Some(ref pattern) = self.regex {
255			if pattern.len() > Self::MAX_REGEX_PATTERN_LENGTH {
256				return Err(reinhardt_core::validators::ValidationError::Custom(
257					format!(
258						"Regex pattern length {} exceeds maximum allowed length {}",
259						pattern.len(),
260						Self::MAX_REGEX_PATTERN_LENGTH
261					),
262				));
263			}
264			reinhardt_core::validators::RegexValidator::new(pattern)
265				.map_err(|e| {
266					reinhardt_core::validators::ValidationError::Custom(format!(
267						"Invalid regex pattern: {}",
268						e
269					))
270				})?
271				.validate(value)?;
272		}
273
274		// Email constraint
275		if self.email {
276			reinhardt_core::validators::EmailValidator::new().validate(value)?;
277		}
278
279		// URL constraint
280		if self.url {
281			reinhardt_core::validators::UrlValidator::new().validate(value)?;
282		}
283
284		Ok(())
285	}
286
287	/// Validate a numeric value against the constraints
288	pub fn validate_number<N>(&self, value: &N) -> ValidationResult<()>
289	where
290		N: PartialOrd + std::fmt::Display + Clone + std::str::FromStr,
291		<N as std::str::FromStr>::Err: std::fmt::Display,
292	{
293		if let Some(ref min_str) = self.min_value
294			&& let Ok(min) = min_str.parse::<N>()
295		{
296			reinhardt_core::validators::MinValueValidator::new(min).validate(value)?;
297		}
298		if let Some(ref max_str) = self.max_value
299			&& let Ok(max) = max_str.parse::<N>()
300		{
301			reinhardt_core::validators::MaxValueValidator::new(max).validate(value)?;
302		}
303		Ok(())
304	}
305
306	/// Get the inner value
307	pub fn into_inner(self) -> T {
308		self.inner
309	}
310}
311
312#[cfg(feature = "validation")]
313impl<T> Deref for ValidationConstraints<T> {
314	type Target = T;
315
316	fn deref(&self) -> &Self::Target {
317		&self.inner
318	}
319}
320
321// ============================================================================
322// WithValidation Trait (feature-gated)
323// ============================================================================
324
325/// Trait for adding validation constraints to parameters
326///
327/// This trait is enabled with the `validation` feature flag.
328#[cfg(feature = "validation")]
329pub trait WithValidation: Sized {
330	/// Add minimum length constraint
331	fn min_length(self, min: usize) -> ValidationConstraints<Self> {
332		ValidationConstraints {
333			inner: self,
334			min_length: Some(min),
335			max_length: None,
336			min_value: None,
337			max_value: None,
338			regex: None,
339			email: false,
340			url: false,
341		}
342	}
343
344	/// Add maximum length constraint
345	fn max_length(self, max: usize) -> ValidationConstraints<Self> {
346		ValidationConstraints {
347			inner: self,
348			min_length: None,
349			max_length: Some(max),
350			min_value: None,
351			max_value: None,
352			regex: None,
353			email: false,
354			url: false,
355		}
356	}
357
358	/// Add minimum value constraint
359	fn min_value<V: ToString>(self, min: V) -> ValidationConstraints<Self> {
360		ValidationConstraints {
361			inner: self,
362			min_length: None,
363			max_length: None,
364			min_value: Some(min.to_string()),
365			max_value: None,
366			regex: None,
367			email: false,
368			url: false,
369		}
370	}
371
372	/// Add maximum value constraint
373	fn max_value<V: ToString>(self, max: V) -> ValidationConstraints<Self> {
374		ValidationConstraints {
375			inner: self,
376			min_length: None,
377			max_length: None,
378			min_value: None,
379			max_value: Some(max.to_string()),
380			regex: None,
381			email: false,
382			url: false,
383		}
384	}
385
386	/// Add regex pattern constraint
387	fn regex(self, pattern: impl Into<String>) -> ValidationConstraints<Self> {
388		ValidationConstraints {
389			inner: self,
390			min_length: None,
391			max_length: None,
392			min_value: None,
393			max_value: None,
394			regex: Some(pattern.into()),
395			email: false,
396			url: false,
397		}
398	}
399
400	/// Add email validation
401	fn email(self) -> ValidationConstraints<Self> {
402		ValidationConstraints {
403			inner: self,
404			min_length: None,
405			max_length: None,
406			min_value: None,
407			max_value: None,
408			regex: None,
409			email: true,
410			url: false,
411		}
412	}
413
414	/// Add URL validation
415	fn url(self) -> ValidationConstraints<Self> {
416		ValidationConstraints {
417			inner: self,
418			min_length: None,
419			max_length: None,
420			min_value: None,
421			max_value: None,
422			regex: None,
423			email: false,
424			url: true,
425		}
426	}
427}
428
429// WithValidation implementations are provided in their respective modules:
430// - Path<T>: path.rs
431// - Query<T>: query.rs
432// - Form<T>: form.rs
433
434// ============================================================================
435// Type Aliases for Validated Parameters
436// ============================================================================
437
438/// Type alias for validated path parameters
439///
440/// This is a convenience type that wraps a `Path<T>` with validation constraints.
441///
442/// # Examples
443///
444/// ```rust,no_run
445/// # use reinhardt_di::params::{ValidatedPath, WithValidation, Path};
446/// # #[tokio::main]
447/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
448/// # let req = ();
449/// # let ctx = ();
450/// // In your handler:
451/// // async fn handler(
452/// //     // Extract path parameter "id" and validate it
453/// //     id: ValidatedPath<i32>,
454/// // ) {
455/// //     // Use the validated value
456/// //     let value = id.0;
457/// // }
458///
459// Usage pattern:
460// 1. Extract Path<T> from request
461// 2. Apply validation constraints
462// 3. Validate
463/// // let path = Path::<i32>::from_request(req, ctx).await?;
464/// // let validated = path.min_value(1).max_value(100);
465/// // validated.validate_number(&validated.0)?;
466/// # Ok(())
467/// # }
468/// ```
469#[cfg(feature = "validation")]
470pub type ValidatedPath<T> = ValidationConstraints<super::Path<T>>;
471
472/// Type alias for validated query parameters
473///
474/// This is a convenience type that wraps a `Query<T>` with validation constraints.
475#[cfg(feature = "validation")]
476pub type ValidatedQuery<T> = ValidationConstraints<super::Query<T>>;
477
478/// Type alias for validated form parameters
479///
480/// This is a convenience type that wraps a `Form<T>` with validation constraints.
481#[cfg(feature = "validation")]
482pub type ValidatedForm<T> = ValidationConstraints<super::Form<T>>;
483
484// ============================================================================
485// Non-feature-gated versions for testing
486// ============================================================================
487
488/// Validation constraints wrapper for parameter types.
489///
490/// Wraps an extracted parameter with configurable validation rules
491/// including length limits, value ranges, regex patterns, and format checks.
492#[cfg(not(feature = "validation"))]
493pub struct ValidationConstraints<T> {
494	/// The wrapped parameter value.
495	pub inner: T,
496	/// Minimum required string length.
497	pub min_length: Option<usize>,
498	/// Maximum allowed string length.
499	pub max_length: Option<usize>,
500	/// Minimum allowed value (as string for generic comparison).
501	pub min_value: Option<String>,
502	/// Maximum allowed value (as string for generic comparison).
503	pub max_value: Option<String>,
504	/// Regular expression pattern that the value must match.
505	pub regex: Option<String>,
506	/// Whether the value must be a valid email address.
507	pub email: bool,
508	/// Whether the value must be a valid URL.
509	pub url: bool,
510}
511
512#[cfg(not(feature = "validation"))]
513impl<T> ValidationConstraints<T> {
514	/// Sets the minimum string length constraint.
515	pub fn min_length(mut self, min: usize) -> Self {
516		self.min_length = Some(min);
517		self
518	}
519
520	/// Sets the maximum string length constraint.
521	pub fn max_length(mut self, max: usize) -> Self {
522		self.max_length = Some(max);
523		self
524	}
525
526	/// Sets the minimum value constraint.
527	pub fn min_value<V: ToString>(mut self, min: V) -> Self {
528		self.min_value = Some(min.to_string());
529		self
530	}
531
532	/// Sets the maximum value constraint.
533	pub fn max_value<V: ToString>(mut self, max: V) -> Self {
534		self.max_value = Some(max.to_string());
535		self
536	}
537
538	/// Sets a regex pattern that the value must match.
539	pub fn regex(mut self, pattern: impl Into<String>) -> Self {
540		self.regex = Some(pattern.into());
541		self
542	}
543
544	/// Enables email format validation.
545	pub fn email(mut self) -> Self {
546		self.email = true;
547		self
548	}
549
550	/// Enables URL format validation.
551	pub fn url(mut self) -> Self {
552		self.url = true;
553		self
554	}
555
556	/// Maximum allowed length for user-supplied regex patterns (in bytes).
557	/// Limits regex complexity to prevent ReDoS attacks via excessively large patterns.
558	const MAX_REGEX_PATTERN_LENGTH: usize = 1024;
559
560	/// Validates a string value against the configured constraints.
561	pub fn validate_string(&self, value: &str) -> Result<(), String> {
562		if let Some(min) = self.min_length
563			&& value.len() < min
564		{
565			return Err(format!(
566				"String length {} is less than minimum {}",
567				value.len(),
568				min
569			));
570		}
571		if let Some(max) = self.max_length
572			&& value.len() > max
573		{
574			return Err(format!(
575				"String length {} exceeds maximum {}",
576				value.len(),
577				max
578			));
579		}
580		if let Some(ref pattern) = self.regex {
581			if pattern.len() > Self::MAX_REGEX_PATTERN_LENGTH {
582				return Err(format!(
583					"Regex pattern length {} exceeds maximum allowed length {}",
584					pattern.len(),
585					Self::MAX_REGEX_PATTERN_LENGTH
586				));
587			}
588			use regex::Regex;
589			let regex = Regex::new(pattern).map_err(|e| format!("Invalid regex: {}", e))?;
590			if !regex.is_match(value) {
591				return Err(format!("String does not match pattern: {}", pattern));
592			}
593		}
594		if self.email {
595			if !value.contains('@') || !value.contains('.') {
596				return Err("Invalid email format".to_string());
597			}
598			let parts: Vec<&str> = value.split('@').collect();
599			if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
600				return Err("Invalid email format".to_string());
601			}
602		}
603		if self.url && !value.starts_with("http://") && !value.starts_with("https://") {
604			return Err("URL must start with http:// or https://".to_string());
605		}
606		Ok(())
607	}
608
609	/// Validates a numeric value against the configured min/max constraints.
610	pub fn validate_number<N>(&self, value: &N) -> Result<(), String>
611	where
612		N: PartialOrd + std::fmt::Display + Clone + std::str::FromStr,
613		<N as std::str::FromStr>::Err: std::fmt::Display,
614	{
615		if let Some(ref min_str) = self.min_value
616			&& let Ok(min) = min_str.parse::<N>()
617			&& value < &min
618		{
619			return Err(format!("Value {} is less than minimum {}", value, min));
620		}
621		if let Some(ref max_str) = self.max_value
622			&& let Ok(max) = max_str.parse::<N>()
623			&& value > &max
624		{
625			return Err(format!("Value {} exceeds maximum {}", value, max));
626		}
627		Ok(())
628	}
629
630	/// Consumes the wrapper and returns the inner value.
631	pub fn into_inner(self) -> T {
632		self.inner
633	}
634}
635
636#[cfg(not(feature = "validation"))]
637impl<T> Deref for ValidationConstraints<T> {
638	type Target = T;
639
640	fn deref(&self) -> &Self::Target {
641		&self.inner
642	}
643}
644
645/// A `Path<T>` parameter wrapped with validation constraints.
646#[cfg(not(feature = "validation"))]
647pub type ValidatedPath<T> = ValidationConstraints<super::Path<T>>;
648
649/// A `Query<T>` parameter wrapped with validation constraints.
650#[cfg(not(feature = "validation"))]
651pub type ValidatedQuery<T> = ValidationConstraints<super::Query<T>>;
652
653/// A `Form<T>` parameter wrapped with validation constraints.
654#[cfg(not(feature = "validation"))]
655pub type ValidatedForm<T> = ValidationConstraints<super::Form<T>>;
656
657// Implement WithValidation trait for Path and Query
658#[cfg(not(feature = "validation"))]
659impl<T> WithValidation for super::Path<T> {}
660
661#[cfg(not(feature = "validation"))]
662impl<T> WithValidation for super::Query<T> {}
663
664/// Extension trait for adding validation constraints to parameter types.
665#[cfg(not(feature = "validation"))]
666pub trait WithValidation: Sized {
667	/// Creates a `ValidationConstraints` wrapper with a minimum length.
668	fn min_length(self, min: usize) -> ValidationConstraints<Self> {
669		ValidationConstraints {
670			inner: self,
671			min_length: Some(min),
672			max_length: None,
673			min_value: None,
674			max_value: None,
675			regex: None,
676			email: false,
677			url: false,
678		}
679	}
680
681	/// Creates a `ValidationConstraints` wrapper with a maximum length.
682	fn max_length(self, max: usize) -> ValidationConstraints<Self> {
683		ValidationConstraints {
684			inner: self,
685			min_length: None,
686			max_length: Some(max),
687			min_value: None,
688			max_value: None,
689			regex: None,
690			email: false,
691			url: false,
692		}
693	}
694
695	/// Creates a `ValidationConstraints` wrapper with a minimum value.
696	fn min_value<V: ToString>(self, min: V) -> ValidationConstraints<Self> {
697		ValidationConstraints {
698			inner: self,
699			min_length: None,
700			max_length: None,
701			min_value: Some(min.to_string()),
702			max_value: None,
703			regex: None,
704			email: false,
705			url: false,
706		}
707	}
708
709	/// Creates a `ValidationConstraints` wrapper with a maximum value.
710	fn max_value<V: ToString>(self, max: V) -> ValidationConstraints<Self> {
711		ValidationConstraints {
712			inner: self,
713			min_length: None,
714			max_length: None,
715			min_value: None,
716			max_value: Some(max.to_string()),
717			regex: None,
718			email: false,
719			url: false,
720		}
721	}
722
723	/// Creates a `ValidationConstraints` wrapper with a regex pattern.
724	fn regex(self, pattern: impl Into<String>) -> ValidationConstraints<Self> {
725		ValidationConstraints {
726			inner: self,
727			min_length: None,
728			max_length: None,
729			min_value: None,
730			max_value: None,
731			regex: Some(pattern.into()),
732			email: false,
733			url: false,
734		}
735	}
736
737	/// Creates a `ValidationConstraints` wrapper with email format validation.
738	fn email(self) -> ValidationConstraints<Self> {
739		ValidationConstraints {
740			inner: self,
741			min_length: None,
742			max_length: None,
743			min_value: None,
744			max_value: None,
745			regex: None,
746			email: true,
747			url: false,
748		}
749	}
750
751	/// Creates a `ValidationConstraints` wrapper with URL format validation.
752	fn url(self) -> ValidationConstraints<Self> {
753		ValidationConstraints {
754			inner: self,
755			min_length: None,
756			max_length: None,
757			min_value: None,
758			max_value: None,
759			regex: None,
760			email: false,
761			url: true,
762		}
763	}
764}
765
766#[cfg(test)]
767#[cfg(feature = "validation")]
768mod tests {
769	use super::*;
770	use crate::params::Path;
771	use rstest::rstest;
772
773	#[rstest]
774	fn test_validation_constraints_builder() {
775		// Arrange
776		let path = Path(42i32);
777		let constrained = path.min_value(0).max_value(100);
778
779		// Act & Assert
780		assert!(constrained.validate_number(&42).is_ok());
781		assert!(constrained.validate_number(&-1).is_err());
782		assert!(constrained.validate_number(&101).is_err());
783	}
784
785	#[rstest]
786	fn test_string_validation_constraints() {
787		// Arrange
788		let path = Path("test".to_string());
789		let constrained = path.min_length(2).max_length(10);
790
791		// Act & Assert
792		assert!(constrained.validate_string("test").is_ok());
793		assert!(constrained.validate_string("a").is_err());
794		assert!(constrained.validate_string("this is too long").is_err());
795	}
796
797	#[rstest]
798	fn test_regex_pattern_length_limit_rejects_oversized_patterns() {
799		// Arrange
800		let path = Path("test".to_string());
801		let oversized_pattern = "a".repeat(2048);
802		let constrained = path.regex(oversized_pattern);
803
804		// Act
805		let result = constrained.validate_string("test");
806
807		// Assert
808		assert!(result.is_err());
809		let err_msg = format!("{}", result.unwrap_err());
810		assert!(
811			err_msg.contains("exceeds maximum allowed length"),
812			"Expected pattern length error, got: {}",
813			err_msg
814		);
815	}
816
817	#[rstest]
818	fn test_regex_pattern_within_limit_succeeds() {
819		// Arrange
820		let path = Path("hello123".to_string());
821		let valid_pattern = r"^[a-zA-Z0-9]+$";
822		let constrained = path.regex(valid_pattern);
823
824		// Act
825		let result = constrained.validate_string("hello123");
826
827		// Assert
828		assert!(result.is_ok());
829	}
830
831	#[rstest]
832	fn test_regex_pattern_just_over_limit_is_rejected() {
833		// Arrange
834		let path = Path("a".to_string());
835		let pattern_over_limit =
836			"a".repeat(ValidationConstraints::<Path<String>>::MAX_REGEX_PATTERN_LENGTH + 1);
837		let constrained = path.regex(pattern_over_limit);
838
839		// Act
840		let result = constrained.validate_string("a");
841
842		// Assert
843		assert!(result.is_err());
844		let err_msg = format!("{}", result.unwrap_err());
845		assert!(
846			err_msg.contains("exceeds maximum allowed length"),
847			"Expected pattern length error, got: {}",
848			err_msg
849		);
850	}
851}
852
853#[cfg(test)]
854#[cfg(not(feature = "validation"))]
855mod tests_non_validation {
856	use super::*;
857	use crate::params::Path;
858	use rstest::rstest;
859
860	#[rstest]
861	fn test_regex_pattern_length_limit_rejects_oversized_patterns() {
862		// Arrange
863		let path = Path("test".to_string());
864		let oversized_pattern = "a".repeat(2048);
865		let constrained = path.regex(oversized_pattern);
866
867		// Act
868		let result = constrained.validate_string("test");
869
870		// Assert
871		assert!(result.is_err());
872		let err_msg = result.unwrap_err();
873		assert!(
874			err_msg.contains("exceeds maximum allowed length"),
875			"Expected pattern length error, got: {}",
876			err_msg
877		);
878	}
879
880	#[rstest]
881	fn test_regex_pattern_within_limit_succeeds() {
882		// Arrange
883		let path = Path("hello123".to_string());
884		let valid_pattern = r"^[a-zA-Z0-9]+$";
885		let constrained = path.regex(valid_pattern);
886
887		// Act
888		let result = constrained.validate_string("hello123");
889
890		// Assert
891		assert!(result.is_ok());
892	}
893
894	#[rstest]
895	fn test_regex_pattern_just_over_limit_is_rejected() {
896		// Arrange
897		let path = Path("a".to_string());
898		let pattern_over_limit =
899			"a".repeat(ValidationConstraints::<Path<String>>::MAX_REGEX_PATTERN_LENGTH + 1);
900		let constrained = path.regex(pattern_over_limit);
901
902		// Act
903		let result = constrained.validate_string("a");
904
905		// Assert
906		assert!(result.is_err());
907		let err_msg = result.unwrap_err();
908		assert!(
909			err_msg.contains("exceeds maximum allowed length"),
910			"Expected pattern length error, got: {}",
911			err_msg
912		);
913	}
914}