#[cfg(feature = "validation")]
use reinhardt_core::validators::{Validate, ValidationResult, Validator};
#[cfg(feature = "validation")]
use std::fmt::{self, Debug};
use std::ops::Deref;
#[cfg(feature = "validation")]
pub struct Validated<T>(T);
#[cfg(feature = "validation")]
impl<T> Validated<T> {
pub fn into_inner(self) -> T {
self.0
}
}
#[cfg(feature = "validation")]
impl<T> Deref for Validated<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
#[cfg(feature = "validation")]
impl<T: Debug> Debug for Validated<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Validated").field(&self.0).finish()
}
}
#[cfg(feature = "validation")]
#[async_trait::async_trait]
impl<E> super::extract::FromRequest for Validated<E>
where
E: super::extract::FromRequest + super::has_inner::HasInner + Send,
E::Inner: Validate,
{
async fn from_request(
req: &super::Request,
ctx: &super::ParamContext,
) -> super::ParamResult<Self> {
let extractor = E::from_request(req, ctx).await?;
extractor
.inner_ref()
.validate()
.map_err(|errors| super::ParamError::ValidationFailed(Box::new(errors)))?;
Ok(Validated(extractor))
}
}
#[cfg(feature = "validation")]
pub struct ValidationConstraints<T> {
inner: T,
min_length: Option<usize>,
max_length: Option<usize>,
min_value: Option<String>,
max_value: Option<String>,
regex: Option<String>,
email: bool,
url: bool,
}
#[cfg(feature = "validation")]
impl<T> ValidationConstraints<T> {
pub fn min_length(mut self, min: usize) -> Self {
self.min_length = Some(min);
self
}
pub fn max_length(mut self, max: usize) -> Self {
self.max_length = Some(max);
self
}
pub fn min_value<V: ToString>(mut self, min: V) -> Self {
self.min_value = Some(min.to_string());
self
}
pub fn max_value<V: ToString>(mut self, max: V) -> Self {
self.max_value = Some(max.to_string());
self
}
pub fn regex(mut self, pattern: impl Into<String>) -> Self {
self.regex = Some(pattern.into());
self
}
pub fn email(mut self) -> Self {
self.email = true;
self
}
pub fn url(mut self) -> Self {
self.url = true;
self
}
const MAX_REGEX_PATTERN_LENGTH: usize = 1024;
pub fn validate_string(&self, value: &str) -> ValidationResult<()> {
if let Some(min) = self.min_length {
reinhardt_core::validators::MinLengthValidator::new(min).validate(value)?;
}
if let Some(max) = self.max_length {
reinhardt_core::validators::MaxLengthValidator::new(max).validate(value)?;
}
if let Some(ref pattern) = self.regex {
if pattern.len() > Self::MAX_REGEX_PATTERN_LENGTH {
return Err(reinhardt_core::validators::ValidationError::Custom(
format!(
"Regex pattern length {} exceeds maximum allowed length {}",
pattern.len(),
Self::MAX_REGEX_PATTERN_LENGTH
),
));
}
reinhardt_core::validators::RegexValidator::new(pattern)
.map_err(|e| {
reinhardt_core::validators::ValidationError::Custom(format!(
"Invalid regex pattern: {}",
e
))
})?
.validate(value)?;
}
if self.email {
reinhardt_core::validators::EmailValidator::new().validate(value)?;
}
if self.url {
reinhardt_core::validators::UrlValidator::new().validate(value)?;
}
Ok(())
}
pub fn validate_number<N>(&self, value: &N) -> ValidationResult<()>
where
N: PartialOrd + std::fmt::Display + Clone + std::str::FromStr,
<N as std::str::FromStr>::Err: std::fmt::Display,
{
if let Some(ref min_str) = self.min_value
&& let Ok(min) = min_str.parse::<N>()
{
reinhardt_core::validators::MinValueValidator::new(min).validate(value)?;
}
if let Some(ref max_str) = self.max_value
&& let Ok(max) = max_str.parse::<N>()
{
reinhardt_core::validators::MaxValueValidator::new(max).validate(value)?;
}
Ok(())
}
pub fn into_inner(self) -> T {
self.inner
}
}
#[cfg(feature = "validation")]
impl<T> Deref for ValidationConstraints<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[cfg(feature = "validation")]
pub trait WithValidation: Sized {
fn min_length(self, min: usize) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: Some(min),
max_length: None,
min_value: None,
max_value: None,
regex: None,
email: false,
url: false,
}
}
fn max_length(self, max: usize) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: Some(max),
min_value: None,
max_value: None,
regex: None,
email: false,
url: false,
}
}
fn min_value<V: ToString>(self, min: V) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: None,
min_value: Some(min.to_string()),
max_value: None,
regex: None,
email: false,
url: false,
}
}
fn max_value<V: ToString>(self, max: V) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: None,
min_value: None,
max_value: Some(max.to_string()),
regex: None,
email: false,
url: false,
}
}
fn regex(self, pattern: impl Into<String>) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: None,
min_value: None,
max_value: None,
regex: Some(pattern.into()),
email: false,
url: false,
}
}
fn email(self) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: None,
min_value: None,
max_value: None,
regex: None,
email: true,
url: false,
}
}
fn url(self) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: None,
min_value: None,
max_value: None,
regex: None,
email: false,
url: true,
}
}
}
#[cfg(feature = "validation")]
pub type ValidatedPath<T> = ValidationConstraints<super::Path<T>>;
#[cfg(feature = "validation")]
pub type ValidatedQuery<T> = ValidationConstraints<super::Query<T>>;
#[cfg(feature = "validation")]
pub type ValidatedForm<T> = ValidationConstraints<super::Form<T>>;
#[cfg(not(feature = "validation"))]
pub struct ValidationConstraints<T> {
pub inner: T,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub min_value: Option<String>,
pub max_value: Option<String>,
pub regex: Option<String>,
pub email: bool,
pub url: bool,
}
#[cfg(not(feature = "validation"))]
impl<T> ValidationConstraints<T> {
pub fn min_length(mut self, min: usize) -> Self {
self.min_length = Some(min);
self
}
pub fn max_length(mut self, max: usize) -> Self {
self.max_length = Some(max);
self
}
pub fn min_value<V: ToString>(mut self, min: V) -> Self {
self.min_value = Some(min.to_string());
self
}
pub fn max_value<V: ToString>(mut self, max: V) -> Self {
self.max_value = Some(max.to_string());
self
}
pub fn regex(mut self, pattern: impl Into<String>) -> Self {
self.regex = Some(pattern.into());
self
}
pub fn email(mut self) -> Self {
self.email = true;
self
}
pub fn url(mut self) -> Self {
self.url = true;
self
}
const MAX_REGEX_PATTERN_LENGTH: usize = 1024;
pub fn validate_string(&self, value: &str) -> Result<(), String> {
if let Some(min) = self.min_length
&& value.len() < min
{
return Err(format!(
"String length {} is less than minimum {}",
value.len(),
min
));
}
if let Some(max) = self.max_length
&& value.len() > max
{
return Err(format!(
"String length {} exceeds maximum {}",
value.len(),
max
));
}
if let Some(ref pattern) = self.regex {
if pattern.len() > Self::MAX_REGEX_PATTERN_LENGTH {
return Err(format!(
"Regex pattern length {} exceeds maximum allowed length {}",
pattern.len(),
Self::MAX_REGEX_PATTERN_LENGTH
));
}
use regex::Regex;
let regex = Regex::new(pattern).map_err(|e| format!("Invalid regex: {}", e))?;
if !regex.is_match(value) {
return Err(format!("String does not match pattern: {}", pattern));
}
}
if self.email {
if !value.contains('@') || !value.contains('.') {
return Err("Invalid email format".to_string());
}
let parts: Vec<&str> = value.split('@').collect();
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
return Err("Invalid email format".to_string());
}
}
if self.url && !value.starts_with("http://") && !value.starts_with("https://") {
return Err("URL must start with http:// or https://".to_string());
}
Ok(())
}
pub fn validate_number<N>(&self, value: &N) -> Result<(), String>
where
N: PartialOrd + std::fmt::Display + Clone + std::str::FromStr,
<N as std::str::FromStr>::Err: std::fmt::Display,
{
if let Some(ref min_str) = self.min_value
&& let Ok(min) = min_str.parse::<N>()
&& value < &min
{
return Err(format!("Value {} is less than minimum {}", value, min));
}
if let Some(ref max_str) = self.max_value
&& let Ok(max) = max_str.parse::<N>()
&& value > &max
{
return Err(format!("Value {} exceeds maximum {}", value, max));
}
Ok(())
}
pub fn into_inner(self) -> T {
self.inner
}
}
#[cfg(not(feature = "validation"))]
impl<T> Deref for ValidationConstraints<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[cfg(not(feature = "validation"))]
pub type ValidatedPath<T> = ValidationConstraints<super::Path<T>>;
#[cfg(not(feature = "validation"))]
pub type ValidatedQuery<T> = ValidationConstraints<super::Query<T>>;
#[cfg(not(feature = "validation"))]
pub type ValidatedForm<T> = ValidationConstraints<super::Form<T>>;
#[cfg(not(feature = "validation"))]
impl<T> WithValidation for super::Path<T> {}
#[cfg(not(feature = "validation"))]
impl<T> WithValidation for super::Query<T> {}
#[cfg(not(feature = "validation"))]
pub trait WithValidation: Sized {
fn min_length(self, min: usize) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: Some(min),
max_length: None,
min_value: None,
max_value: None,
regex: None,
email: false,
url: false,
}
}
fn max_length(self, max: usize) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: Some(max),
min_value: None,
max_value: None,
regex: None,
email: false,
url: false,
}
}
fn min_value<V: ToString>(self, min: V) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: None,
min_value: Some(min.to_string()),
max_value: None,
regex: None,
email: false,
url: false,
}
}
fn max_value<V: ToString>(self, max: V) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: None,
min_value: None,
max_value: Some(max.to_string()),
regex: None,
email: false,
url: false,
}
}
fn regex(self, pattern: impl Into<String>) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: None,
min_value: None,
max_value: None,
regex: Some(pattern.into()),
email: false,
url: false,
}
}
fn email(self) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: None,
min_value: None,
max_value: None,
regex: None,
email: true,
url: false,
}
}
fn url(self) -> ValidationConstraints<Self> {
ValidationConstraints {
inner: self,
min_length: None,
max_length: None,
min_value: None,
max_value: None,
regex: None,
email: false,
url: true,
}
}
}
#[cfg(test)]
#[cfg(feature = "validation")]
mod tests {
use super::*;
use crate::params::extract::FromRequest;
use crate::params::{Form, HasInner, ParamContext, ParamError, Path};
use bytes::Bytes;
use reinhardt_core::validators::{Validate, ValidationError, ValidationErrors};
use reinhardt_http::Request;
use rstest::rstest;
#[allow(dead_code)]
#[derive(Debug, serde::Deserialize)]
struct TestForm {
email: String,
}
impl Validate for TestForm {
fn validate(&self) -> Result<(), ValidationErrors> {
let mut errors = ValidationErrors::new();
if !self.email.contains('@') {
errors.add(
"email",
ValidationError::Custom("must contain @".to_string()),
);
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
fn make_form_request(body: &str) -> Request {
use hyper::{HeaderMap, Method, Version, header};
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
"application/x-www-form-urlencoded".parse().unwrap(),
);
Request::builder()
.method(Method::POST)
.uri("/test")
.version(Version::HTTP_11)
.headers(headers)
.body(Bytes::from(body.to_string()))
.build()
.unwrap()
}
#[rstest]
fn test_has_inner_form_valid_data() {
let form = Form(TestForm {
email: "user@example.com".to_string(),
});
let result = form.inner_ref().validate();
assert!(result.is_ok());
}
#[rstest]
fn test_has_inner_form_invalid_data() {
let form = Form(TestForm {
email: "invalid".to_string(),
});
let result = form.inner_ref().validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.field_errors().contains_key("email"));
}
#[rstest]
#[tokio::test]
async fn test_validated_form_extraction_valid() {
let req = make_form_request("email=user%40example.com");
let ctx = ParamContext::new();
let result = Validated::<Form<TestForm>>::from_request(&req, &ctx).await;
assert!(result.is_ok());
let validated = result.unwrap();
assert_eq!(validated.into_inner().0.email, "user@example.com");
}
#[rstest]
#[tokio::test]
async fn test_validated_form_extraction_invalid() {
let req = make_form_request("email=invalid");
let ctx = ParamContext::new();
let result = Validated::<Form<TestForm>>::from_request(&req, &ctx).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, ParamError::ValidationFailed(_)),
"expected ValidationFailed, got: {:?}",
err
);
}
#[rstest]
fn test_validation_constraints_builder() {
let path = Path(42i32);
let constrained = path.min_value(0).max_value(100);
assert!(constrained.validate_number(&42).is_ok());
assert!(constrained.validate_number(&-1).is_err());
assert!(constrained.validate_number(&101).is_err());
}
#[rstest]
fn test_string_validation_constraints() {
let path = Path("test".to_string());
let constrained = path.min_length(2).max_length(10);
assert!(constrained.validate_string("test").is_ok());
assert!(constrained.validate_string("a").is_err());
assert!(constrained.validate_string("this is too long").is_err());
}
#[rstest]
fn test_regex_pattern_length_limit_rejects_oversized_patterns() {
let path = Path("test".to_string());
let oversized_pattern = "a".repeat(2048);
let constrained = path.regex(oversized_pattern);
let result = constrained.validate_string("test");
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("exceeds maximum allowed length"),
"Expected pattern length error, got: {}",
err_msg
);
}
#[rstest]
fn test_regex_pattern_within_limit_succeeds() {
let path = Path("hello123".to_string());
let valid_pattern = r"^[a-zA-Z0-9]+$";
let constrained = path.regex(valid_pattern);
let result = constrained.validate_string("hello123");
assert!(result.is_ok());
}
#[rstest]
fn test_regex_pattern_just_over_limit_is_rejected() {
let path = Path("a".to_string());
let pattern_over_limit =
"a".repeat(ValidationConstraints::<Path<String>>::MAX_REGEX_PATTERN_LENGTH + 1);
let constrained = path.regex(pattern_over_limit);
let result = constrained.validate_string("a");
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("exceeds maximum allowed length"),
"Expected pattern length error, got: {}",
err_msg
);
}
}
#[cfg(test)]
#[cfg(not(feature = "validation"))]
mod tests_non_validation {
use super::*;
use crate::params::Path;
use rstest::rstest;
#[rstest]
fn test_regex_pattern_length_limit_rejects_oversized_patterns() {
let path = Path("test".to_string());
let oversized_pattern = "a".repeat(2048);
let constrained = path.regex(oversized_pattern);
let result = constrained.validate_string("test");
assert!(result.is_err());
let err_msg = result.unwrap_err();
assert!(
err_msg.contains("exceeds maximum allowed length"),
"Expected pattern length error, got: {}",
err_msg
);
}
#[rstest]
fn test_regex_pattern_within_limit_succeeds() {
let path = Path("hello123".to_string());
let valid_pattern = r"^[a-zA-Z0-9]+$";
let constrained = path.regex(valid_pattern);
let result = constrained.validate_string("hello123");
assert!(result.is_ok());
}
#[rstest]
fn test_regex_pattern_just_over_limit_is_rejected() {
let path = Path("a".to_string());
let pattern_over_limit =
"a".repeat(ValidationConstraints::<Path<String>>::MAX_REGEX_PATTERN_LENGTH + 1);
let constrained = path.regex(pattern_over_limit);
let result = constrained.validate_string("a");
assert!(result.is_err());
let err_msg = result.unwrap_err();
assert!(
err_msg.contains("exceeds maximum allowed length"),
"Expected pattern length error, got: {}",
err_msg
);
}
}