arcature 0.1.0

Arcature: an opinionated full-stack Rust web framework. One package, batteries included.
Documentation
//! `Resolve<S>` -- typed application resource resolution.
//!
//! The mechanism Arcature uses instead of a runtime DI container. A type
//! implementing `Resolve<S>` can be constructed from application state `S` --
//! cheaply, at compile time, with no `HashMap<TypeId, Box<dyn Any>>`, no
//! runtime reflection, no service locator.
//!
//! The `#[service]` proc-macro generates `impl Resolve<S>` for service
//! types, composing from the `Resolve<S>` impls of their field types.
//! Built-in resources (`Db`) have `Resolve<S>` impls provided by Arcature
//! via their `*FromState` traits.
//!
//! `Resolve<S>` is the construction trait; [`Inject<T>`](super::service::Inject)
//! is the Axum extractor that calls `T::resolve(state)` on the request path.
//!
//! # Lifetime model
//!
//! - **Application resource** (startup): lives in application state `S`.
//! - **Service** (request): cheap composition from `S` via `Resolve<S>`.
//! - **Request value** (current request): derived from request parts.
//!
//! Services are NOT singletons. A `#[service]` is constructed per request
//! from `Arc`/`Clone`-backed application resources. This is cheap because
//! the underlying handles (`Db`, `Cache`, ...) are `Clone` and backed by
//! `Arc` pools.

/// How to construct a value of type `Self` from application state `S`.
///
/// Implementations are generated by `#[service]` for service types and
/// provided by Arcature for built-in resources. The trait is the typed
/// replacement for a runtime DI container: no `TypeId`, no `Any`, no
/// runtime lookup.
///
/// # Example
///
/// ```ignore
/// // Arcature provides this for Db (behind `dx` + `database`):
/// impl<S> Resolve<S> for Db
/// where Db: DbFromState<S>, S: Send + Sync
/// {
///     fn resolve(state: &S) -> Self { Db::db_from_state(state) }
/// }
///
/// // #[service] generates this for LinkService:
/// impl<S> Resolve<S> for LinkService
/// where Db: Resolve<S>, Cache: Resolve<S>, S: Send + Sync
/// {
///     fn resolve(state: &S) -> Self {
///         LinkService {
///             db: Db::resolve(state),
///             cache: Cache::resolve(state),
///         }
///     }
/// }
/// ```
pub trait Resolve<S>: Send + Sync + 'static {
    /// Construct `Self` from application state `S`.
    fn resolve(state: &S) -> Self;
}

// Built-in resource impls: Arcature provides `Resolve<S>` for the
// subsystem handles it owns the `*FromState` trait for. Each is gated
// behind the feature that enables that subsystem.
//
// For resources without a provided `Resolve<S>` impl (e.g. a user-defined
// Stripe client), the application writes a one-line impl:
//
//   impl Resolve<AppState> for StripeClient {
//       fn resolve(state: &AppState) -> Self { state.stripe.clone() }
//   }

/// `Resolve<S>` for `Db` -- delegates to the existing `DbFromState<S>`
/// trait. Behind `dx` + `database` (DbFromState requires Db).
#[cfg(feature = "database")]
impl<S> Resolve<S> for crate::database::Db
where
    crate::database::Db: crate::dx::db_from_state::DbFromState<S>,
    S: Send + Sync,
{
    fn resolve(state: &S) -> Self {
        use crate::dx::db_from_state::DbFromState;
        crate::database::Db::db_from_state(state)
    }
}

/// `Resolve<S>` for `Cache` -- delegates to
/// [`CacheFromState<S>`](crate::dx::from_state::CacheFromState).
#[cfg(feature = "cache")]
impl<S> Resolve<S> for crate::cache::Cache
where
    crate::cache::Cache: crate::dx::from_state::CacheFromState<S>,
    S: Send + Sync,
{
    fn resolve(state: &S) -> Self {
        use crate::dx::from_state::CacheFromState;
        crate::cache::Cache::cache_from_state(state)
    }
}

/// `Resolve<S>` for `Storage` -- delegates to
/// [`StorageFromState<S>`](crate::dx::from_state::StorageFromState).
#[cfg(feature = "storage-fs")]
impl<S> Resolve<S> for crate::storage::Storage
where
    crate::storage::Storage: crate::dx::from_state::StorageFromState<S>,
    S: Send + Sync,
{
    fn resolve(state: &S) -> Self {
        use crate::dx::from_state::StorageFromState;
        crate::storage::Storage::storage_from_state(state)
    }
}

/// `Resolve<S>` for `Mail` -- delegates to
/// [`MailFromState<S>`](crate::dx::from_state::MailFromState).
#[cfg(feature = "mail")]
impl<S> Resolve<S> for crate::mail::Mail
where
    crate::mail::Mail: crate::dx::from_state::MailFromState<S>,
    S: Send + Sync,
{
    fn resolve(state: &S) -> Self {
        use crate::dx::from_state::MailFromState;
        crate::mail::Mail::mail_from_state(state)
    }
}

/// `Resolve<S>` for `Jobs` -- delegates to
/// [`JobsFromState<S>`](crate::dx::from_state::JobsFromState).
#[cfg(feature = "jobs")]
impl<S> Resolve<S> for crate::jobs::Jobs
where
    crate::jobs::Jobs: crate::dx::from_state::JobsFromState<S>,
    S: Send + Sync,
{
    fn resolve(state: &S) -> Self {
        use crate::dx::from_state::JobsFromState;
        crate::jobs::Jobs::jobs_from_state(state)
    }
}

/// `Resolve<S>` for the event `Dispatcher` -- delegates to
/// [`EventsFromState<S>`](crate::dx::from_state::EventsFromState).
#[cfg(feature = "events")]
impl<S> Resolve<S> for crate::events::Dispatcher
where
    crate::events::Dispatcher: crate::dx::from_state::EventsFromState<S>,
    S: Send + Sync,
{
    fn resolve(state: &S) -> Self {
        use crate::dx::from_state::EventsFromState;
        crate::events::Dispatcher::events_from_state(state)
    }
}

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

    // The identity seams: when the state *is* the handle, `Resolve` must
    // already work with no application code at all. These compile-only
    // checks are the regression guard -- a missing `Resolve` impl is a
    // compile error at the `Inject<T>` call site, which is a long way from
    // here.
    #[cfg(feature = "cache")]
    #[test]
    fn cache_resolves_from_itself() {
        fn assert_resolves<T: Resolve<T>>() {}
        assert_resolves::<crate::cache::Cache>();
    }

    #[cfg(feature = "storage-fs")]
    #[test]
    fn storage_resolves_from_itself() {
        fn assert_resolves<T: Resolve<T>>() {}
        assert_resolves::<crate::storage::Storage>();
    }

    #[cfg(feature = "mail")]
    #[test]
    fn mail_resolves_from_itself() {
        fn assert_resolves<T: Resolve<T>>() {}
        assert_resolves::<crate::mail::Mail>();
    }

    #[cfg(feature = "jobs")]
    #[test]
    fn jobs_resolves_from_itself() {
        fn assert_resolves<T: Resolve<T>>() {}
        assert_resolves::<crate::jobs::Jobs>();
    }

    #[cfg(feature = "events")]
    #[test]
    fn events_resolve_from_themselves() {
        fn assert_resolves<T: Resolve<T>>() {}
        assert_resolves::<crate::events::Dispatcher>();
    }

    #[cfg(feature = "database")]
    #[test]
    fn db_resolves_from_itself() {
        fn assert_resolves<T: Resolve<T>>() {}
        assert_resolves::<crate::database::Db>();
    }
}