aro-web 1.0.0

HTTP/ADR layer for the Aro web framework
Documentation
//! Dependency injection extractor for Aro actions.
//!
//! [`Dep<T>`] extracts an `Arc<T>` service from [`AroState`],
//! enabling type-safe dependency injection in Axum handlers without macro magic.
//!
//! # Usage in handlers
//!
//! ```ignore
//! async fn list_users(
//!     Dep(repo): Dep<dyn UserRepo>,
//! ) -> Result<Json<Vec<User>>, AroError> {
//!     let users = repo.find_all().await?;
//!     Ok(Json(users))
//! }
//! ```
//!
//! # Unit testing
//!
//! `Dep<T>` can be constructed directly without an HTTP context:
//!
//! ```ignore
//! let dep = Dep(Arc::new(MockUserRepo::new()));
//! let users = dep.find_all().await.unwrap();
//! ```

use std::any::type_name;
use std::ops::Deref;
use std::sync::Arc;

use axum::extract::{FromRef, FromRequestParts};
use axum::http::request::Parts;

use crate::state::AroState;

/// Extractor that provides an `Arc<T>` dependency from [`AroState`].
///
/// Wraps `Arc<T>` and implements `Deref`, so trait methods on `T`
/// can be called directly on `Dep<T>`.
pub struct Dep<T: ?Sized + 'static>(pub Arc<T>);

impl<T: ?Sized + 'static> Deref for Dep<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

impl<T: ?Sized + 'static> Clone for Dep<T> {
    fn clone(&self) -> Self {
        Self(Arc::clone(&self.0))
    }
}

impl<T: ?Sized + 'static> std::fmt::Debug for Dep<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Dep<{}>", type_name::<T>())
    }
}

impl<T: ?Sized + 'static> FromRef<AroState> for Dep<T>
where
    Arc<T>: Send + Sync + Clone + 'static,
{
    #[expect(
        clippy::panic,
        reason = "intentional boot-time crash for missing dependency registration"
    )]
    fn from_ref(state: &AroState) -> Self {
        Self(state.get::<T>().unwrap_or_else(|| {
            panic!(
                "Dep<{}> not registered in AroState. \
                 Call AroState::builder().register::<{}>(...) at startup.",
                type_name::<T>(),
                type_name::<T>(),
            )
        }))
    }
}

impl<T: ?Sized + 'static> FromRequestParts<AroState> for Dep<T>
where
    Arc<T>: Send + Sync + Clone + 'static,
{
    type Rejection = std::convert::Infallible;

    async fn from_request_parts(
        _parts: &mut Parts,
        state: &AroState,
    ) -> Result<Self, Self::Rejection> {
        Ok(Self::from_ref(state))
    }
}

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

    trait UserRepo: Send + Sync {
        fn find_name(&self, id: u64) -> Option<String>;
    }

    struct MockUserRepo {
        users: Vec<(u64, String)>,
    }

    impl UserRepo for MockUserRepo {
        fn find_name(&self, id: u64) -> Option<String> {
            self.users
                .iter()
                .find(|(uid, _)| *uid == id)
                .map(|(_, name)| name.clone())
        }
    }

    trait Notifier: Send + Sync {
        fn notify(&self, msg: &str) -> bool;
    }

    struct MockNotifier;
    impl Notifier for MockNotifier {
        fn notify(&self, _msg: &str) -> bool {
            true
        }
    }

    #[test]
    fn dep_extracts_from_aro_state() {
        let state = AroState::builder()
            .register::<dyn UserRepo>(Arc::new(MockUserRepo {
                users: vec![(1, "Alice".into())],
            }))
            .build();

        let dep = Dep::<dyn UserRepo>::from_ref(&state);
        assert_eq!(dep.find_name(1), Some("Alice".into()));
    }

    #[test]
    #[should_panic(expected = "not registered in AroState")]
    fn missing_dep_panics_with_clear_message() {
        let state = AroState::builder().build();
        let _dep = Dep::<dyn UserRepo>::from_ref(&state);
    }

    #[test]
    fn dep_deref_calls_trait_methods() {
        let dep = Dep::<dyn UserRepo>(Arc::new(MockUserRepo {
            users: vec![(1, "Bob".into())],
        }));
        assert_eq!(dep.find_name(1), Some("Bob".into()));
    }

    #[test]
    fn dep_direct_construction_for_testing() {
        let repo: Arc<dyn UserRepo> = Arc::new(MockUserRepo {
            users: vec![(42, "TestUser".into())],
        });
        let dep = Dep(repo);
        assert_eq!(dep.find_name(42), Some("TestUser".into()));
        assert!(dep.find_name(99).is_none());
    }

    #[test]
    fn dep_is_clone() {
        let repo: Arc<dyn UserRepo> = Arc::new(MockUserRepo {
            users: vec![(1, "Alice".into())],
        });
        let dep = Dep(repo);
        let cloned = dep.clone();
        assert_eq!(cloned.find_name(1), Some("Alice".into()));
    }

    #[test]
    fn multiple_deps_resolve_from_same_state() {
        let state = AroState::builder()
            .register::<dyn UserRepo>(Arc::new(MockUserRepo {
                users: vec![(1, "Alice".into())],
            }))
            .register::<dyn Notifier>(Arc::new(MockNotifier))
            .build();

        let user_dep = Dep::<dyn UserRepo>::from_ref(&state);
        let notifier_dep = Dep::<dyn Notifier>::from_ref(&state);

        assert_eq!(user_dep.find_name(1), Some("Alice".into()));
        assert!(notifier_dep.notify("test"));
    }
}