cruxi 0.2.0

Minimal, transport-agnostic hexagonal architecture framework
Documentation
//! Handler trait for receiving and processing inbound requests.
//!
//! Handlers are the entry point for inbound requests (HTTP, gRPC, MQTT, etc.).
//! They validate transport format and delegate to services.

use crate::Context;
use crate::service::Service;
use crate::validator::Validator;

/// Receives inbound requests and delegates to a service.
///
/// Handlers are the inbound adapter in the hexagonal architecture. They:
/// - Receive transport-specific requests
/// - Validate transport format (e.g., "field required in JSON")
/// - Delegate to a [`Service`] for business logic
///
/// # Type Parameters
///
/// - `Req`: The request type (should be transport-agnostic)
/// - `Resp`: The response type
///
/// # Example
///
/// ```
/// use cruxi::{Context, Handler, HandlerFn};
///
/// #[derive(Clone)]
/// struct GetUserReq { id: u64 }
/// struct User { name: String }
///
/// #[derive(Debug)]
/// struct DomainError;
///
/// impl std::fmt::Display for DomainError {
///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
///         write!(f, "handler failed")
///     }
/// }
///
/// impl std::error::Error for DomainError {}
///
/// let handler: HandlerFn<_, _, DomainError> = HandlerFn::new(|_ctx, req: GetUserReq| {
///     Ok(User { name: format!("User {}", req.id) })
/// });
///
/// let ctx = Context::new();
/// let result = handler.handle(&ctx, GetUserReq { id: 42 });
/// assert!(result.is_ok());
/// ```
pub trait Handler<Req, Resp> {
    /// The error type returned by this handler.
    type Error: std::error::Error;

    /// Handles the request and returns a response.
    ///
    /// # Errors
    ///
    /// Returns an error when request handling fails.
    fn handle(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error>;
}

/// Adapts a function to the [`Handler`] trait.
///
/// This allows using closures and functions as handlers without implementing
/// the trait manually.
///
/// # Example
///
/// ```
/// use cruxi::{Context, Handler, HandlerFn};
///
/// let handler = HandlerFn::new(|ctx: &Context, req: String| -> Result<String, std::convert::Infallible> {
///     Ok(format!("Hello, {}!", req))
/// });
///
/// let result = handler.handle(&Context::new(), "World".to_string());
/// assert_eq!(result.ok(), Some("Hello, World!".to_string()));
/// ```
#[derive(Clone)]
pub struct HandlerFn<F, Resp, E>
where
    E: std::error::Error,
{
    f: F,
    _marker: std::marker::PhantomData<(Resp, E)>,
}

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

impl<F, Req, Resp, E> Handler<Req, Resp> for HandlerFn<F, Resp, E>
where
    F: Fn(&Context, Req) -> Result<Resp, E>,
    E: std::error::Error,
{
    type Error = E;

    fn handle(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error> {
        (self.f)(ctx, req)
    }
}

/// Error type for [`ValidatingHandler`].
#[derive(Debug)]
pub enum ValidatingHandlerError<VE, SE> {
    /// Validation failed.
    Validation(VE),
    /// Service error.
    Service(SE),
}

impl<VE, SE> std::fmt::Display for ValidatingHandlerError<VE, SE>
where
    VE: std::fmt::Display,
    SE: std::fmt::Display,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Validation(e) => write!(f, "validation error: {e}"),
            Self::Service(e) => write!(f, "service error: {e}"),
        }
    }
}

impl<VE, SE> std::error::Error for ValidatingHandlerError<VE, SE>
where
    VE: std::error::Error + 'static,
    SE: std::error::Error + 'static,
{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Validation(e) => Some(e),
            Self::Service(e) => Some(e),
        }
    }
}

