cruxi 0.2.0

Minimal, transport-agnostic hexagonal architecture framework
Documentation
//! Validator trait for input validation.
//!
//! Validators check input at both handler (transport format) and
//! repository (domain rules) layers.

use crate::Context;
use crate::error::CodedError;

/// Validates input and returns its configured error type on failure.
///
/// Validators are used at two layers:
/// - **Handler layer**: Transport format validation (e.g., "field required in JSON")
/// - **Repository layer**: Domain rule validation (e.g., "email must be unique")
///
/// # Type Parameters
///
/// - `T`: The type to validate
///
/// # Example
///
/// ```
/// use cruxi::{Context, Validator, ValidatorFn};
///
/// struct CreateUserReq {
///     email: String,
///     age: u8,
/// }
///
/// #[derive(Debug)]
/// enum ValidationError {
///     EmailRequired,
///     EmailInvalid,
/// }
///
/// impl std::fmt::Display for ValidationError {
///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
///         match self {
///             Self::EmailRequired => write!(f, "email is required"),
///             Self::EmailInvalid => write!(f, "email must contain @"),
///         }
///     }
/// }
///
/// impl std::error::Error for ValidationError {}
///
/// let email_validator = ValidatorFn::new(|_ctx: &Context, req: &CreateUserReq| {
///     if req.email.is_empty() {
///         Err(ValidationError::EmailRequired)
///     } else if !req.email.contains('@') {
///         Err(ValidationError::EmailInvalid)
///     } else {
///         Ok(())
///     }
/// });
///
/// let ctx = Context::new();
/// let valid_req = CreateUserReq { email: "test@example.com".into(), age: 25 };
/// assert!(email_validator.validate(&ctx, &valid_req).is_ok());
///
/// let invalid_req = CreateUserReq { email: "invalid".into(), age: 25 };
/// assert!(email_validator.validate(&ctx, &invalid_req).is_err());
/// ```
pub trait Validator<T> {
    /// The error type returned on validation failure.
    type Error: std::error::Error;

    /// Validates the given value.
    ///
    /// Returns `Ok(())` if validation passes, or an error describing the failure.
    ///
    /// # Errors
    ///
    /// Returns an error when validation fails.
    fn validate(&self, ctx: &Context, value: &T) -> Result<(), Self::Error>;
}

/// Adapts a function to the [`Validator`] trait.
///
/// This allows using closures and functions as validators without implementing
/// the trait manually.
///
/// # Example
///
/// ```
/// use cruxi::{Context, Validator, ValidatorFn};
///
/// let positive_validator = ValidatorFn::new(|_ctx: &Context, value: &i32| {
///     if *value > 0 {
///         Ok(())
///     } else {
///         Err(std::io::Error::new(
///             std::io::ErrorKind::InvalidInput,
///             "value must be positive",
///         ))
///     }
/// });
///
/// let ctx = Context::new();
/// assert!(positive_validator.validate(&ctx, &42).is_ok());
/// assert!(positive_validator.validate(&ctx, &-1).is_err());
/// ```
pub struct ValidatorFn<F, E>
where
    E: std::error::Error,
{
    f: F,
    _marker: std::marker::PhantomData<E>,
}

impl<F, E> ValidatorFn<F, E>
where
    E: std::error::Error,
{
    /// Creates a new validator from a function.
    pub fn new<T>(f: F) -> Self
    where
        F: Fn(&Context, &T) -> Result<(), E>,
    {
        Self {
            f,
            _marker: std::marker::PhantomData,
        }
    }
}

impl<F, T, E> Validator<T> for ValidatorFn<F, E>
where
    F: Fn(&Context, &T) -> Result<(), E>,
    E: std::error::Error,
{
    type Error = E;

    fn validate(&self, ctx: &Context, value: &T) -> Result<(), Self::Error> {
        (self.f)(ctx, value)
    }
}

/// A validator that always passes.
///
/// Useful as a default or placeholder.
pub struct PassValidator;

impl<T> Validator<T> for PassValidator {
    type Error = std::convert::Infallible;

    fn validate(&self, _ctx: &Context, _value: &T) -> Result<(), Self::Error> {
        Ok(())
    }
}

/// A validator that always fails with the given error.
///
/// Useful for testing or as a circuit breaker.
pub struct FailValidator {
    error: CodedError,
}

impl FailValidator {
    /// Creates a validator that always fails with the given error.
    #[must_use]
    pub fn new(error: CodedError) -> Self {
        Self { error }
    }
}

impl<T> Validator<T> for FailValidator {
    type Error = CodedError;

    fn validate(&self, _ctx: &Context, _value: &T) -> Result<(), Self::Error> {
        Err(self.error.clone())
    }
}

// Async variants

#[cfg(feature = "async")]
use async_trait::async_trait;

/// Async version of [`Validator`] for use with async runtimes.
///
/// This trait is only available with the `async` feature.
#[cfg(feature = "async")]
#[async_trait]
pub trait AsyncValidator<T>: Send + Sync
where
    T: Send + Sync,
{
    /// The error type returned on validation failure.
    type Error: std::error::Error + Send;

    /// Validates the given value asynchronously.
    ///
    /// # Errors
    ///
    /// Returns an error when validation fails.
    async fn validate(&self, ctx: &Context, value: &T) -> Result<(), Self::Error>;
}

/// Blanket implementation allowing sync validators to be used as async validators.
#[cfg(feature = "async")]
#[async_trait]
impl<V, T> AsyncValidator<T> for V
where
    V: Validator<T> + Send + Sync,
    V::Error: Send,
    T: Send + Sync,
{
    type Error = V::Error;

    async fn validate(&self, ctx: &Context, value: &T) -> Result<(), Self::Error> {
        Validator::validate(self, ctx, value)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn validator_fn_pass() {
        let validator = ValidatorFn::new(|_ctx: &Context, value: &i32| -> Result<(), CodedError> {
            if *value > 0 {
                Ok(())
            } else {
                Err(CodedError::new("INVALID"))
            }
        });

        let result = Validator::validate(&validator, &Context::new(), &42);
        assert!(result.is_ok());
    }

    #[test]
    fn validator_fn_fail() {
        let validator = ValidatorFn::new(|_ctx: &Context, value: &i32| -> Result<(), CodedError> {
            if *value > 0 {
                Ok(())
            } else {
                Err(CodedError::new("INVALID"))
            }
        });

        let result = Validator::validate(&validator, &Context::new(), &-1);
        assert!(result.is_err());
    }

    #[test]
    fn pass_validator() {
        let validator = PassValidator;
        let result: Result<(), std::convert::Infallible> =
            Validator::validate(&validator, &Context::new(), &"anything");
        assert!(result.is_ok());
    }

    #[test]
    fn fail_validator() {
        let validator = FailValidator::new(CodedError::new("ALWAYS_FAIL"));
        let result = Validator::validate(&validator, &Context::new(), &42);
        assert!(result.is_err());
        assert_eq!(
            result.err().map(|e| e.code().to_string()),
            Some("ALWAYS_FAIL".to_string())
        );
    }
}