1#[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#[cfg(feature = "validation")]
129pub struct Validated<T>(T);
130
131#[cfg(feature = "validation")]
132impl<T> Validated<T> {
133 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#[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 pub fn min_length(mut self, min: usize) -> Self {
194 self.min_length = Some(min);
195 self
196 }
197
198 pub fn max_length(mut self, max: usize) -> Self {
200 self.max_length = Some(max);
201 self
202 }
203
204 pub fn min_value<V: ToString>(mut self, min: V) -> Self {
206 self.min_value = Some(min.to_string());
207 self
208 }
209
210 pub fn max_value<V: ToString>(mut self, max: V) -> Self {
212 self.max_value = Some(max.to_string());
213 self
214 }
215
216 pub fn regex(mut self, pattern: impl Into<String>) -> Self {
218 self.regex = Some(pattern.into());
219 self
220 }
221
222 pub fn email(mut self) -> Self {
224 self.email = true;
225 self
226 }
227
228 pub fn url(mut self) -> Self {
230 self.url = true;
231 self
232 }
233
234 const MAX_REGEX_PATTERN_LENGTH: usize = 1024;
237
238 pub fn validate_string(&self, value: &str) -> ValidationResult<()> {
240 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 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 if self.email {
271 reinhardt_core::validators::EmailValidator::new().validate(value)?;
272 }
273
274 if self.url {
276 reinhardt_core::validators::UrlValidator::new().validate(value)?;
277 }
278
279 Ok(())
280 }
281
282 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 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#[cfg(feature = "validation")]
324pub trait WithValidation: Sized {
325 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 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 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 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 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 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 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#[cfg(feature = "validation")]
465pub type ValidatedPath<T> = ValidationConstraints<super::Path<T>>;
466
467#[cfg(feature = "validation")]
471pub type ValidatedQuery<T> = ValidationConstraints<super::Query<T>>;
472
473#[cfg(feature = "validation")]
477pub type ValidatedForm<T> = ValidationConstraints<super::Form<T>>;
478
479#[cfg(not(feature = "validation"))]
488pub struct ValidationConstraints<T> {
489 pub inner: T,
491 pub min_length: Option<usize>,
493 pub max_length: Option<usize>,
495 pub min_value: Option<String>,
497 pub max_value: Option<String>,
499 pub regex: Option<String>,
501 pub email: bool,
503 pub url: bool,
505}
506
507#[cfg(not(feature = "validation"))]
508impl<T> ValidationConstraints<T> {
509 pub fn min_length(mut self, min: usize) -> Self {
511 self.min_length = Some(min);
512 self
513 }
514
515 pub fn max_length(mut self, max: usize) -> Self {
517 self.max_length = Some(max);
518 self
519 }
520
521 pub fn min_value<V: ToString>(mut self, min: V) -> Self {
523 self.min_value = Some(min.to_string());
524 self
525 }
526
527 pub fn max_value<V: ToString>(mut self, max: V) -> Self {
529 self.max_value = Some(max.to_string());
530 self
531 }
532
533 pub fn regex(mut self, pattern: impl Into<String>) -> Self {
535 self.regex = Some(pattern.into());
536 self
537 }
538
539 pub fn email(mut self) -> Self {
541 self.email = true;
542 self
543 }
544
545 pub fn url(mut self) -> Self {
547 self.url = true;
548 self
549 }
550
551 const MAX_REGEX_PATTERN_LENGTH: usize = 1024;
554
555 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 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 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#[cfg(not(feature = "validation"))]
642pub type ValidatedPath<T> = ValidationConstraints<super::Path<T>>;
643
644#[cfg(not(feature = "validation"))]
646pub type ValidatedQuery<T> = ValidationConstraints<super::Query<T>>;
647
648#[cfg(not(feature = "validation"))]
650pub type ValidatedForm<T> = ValidationConstraints<super::Form<T>>;
651
652#[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#[cfg(not(feature = "validation"))]
661pub trait WithValidation: Sized {
662 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 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 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 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 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 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 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)]
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 let form = Form(TestForm {
817 email: "user@example.com".to_string(),
818 });
819
820 let result = form.inner_ref().validate();
822
823 assert!(result.is_ok());
825 }
826
827 #[rstest]
828 fn test_has_inner_form_invalid_data() {
829 let form = Form(TestForm {
831 email: "invalid".to_string(),
832 });
833
834 let result = form.inner_ref().validate();
836
837 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 let req = make_form_request("email=user%40example.com");
848 let ctx = ParamContext::new();
849
850 let result = Validated::<Form<TestForm>>::from_request(&req, &ctx).await;
852
853 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 let req = make_form_request("email=invalid");
864 let ctx = ParamContext::new();
865
866 let result = Validated::<Form<TestForm>>::from_request(&req, &ctx).await;
868
869 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 let path = Path(42i32);
883 let constrained = path.min_value(0).max_value(100);
884
885 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 let path = Path("test".to_string());
895 let constrained = path.min_length(2).max_length(10);
896
897 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 let path = Path("test".to_string());
907 let oversized_pattern = "a".repeat(2048);
908 let constrained = path.regex(oversized_pattern);
909
910 let result = constrained.validate_string("test");
912
913 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 let path = Path("hello123".to_string());
927 let valid_pattern = r"^[a-zA-Z0-9]+$";
928 let constrained = path.regex(valid_pattern);
929
930 let result = constrained.validate_string("hello123");
932
933 assert!(result.is_ok());
935 }
936
937 #[rstest]
938 fn test_regex_pattern_just_over_limit_is_rejected() {
939 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 let result = constrained.validate_string("a");
947
948 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 let path = Path("test".to_string());
970 let oversized_pattern = "a".repeat(2048);
971 let constrained = path.regex(oversized_pattern);
972
973 let result = constrained.validate_string("test");
975
976 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 let path = Path("hello123".to_string());
990 let valid_pattern = r"^[a-zA-Z0-9]+$";
991 let constrained = path.regex(valid_pattern);
992
993 let result = constrained.validate_string("hello123");
995
996 assert!(result.is_ok());
998 }
999
1000 #[rstest]
1001 fn test_regex_pattern_just_over_limit_is_rejected() {
1002 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 let result = constrained.validate_string("a");
1010
1011 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}