cruxi 0.2.0

Minimal, transport-agnostic hexagonal architecture framework
Documentation
//! Provider trait for infrastructure I/O operations.
//!
//! Providers perform the actual I/O: database queries, HTTP calls, message queues, etc.
//! They are the outbound adapters in the hexagonal architecture.

use crate::Context;

/// Performs infrastructure I/O operations.
///
/// Providers are the outbound adapters in the hexagonal architecture. They:
/// - Execute database queries and writes
/// - Make HTTP calls to external APIs
/// - Publish/subscribe to message queues
/// - Perform file system operations
/// - Interact with caches
///
/// Providers should contain no business logic - they are pure I/O.
///
/// # Type Parameters
///
/// - `Req`: The request type
/// - `Resp`: The response type
///
/// # Example
///
/// ```
/// use cruxi::{Context, Provider, ProviderFn};
///
/// struct GetUserByIdReq { id: u64 }
/// struct UserRow { id: u64, name: String, email: String }
///
/// #[derive(Debug)]
/// struct ProviderError;
///
/// impl std::fmt::Display for ProviderError {
///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
///         write!(f, "provider I/O failed")
///     }
/// }
///
/// impl std::error::Error for ProviderError {}
///
/// let user_provider = ProviderFn::new(|ctx: &Context, req: GetUserByIdReq| -> Result<UserRow, ProviderError> {
///     // In a real implementation, this would query a database
///     Ok(UserRow {
///         id: req.id,
///         name: "Alice".to_string(),
///         email: "alice@example.com".to_string(),
///     })
/// });
///
/// let result = user_provider.execute(&Context::new(), GetUserByIdReq { id: 42 });
/// assert!(result.is_ok());
/// ```
pub trait Provider<Req, Resp> {
    /// The error type returned by this provider.
    type Error: std::error::Error;

    /// Executes the I/O operation.
    ///
    /// # Errors
    ///
    /// Returns an error when the underlying I/O operation fails.
    fn execute(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error>;
}

/// Adapts a function to the [`Provider`] trait.
///
/// This allows using closures and functions as providers without implementing
/// the trait manually.
///
/// # Example
///
/// ```
/// use cruxi::{Context, Provider, ProviderFn};
///
/// let provider = ProviderFn::new(|_ctx: &Context, key: String| -> Result<Option<String>, std::convert::Infallible> {
///     // Simulate cache lookup
///     if key == "cached_key" {
///         Ok(Some("cached_value".to_string()))
///     } else {
///         Ok(None)
///     }
/// });
///
/// let result = provider.execute(&Context::new(), "cached_key".to_string());
/// assert_eq!(result.ok(), Some(Some("cached_value".to_string())));
/// ```
pub struct ProviderFn<F, Resp, E>
where
    E: std::error::Error,
{
    f: F,
    _marker: std::marker::PhantomData<(Resp, E)>,
}

impl<F, Resp, E> ProviderFn<F, Resp, E>
where
    E: std::error::Error,
{
    /// Creates a new provider 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> Provider<Req, Resp> for ProviderFn<F, Resp, E>
where
    F: Fn(&Context, Req) -> Result<Resp, E>,
    E: std::error::Error,
{
    type Error = E;

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

// Async variants

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

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

    /// Executes the I/O operation asynchronously.
    ///
    /// # Errors
    ///
    /// Returns an error when the underlying I/O operation fails.
    async fn execute(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error>;
}

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

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

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

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

        let result = Provider::execute(&provider, &Context::new(), 21);
        assert_eq!(result.ok(), Some(42));
    }

    #[test]
    fn provider_fn_error() {
        let provider = ProviderFn::new(|_ctx: &Context, _req: ()| -> Result<(), CodedError> {
            Err(CodedError::new("DB_ERROR").with_reason("connection failed"))
        });

        let result = Provider::execute(&provider, &Context::new(), ());
        assert!(result.is_err());
    }

    #[test]
    fn provider_fn_with_context_deadline() {
        use std::time::Duration;

        let provider = ProviderFn::new(|ctx: &Context, _req: ()| -> Result<bool, CodedError> {
            Ok(ctx.deadline().is_some())
        });

        let ctx = Context::with_timeout(Duration::from_secs(30));
        let result = Provider::execute(&provider, &ctx, ());
        assert_eq!(result.ok(), Some(true));
    }
}