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::{Validate, ValidationResult, Validator};
104#[cfg(feature = "validation")]
105use std::fmt::{self, Debug};
106use std::ops::Deref;
107
108/// Wrapper extractor that auto-validates the inner value after extraction.
109///
110/// `Validated<E>` extracts `E` from the request via `FromRequest`, then calls
111/// `Validate::validate()` on the inner value. If validation fails, the request
112/// is rejected with structured `ValidationErrors`.
113///
114/// Works with any extractor implementing `HasInner` where the inner type
115/// implements `Validate`: `Form<T>`, `Json<T>`, `Query<T>`.
116///
117/// # Examples
118///
119/// ```rust,no_run
120/// # use reinhardt_di::params::{Validated, Form};
121/// # use reinhardt_core::validators::Validate;
122/// // In a server_fn handler:
123/// // async fn login(form: Validated<Form<LoginRequest>>) -> Result<(), String> {
124/// //     let login = form.into_inner().into_inner(); // already validated
125/// //     Ok(())
126/// // }
127/// ```
128#[cfg(feature = "validation")]
129pub struct Validated<T>(T);
130
131#[cfg(feature = "validation")]
132impl<T> Validated<T> {
133	/// Unwrap and return the inner extractor.
134	pub fn into_inner(self) -> T {
135		self.0
136	}
137}
138
139#[cfg(feature = "validation")]
140impl<T> Deref for Validated<T> {
141	type Target = T;
142
143	fn deref(&self) -> &T {
144		&self.0
145	}
146}
147
148#[cfg(feature = "validation")]
149impl<T: Debug> Debug for Validated<T> {
150	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151		f.debug_tuple("Validated").field(&self.0).finish()
152	}
153}
154
155#[cfg(feature = "validation")]
156#[async_trait::async_trait]
157impl<E> super::extract::FromRequest for Validated<E>
158where
159	E: super::extract::FromRequest + super::has_inner::HasInner + Send,
160	E::Inner: Validate,
161{
162	async fn from_request(
163		req: &super::Request,
164		ctx: &super::ParamContext,
165	) -> super::ParamResult<Self> {
166		let extractor = E::from_request(req, ctx).await?;
167		extractor
168			.inner_ref()
169			.validate()
170			.map_err(|errors| super::ParamError::ValidationFailed(Box::new(errors)))?;
171		Ok(Validated(extractor))
172	}
173}
174
175// Feature-gated trait is defined at the end of the file for non-validation builds
176
177/// Validation constraints for a parameter
178#[cfg(feature = "validation")]
179pub struct ValidationConstraints<T> {
180	inner: T,
181	min_length: Option<usize>,
182	max_length: Option<usize>,
183	min_value: Option<String>,
184	max_value: Option<String>,
185	regex: Option<String>,
186	email: bool,
187	url: bool,
188}
189
190#[cfg(feature = "validation")]
191impl<T> ValidationConstraints<T> {
192	/// Add another min_length constraint
193	pub fn min_length(mut self, min: usize) -> Self {
194		self.min_length = Some(min);
195		self
196	}
197
198	/// Add another max_length constraint
199	pub fn max_length(mut self, max: usize) -> Self {
200		self.max_length = Some(max);
201		self
202	}
203
204	/// Add another min_value constraint
205	pub fn min_value<V: ToString>(mut self, min: V) -> Self {
206		self.min_value = Some(min.to_string());
207		self
208	}
209
210	/// Add another max_value constraint
211	pub fn max_value<V: ToString>(mut self, max: V) -> Self {
212		self.max_value = Some(max.to_string());
213		self
214	}
215
216	/// Add regex constraint
217	pub fn regex(mut self, pattern: impl Into<String>) -> Self {
218		self.regex = Some(pattern.into());
219		self
220	}
221
222	/// Add email validation
223	pub fn email(mut self) -> Self {
224		self.email = true;
225		self
226	}
227
228	/// Add URL validation
229	pub fn url(mut self) -> Self {
230		self.url = true;
231		self
232	}
233
234	/// Maximum allowed length for user-supplied regex patterns (in bytes).
235	/// Limits regex complexity to prevent ReDoS attacks via excessively large patterns.
236	const MAX_REGEX_PATTERN_LENGTH: usize = 1024;
237
238	/// Validate a string value against the constraints
239	pub fn validate_string(&self, value: &str) -> ValidationResult<()> {
240		// Length constraints
241		if let Some(min) = self.min_length {
242			reinhardt_core::validators::MinLengthValidator::new(min).validate(value)?;
243		}
244		if let Some(max) = self.max_length {
245			reinhardt_core::validators::MaxLengthValidator::new(max).validate(value)?;
246		}
247
248		// Regex constraint with pattern length limit to prevent ReDoS
249		if let Some(ref pattern) = self.regex {
250			if pattern.len() > Self::MAX_REGEX_PATTERN_LENGTH {
251				return Err(reinhardt_core::validators::ValidationError::Custom(
252					format!(
253						"Regex pattern length {} exceeds maximum allowed length {}",
254						pattern.len(),
255						Self::MAX_REGEX_PATTERN_LENGTH
256					),
257				));
258			}
259			reinhardt_core::validators::RegexValidator::new(pattern)
260				.map_err(|e| {
261					reinhardt_core::validators::ValidationError::Custom(format!(
262						"Invalid regex pattern: {}",
263						e
264					))
265				})?
266				.validate(value)?;
267		}
268
269		// Email constraint
270		if self.email {
271			reinhardt_core::validators::EmailValidator::new().validate(value)?;
272		}
273
274		// URL constraint
275		if self.url {
276			reinhardt_core::validators::UrlValidator::new().validate(value)?;
277		}
278
279		Ok(())
280	}
281
282	/// Validate a numeric value against the constraints
283	pub fn validate_number<N>(&self, value: &N) -> ValidationResult<()>
284	where
285		N: PartialOrd + std::fmt::Display + Clone + std::str::FromStr,
286		<N as std::str::FromStr>::Err: std::fmt::Display,
287	{
288		if let Some(ref min_str) = self.min_value
289			&& let Ok(min) = min_str.parse::<N>()
290		{
291			reinhardt_core::validators::MinValueValidator::new(min).validate(value)?;
292		}
293		if let Some(ref max_str) = self.max_value
294			&& let Ok(max) = max_str.parse::<N>()
295		{
296			reinhardt_core::validators::MaxValueValidator::new(max).validate(value)?;
297		}
298		Ok(())
299	}
300
301	/// Get the inner value
302	pub fn into_inner(self) -> T {
303		self.inner
304	}
305}
306
307#[cfg(feature = "validation")]
308impl<T> Deref for ValidationConstraints<T> {
309	type Target = T;
310
311	fn deref(&self) -> &Self::Target {
312		&self.inner
313	}
314}
315
316// ============================================================================
317// WithValidation Trait (feature-gated)
318// ============================================================================
319
320/// Trait for adding validation constraints to parameters
321///
322/// This trait is enabled with the `validation` feature flag.
323#[cfg(feature = "validation")]
324pub trait WithValidation: Sized {
325	/// Add minimum length constraint
326	fn min_length(self, min: usize) -> ValidationConstraints<Self> {
327		ValidationConstraints {
328			inner: self,
329			min_length: Some(min),
330			max_length: None,
331			min_value: None,
332			max_value: None,
333			regex: None,
334			email: false,
335			url: false,
336		}
337	}
338
339	/// Add maximum length constraint
340	fn max_length(self, max: usize) -> ValidationConstraints<Self> {
341		ValidationConstraints {
342			inner: self,
343			min_length: None,
344			max_length: Some(max),
345			min_value: None,
346			max_value: None,
347			regex: None,
348			email: false,
349			url: false,
350		}
351	}
352
353	/// Add minimum value constraint
354	fn min_value<V: ToString>(self, min: V) -> ValidationConstraints<Self> {
355		ValidationConstraints {
356			inner: self,
357			min_length: None,
358			max_length: None,
359			min_value: Some(min.to_string()),
360			max_value: None,
361			regex: None,
362			email: false,
363			url: false,
364		}
365	}
366
367	/// Add maximum value constraint
368	fn max_value<V: ToString>(self, max: V) -> ValidationConstraints<Self> {
369		ValidationConstraints {
370			inner: self,
371			min_length: None,
372			max_length: None,
373			min_value: None,
374			max_value: Some(max.to_string()),
375			regex: None,
376			email: false,
377			url: false,
378		}
379	}
380
381	/// Add regex pattern constraint
382	fn regex(self, pattern: impl Into<String>) -> ValidationConstraints<Self> {
383		ValidationConstraints {
384			inner: self,
385			min_length: None,
386			max_length: None,
387			min_value: None,
388			max_value: None,
389			regex: Some(pattern.into()),
390			email: false,
391			url: false,
392		}
393	}
394
395	/// Add email validation
396	fn email(self) -> ValidationConstraints<Self> {
397		ValidationConstraints {
398			inner: self,
399			min_length: None,
400			max_length: None,
401			min_value: None,
402			max_value: None,
403			regex: None,
404			email: true,
405			url: false,
406		}
407	}
408
409	/// Add URL validation
410	fn url(self) -> ValidationConstraints<Self> {
411		ValidationConstraints {
412			inner: self,
413			min_length: None,
414			max_length: None,
415			min_value: None,
416			max_value: None,
417			regex: None,
418			email: false,
419			url: true,
420		}
421	}
422}
423
424// WithValidation implementations are provided in their respective modules:
425// - Path<T>: path.rs
426// - Query<T>: query.rs
427// - Form<T>: form.rs
428
429// ============================================================================
430// Type Aliases for Validated Parameters
431// ============================================================================
432
433/// Type alias for validated path parameters
434///
435/// This is a convenience type that wraps a `Path<T>` with validation constraints.
436///
437/// # Examples
438///
439/// ```rust,no_run
440/// # use reinhardt_di::params::{ValidatedPath, WithValidation, Path};
441/// # #[tokio::main]
442/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
443/// # let req = ();
444/// # let ctx = ();
445/// // In your handler:
446/// // async fn handler(
447/// //     // Extract path parameter "id" and validate it
448/// //     id: ValidatedPath<i32>,
449/// // ) {
450/// //     // Use the validated value
451/// //     let value = id.0;
452/// // }
453///
454// Usage pattern:
455// 1. Extract Path<T> from request
456// 2. Apply validation constraints
457// 3. Validate
458/// // let path = Path::<i32>::from_request(req, ctx).await?;
459/// // let validated = path.min_value(1).max_value(100);
460/// // validated.validate_number(&validated.0)?;
461/// # Ok(())
462/// # }
463/// ```
464#[cfg(feature = "validation")]
465pub type ValidatedPath<T> = ValidationConstraints<super::Path<T>>;
466
467/// Type alias for validated query parameters
468///
469/// This is a convenience type that wraps a `Query<T>` with validation constraints.
470#[cfg(feature = "validation")]
471pub type ValidatedQuery<T> = ValidationConstraints<super::Query<T>>;
472
473/// Type alias for validated form parameters
474///
475/// This is a convenience type that wraps a `Form<T>` with validation constraints.
476#[cfg(feature = "validation")]
477pub type ValidatedForm<T> = ValidationConstraints<super::Form<T>>;
478
479// ============================================================================
480// Non-feature-gated versions for testing
481// ============================================================================
482
483/// Validation constraints wrapper for parameter types.
484///
485/// Wraps an extracted parameter with configurable validation rules
486/// including length limits, value ranges, regex patterns, and format checks.
487#[cfg(not(feature = "validation"))]
488pub struct ValidationConstraints<T> {
489	/// The wrapped parameter value.
490	pub inner: T,
491	/// Minimum required string length.
492	pub min_length: Option<usize>,
493	/// Maximum allowed string length.
494	pub max_length: Option<usize>,
495	/// Minimum allowed value (as string for generic comparison).
496	pub min_value: Option<String>,
497	/// Maximum allowed value (as string for generic comparison).
498	pub max_value: Option<String>,
499	/// Regular expression pattern that the value must match.
500	pub regex: Option<String>,
501	/// Whether the value must be a valid email address.
502	pub email: bool,
503	/// Whether the value must be a valid URL.
504	pub url: bool,
505}
506
507#[cfg(not(feature = "validation"))]
508impl<T> ValidationConstraints<T> {
509	/// Sets the minimum string length constraint.
510	pub fn min_length(mut self, min: usize) -> Self {
511		self.min_length = Some(min);
512		self
513	}
514
515	/// Sets the maximum string length constraint.
516	pub fn max_length(mut self, max: usize) -> Self {
517		self.max_length = Some(max);
518		self
519	}
520
521	/// Sets the minimum value constraint.
522	pub fn min_value<V: ToString>(mut self, min: V) -> Self {
523		self.min_value = Some(min.to_string());
524		self
525	}
526
527	/// Sets the maximum value constraint.
528	pub fn max_value<V: ToString>(mut self, max: V) -> Self {
529		self.max_value = Some(max.to_string());
530		self
531	}
532
533	/// Sets a regex pattern that the value must match.
534	pub fn regex(mut self, pattern: impl Into<String>) -> Self {
535		self.regex = Some(pattern.into());
536		self
537	}
538
539	/// Enables email format validation.
540	pub fn email(mut self) -> Self {
541		self.email = true;
542		self
543	}
544
545	/// Enables URL format validation.
546	pub fn url(mut self) -> Self {
547		self.url = true;
548		self
549	}
550
551	/// Maximum allowed length for user-supplied regex patterns (in bytes).
552	/// Limits regex complexity to prevent ReDoS attacks via excessively large patterns.
553	const MAX_REGEX_PATTERN_LENGTH: usize = 1024;
554
555	/// Validates a string value against the configured constraints.
556	pub fn validate_string(&self, value: &str) -> Result<(), String> {
557		if let Some(min) = self.min_length
558			&& value.len() < min
559		{
560			return Err(format!(
561				"String length {} is less than minimum {}",
562				value.len(),
563				min
564			));
565		}
566		if let Some(max) = self.max_length
567			&& value.len() > max
568		{
569			return Err(format!(
570				"String length {} exceeds maximum {}",
571				value.len(),
572				max
573			));
574		}
575		if let Some(ref pattern) = self.regex {
576			if pattern.len() > Self::MAX_REGEX_PATTERN_LENGTH {
577				return Err(format!(
578					"Regex pattern length {} exceeds maximum allowed length {}",
579					pattern.len(),
580					Self::MAX_REGEX_PATTERN_LENGTH
581				));
582			}
583			use regex::Regex;
584			let regex = Regex::new(pattern).map_err(|e| format!("Invalid regex: {}", e))?;
585			if !regex.is_match(value) {
586				return Err(format!("String does not match pattern: {}", pattern));
587			}
588		}
589		if self.email {
590			if !value.contains('@') || !value.contains('.') {
591				return Err("Invalid email format".to_string());
592			}
593			let parts: Vec<&str> = value.split('@').collect();
594			if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
595				return Err("Invalid email format".to_string());
596			}
597		}
598		if self.url && !value.starts_with("http://") && !value.starts_with("https://") {
599			return Err("URL must start with http:// or https://".to_string());
600		}
601		Ok(())
602	}
603
604	/// Validates a numeric value against the configured min/max constraints.
605	pub fn validate_number<N>(&self, value: &N) -> Result<(), String>
606	where
607		N: PartialOrd + std::fmt::Display + Clone + std::str::FromStr,
608		<N as std::str::FromStr>::Err: std::fmt::Display,
609	{
610		if let Some(ref min_str) = self.min_value
611			&& let Ok(min) = min_str.parse::<N>()
612			&& value < &min
613		{
614			return Err(format!("Value {} is less than minimum {}", value, min));
615		}
616		if let Some(ref max_str) = self.max_value
617			&& let Ok(max) = max_str.parse::<N>()
618			&& value > &max
619		{
620			return Err(format!("Value {} exceeds maximum {}", value, max));
621		}
622		Ok(())
623	}
624
625	/// Consumes the wrapper and returns the inner value.
626	pub fn into_inner(self) -> T {
627		self.inner
628	}
629}
630
631#[cfg(not(feature = "validation"))]
632impl<T> Deref for ValidationConstraints<T> {
633	type Target = T;
634
635	fn deref(&self) -> &Self::Target {
636		&self.inner
637	}
638}
639
640/// A `Path<T>` parameter wrapped with validation constraints.
641#[cfg(not(feature = "validation"))]
642pub type ValidatedPath<T> = ValidationConstraints<super::Path<T>>;
643
644/// A `Query<T>` parameter wrapped with validation constraints.
645#[cfg(not(feature = "validation"))]
646pub type ValidatedQuery<T> = ValidationConstraints<super::Query<T>>;
647
648/// A `Form<T>` parameter wrapped with validation constraints.
649#[cfg(not(feature = "validation"))]
650pub type ValidatedForm<T> = ValidationConstraints<super::Form<T>>;
651
652// Implement WithValidation trait for Path and Query
653#[cfg(not(feature = "validation"))]
654impl<T> WithValidation for super::Path<T> {}
655
656#[cfg(not(feature = "validation"))]
657impl<T> WithValidation for super::Query<T> {}
658
659/// Extension trait for adding validation constraints to parameter types.
660#[cfg(not(feature = "validation"))]
661pub trait WithValidation: Sized {
662	/// Creates a `ValidationConstraints` wrapper with a minimum length.
663	fn min_length(self, min: usize) -> ValidationConstraints<Self> {
664		ValidationConstraints {
665			inner: self,
666			min_length: Some(min),
667			max_length: None,
668			min_value: None,
669			max_value: None,
670			regex: None,
671			email: false,
672			url: false,
673		}
674	}
675
676	/// Creates a `ValidationConstraints` wrapper with a maximum length.
677	fn max_length(self, max: usize) -> ValidationConstraints<Self> {
678		ValidationConstraints {
679			inner: self,
680			min_length: None,
681			max_length: Some(max),
682			min_value: None,
683			max_value: None,
684			regex: None,
685			email: false,
686			url: false,
687		}
688	}
689
690	/// Creates a `ValidationConstraints` wrapper with a minimum value.
691	fn min_value<V: ToString>(self, min: V) -> ValidationConstraints<Self> {
692		ValidationConstraints {
693			inner: self,
694			min_length: None,
695			max_length: None,
696			min_value: Some(min.to_string()),
697			max_value: None,
698			regex: None,
699			email: false,
700			url: false,
701		}
702	}
703
704	/// Creates a `ValidationConstraints` wrapper with a maximum value.
705	fn max_value<V: ToString>(self, max: V) -> ValidationConstraints<Self> {
706		ValidationConstraints {
707			inner: self,
708			min_length: None,
709			max_length: None,
710			min_value: None,
711			max_value: Some(max.to_string()),
712			regex: None,
713			email: false,
714			url: false,
715		}
716	}
717
718	/// Creates a `ValidationConstraints` wrapper with a regex pattern.
719	fn regex(self, pattern: impl Into<String>) -> ValidationConstraints<Self> {
720		ValidationConstraints {
721			inner: self,
722			min_length: None,
723			max_length: None,
724			min_value: None,
725			max_value: None,
726			regex: Some(pattern.into()),
727			email: false,
728			url: false,
729		}
730	}
731
732	/// Creates a `ValidationConstraints` wrapper with email format validation.
733	fn email(self) -> ValidationConstraints<Self> {
734		ValidationConstraints {
735			inner: self,
736			min_length: None,
737			max_length: None,
738			min_value: None,
739			max_value: None,
740			regex: None,
741			email: true,
742			url: false,
743		}
744	}
745
746	/// Creates a `ValidationConstraints` wrapper with URL format validation.
747	fn url(self) -> ValidationConstraints<Self> {
748		ValidationConstraints {
749			inner: self,
750			min_length: None,
751			max_length: None,
752			min_value: None,
753			max_value: None,
754			regex: None,
755			email: false,
756			url: true,
757		}
758	}
759}
760
761#[cfg(test)]
762#[cfg(feature = "validation")]
763mod tests {
764	use super::*;
765	use crate::params::extract::FromRequest;
766	use crate::params::{Form, HasInner, ParamContext, ParamError, Path};
767	use bytes::Bytes;
768	use reinhardt_core::validators::{Validate, ValidationError, ValidationErrors};
769	use reinhardt_http::Request;
770	use rstest::rstest;
771
772	// Allow dead_code: fields are accessed via Deserialize derive, not directly in code
773	#[allow(dead_code)]
774	#[derive(Debug, serde::Deserialize)]
775	struct TestForm {
776		email: String,
777	}
778
779	impl Validate for TestForm {
780		fn validate(&self) -> Result<(), ValidationErrors> {
781			let mut errors = ValidationErrors::new();
782			if !self.email.contains('@') {
783				errors.add(
784					"email",
785					ValidationError::Custom("must contain @".to_string()),
786				);
787			}
788			if errors.is_empty() {
789				Ok(())
790			} else {
791				Err(errors)
792			}
793		}
794	}
795
796	fn make_form_request(body: &str) -> Request {
797		use hyper::{HeaderMap, Method, Version, header};
798		let mut headers = HeaderMap::new();
799		headers.insert(
800			header::CONTENT_TYPE,
801			"application/x-www-form-urlencoded".parse().unwrap(),
802		);
803		Request::builder()
804			.method(Method::POST)
805			.uri("/test")
806			.version(Version::HTTP_11)
807			.headers(headers)
808			.body(Bytes::from(body.to_string()))
809			.build()
810			.unwrap()
811	}
812
813	#[rstest]
814	fn test_has_inner_form_valid_data() {
815		// Arrange
816		let form = Form(TestForm {
817			email: "user@example.com".to_string(),
818		});
819
820		// Act
821		let result = form.inner_ref().validate();
822
823		// Assert
824		assert!(result.is_ok());
825	}
826
827	#[rstest]
828	fn test_has_inner_form_invalid_data() {
829		// Arrange
830		let form = Form(TestForm {
831			email: "invalid".to_string(),
832		});
833
834		// Act
835		let result = form.inner_ref().validate();
836
837		// Assert
838		assert!(result.is_err());
839		let errors = result.unwrap_err();
840		assert!(errors.field_errors().contains_key("email"));
841	}
842
843	#[rstest]
844	#[tokio::test]
845	async fn test_validated_form_extraction_valid() {
846		// Arrange
847		let req = make_form_request("email=user%40example.com");
848		let ctx = ParamContext::new();
849
850		// Act
851		let result = Validated::<Form<TestForm>>::from_request(&req, &ctx).await;
852
853		// Assert
854		assert!(result.is_ok());
855		let validated = result.unwrap();
856		assert_eq!(validated.into_inner().0.email, "user@example.com");
857	}
858
859	#[rstest]
860	#[tokio::test]
861	async fn test_validated_form_extraction_invalid() {
862		// Arrange
863		let req = make_form_request("email=invalid");
864		let ctx = ParamContext::new();
865
866		// Act
867		let result = Validated::<Form<TestForm>>::from_request(&req, &ctx).await;
868
869		// Assert
870		assert!(result.is_err());
871		let err = result.unwrap_err();
872		assert!(
873			matches!(err, ParamError::ValidationFailed(_)),
874			"expected ValidationFailed, got: {:?}",
875			err
876		);
877	}
878
879	#[rstest]
880	fn test_validation_constraints_builder() {
881		// Arrange
882		let path = Path(42i32);
883		let constrained = path.min_value(0).max_value(100);
884
885		// Act & Assert
886		assert!(constrained.validate_number(&42).is_ok());
887		assert!(constrained.validate_number(&-1).is_err());
888		assert!(constrained.validate_number(&101).is_err());
889	}
890
891	#[rstest]
892	fn test_string_validation_constraints() {
893		// Arrange
894		let path = Path("test".to_string());
895		let constrained = path.min_length(2).max_length(10);
896
897		// Act & Assert
898		assert!(constrained.validate_string("test").is_ok());
899		assert!(constrained.validate_string("a").is_err());
900		assert!(constrained.validate_string("this is too long").is_err());
901	}
902
903	#[rstest]
904	fn test_regex_pattern_length_limit_rejects_oversized_patterns() {
905		// Arrange
906		let path = Path("test".to_string());
907		let oversized_pattern = "a".repeat(2048);
908		let constrained = path.regex(oversized_pattern);
909
910		// Act
911		let result = constrained.validate_string("test");
912
913		// Assert
914		assert!(result.is_err());
915		let err_msg = format!("{}", result.unwrap_err());
916		assert!(
917			err_msg.contains("exceeds maximum allowed length"),
918			"Expected pattern length error, got: {}",
919			err_msg
920		);
921	}
922
923	#[rstest]
924	fn test_regex_pattern_within_limit_succeeds() {
925		// Arrange
926		let path = Path("hello123".to_string());
927		let valid_pattern = r"^[a-zA-Z0-9]+$";
928		let constrained = path.regex(valid_pattern);
929
930		// Act
931		let result = constrained.validate_string("hello123");
932
933		// Assert
934		assert!(result.is_ok());
935	}
936
937	#[rstest]
938	fn test_regex_pattern_just_over_limit_is_rejected() {
939		// Arrange
940		let path = Path("a".to_string());
941		let pattern_over_limit =
942			"a".repeat(ValidationConstraints::<Path<String>>::MAX_REGEX_PATTERN_LENGTH + 1);
943		let constrained = path.regex(pattern_over_limit);
944
945		// Act
946		let result = constrained.validate_string("a");
947
948		// Assert
949		assert!(result.is_err());
950		let err_msg = format!("{}", result.unwrap_err());
951		assert!(
952			err_msg.contains("exceeds maximum allowed length"),
953			"Expected pattern length error, got: {}",
954			err_msg
955		);
956	}
957}
958
959#[cfg(test)]
960#[cfg(not(feature = "validation"))]
961mod tests_non_validation {
962	use super::*;
963	use crate::params::Path;
964	use rstest::rstest;
965
966	#[rstest]
967	fn test_regex_pattern_length_limit_rejects_oversized_patterns() {
968		// Arrange
969		let path = Path("test".to_string());
970		let oversized_pattern = "a".repeat(2048);
971		let constrained = path.regex(oversized_pattern);
972
973		// Act
974		let result = constrained.validate_string("test");
975
976		// Assert
977		assert!(result.is_err());
978		let err_msg = result.unwrap_err();
979		assert!(
980			err_msg.contains("exceeds maximum allowed length"),
981			"Expected pattern length error, got: {}",
982			err_msg
983		);
984	}
985
986	#[rstest]
987	fn test_regex_pattern_within_limit_succeeds() {
988		// Arrange
989		let path = Path("hello123".to_string());
990		let valid_pattern = r"^[a-zA-Z0-9]+$";
991		let constrained = path.regex(valid_pattern);
992
993		// Act
994		let result = constrained.validate_string("hello123");
995
996		// Assert
997		assert!(result.is_ok());
998	}
999
1000	#[rstest]
1001	fn test_regex_pattern_just_over_limit_is_rejected() {
1002		// Arrange
1003		let path = Path("a".to_string());
1004		let pattern_over_limit =
1005			"a".repeat(ValidationConstraints::<Path<String>>::MAX_REGEX_PATTERN_LENGTH + 1);
1006		let constrained = path.regex(pattern_over_limit);
1007
1008		// Act
1009		let result = constrained.validate_string("a");
1010
1011		// Assert
1012		assert!(result.is_err());
1013		let err_msg = result.unwrap_err();
1014		assert!(
1015			err_msg.contains("exceeds maximum allowed length"),
1016			"Expected pattern length error, got: {}",
1017			err_msg
1018		);
1019	}
1020}