/// A handler that validates requests before delegating to a service.
///
/// This is the primary composite handler in the framework. It:
/// 1. Validates the request using the validator
/// 2. Delegates to the service for business logic
///
/// Use [`crate::PassValidator`] when validation is intentionally a no-op.
///
/// # Example
///
/// ```
/// use cruxi::{Context, Handler, ServiceFn, ValidatingHandler, ValidatorFn};
///
/// #[derive(Clone)]
/// struct CreateUserReq { email: String }
/// struct User { id: u64, email: String }
///
/// #[derive(Debug)]
/// enum DomainError {
///     InvalidEmail,
/// }
///
/// impl std::fmt::Display for DomainError {
///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
///         match self {
///             Self::InvalidEmail => write!(f, "invalid email"),
///         }
///     }
/// }
///
/// impl std::error::Error for DomainError {}
///
/// let validator = ValidatorFn::new(|_ctx: &Context, req: &CreateUserReq| {
///     if req.email.is_empty() {
///         Err(DomainError::InvalidEmail)
///     } else {
///         Ok(())
///     }
/// });
///
/// let service = ServiceFn::new(|_ctx: &Context, req: CreateUserReq| -> Result<User, DomainError> {
///     Ok(User { id: 1, email: req.email })
/// });
///
/// let handler = ValidatingHandler::new(validator, service);
///
/// let ctx = Context::new();
///
/// // Valid request
/// let result = handler.handle(&ctx, CreateUserReq { email: "test@example.com".into() });
/// assert!(result.is_ok());
///
/// // Invalid request
/// let result = handler.handle(&ctx, CreateUserReq { email: "".into() });
/// assert!(result.is_err());
/// ```
///
/// Missing collaborators are compile-time errors:
///
/// ```compile_fail
/// use cruxi::{Context, ValidatorFn, ValidatingHandler};
///
/// let validator = ValidatorFn::new(|_ctx: &Context, _req: &u64| -> Result<(), std::convert::Infallible> {
///     Ok(())
/// });
///
/// // `None` does not implement `Service`, so this does not compile.
/// let _handler = ValidatingHandler::new(validator, None);
/// ```
///
/// ```compile_fail
/// use cruxi::{Context, ServiceFn, ValidatingHandler};
///
/// let service = ServiceFn::new(|_ctx: &Context, req: u64| -> Result<u64, std::convert::Infallible> {
///     Ok(req)
/// });
///
/// // `None` does not implement `Validator`, so this does not compile.
/// let _handler = ValidatingHandler::new(None, service);
/// ```
pub struct ValidatingHandler<V, S, Req, Resp>
where
    V: Validator<Req>,
    S: Service<Req, Resp>,
{
    /// The validator for transport format validation.
    pub validator: V,
    /// The service to delegate to.
    pub service: S,
    _marker: std::marker::PhantomData<(Req, Resp)>,
}

impl<V, S, Req, Resp> ValidatingHandler<V, S, Req, Resp>
where
    V: Validator<Req>,
    S: Service<Req, Resp>,
{
    /// Creates a new validating handler.
    pub fn new(validator: V, service: S) -> Self {
        Self {
            validator,
            service,
            _marker: std::marker::PhantomData,
        }
    }
}

impl<V, S, Req, Resp> Handler<Req, Resp> for ValidatingHandler<V, S, Req, Resp>
where
    V: Validator<Req>,
    V::Error: 'static,
    S: Service<Req, Resp>,
    S::Error: 'static,
{
    type Error = ValidatingHandlerError<V::Error, S::Error>;

    fn handle(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error> {
        self.validator
            .validate(ctx, &req)
            .map_err(ValidatingHandlerError::Validation)?;

        self.service
            .execute(ctx, req)
            .map_err(ValidatingHandlerError::Service)
    }
}

// Async variants

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

/// Async version of [`Handler`] for use with async runtimes.
///
/// This trait is only available with the `async` feature.
#[cfg(feature = "async")]
#[async_trait]
pub trait AsyncHandler<Req, Resp>: Send + Sync
where
    Req: Send,
    Resp: Send,
{
    /// The error type returned by this handler.
    type Error: std::error::Error + Send;

    /// Handles the request asynchronously.
    ///
    /// # Errors
    ///
    /// Returns an error when request handling fails.
    async fn handle(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error>;
}

/// Blanket implementation allowing sync handlers to be used as async handlers.
#[cfg(feature = "async")]
#[async_trait]
impl<H, Req, Resp> AsyncHandler<Req, Resp> for H
where
    H: Handler<Req, Resp> + Send + Sync,
    H::Error: Send,
    Req: Send + 'static,
    Resp: Send,
{
    type Error = H::Error;

    async fn handle(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error> {
        Handler::handle(self, ctx, req)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{CodedError, ServiceFn, ValidatorFn};

    #[test]
    fn handler_fn_basic() {
        let handler =
            HandlerFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> { Ok(req * 2) });

        let result = Handler::handle(&handler, &Context::new(), 21);
        assert_eq!(result.ok(), Some(42));
    }

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

        let service =
            ServiceFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> { Ok(req * 2) });

        let handler = ValidatingHandler::new(validator, service);
        let result = Handler::handle(&handler, &Context::new(), 21);
        assert_eq!(result.ok(), Some(42));
    }

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

        let service =
            ServiceFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> { Ok(req * 2) });

        let handler = ValidatingHandler::new(validator, service);
        let result = Handler::handle(&handler, &Context::new(), -1);
        assert!(result.is_err());
    }

    #[test]
    fn validating_handler_no_validator() {
        let service =
            ServiceFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> { Ok(req * 2) });

        let handler = ValidatingHandler::new(crate::PassValidator, service);
        let result = Handler::handle(&handler, &Context::new(), 21);
        assert_eq!(result.ok(), Some(42));
    }
}