Skip to main content

aro_web/
dep.rs

1//! Dependency injection extractor for Aro actions.
2//!
3//! [`Dep<T>`] extracts an `Arc<T>` service from [`AroState`],
4//! enabling type-safe dependency injection in Axum handlers without macro magic.
5//!
6//! # Usage in handlers
7//!
8//! ```ignore
9//! async fn list_users(
10//!     Dep(repo): Dep<dyn UserRepo>,
11//! ) -> Result<Json<Vec<User>>, AroError> {
12//!     let users = repo.find_all().await?;
13//!     Ok(Json(users))
14//! }
15//! ```
16//!
17//! # Unit testing
18//!
19//! `Dep<T>` can be constructed directly without an HTTP context:
20//!
21//! ```ignore
22//! let dep = Dep(Arc::new(MockUserRepo::new()));
23//! let users = dep.find_all().await.unwrap();
24//! ```
25
26use std::any::type_name;
27use std::ops::Deref;
28use std::sync::Arc;
29
30use axum::extract::{FromRef, FromRequestParts};
31use axum::http::request::Parts;
32
33use crate::state::AroState;
34
35/// Extractor that provides an `Arc<T>` dependency from [`AroState`].
36///
37/// Wraps `Arc<T>` and implements `Deref`, so trait methods on `T`
38/// can be called directly on `Dep<T>`.
39pub struct Dep<T: ?Sized + 'static>(pub Arc<T>);
40
41impl<T: ?Sized + 'static> Deref for Dep<T> {
42    type Target = T;
43
44    fn deref(&self) -> &T {
45        &self.0
46    }
47}
48
49impl<T: ?Sized + 'static> Clone for Dep<T> {
50    fn clone(&self) -> Self {
51        Self(Arc::clone(&self.0))
52    }
53}
54
55impl<T: ?Sized + 'static> std::fmt::Debug for Dep<T> {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "Dep<{}>", type_name::<T>())
58    }
59}
60
61impl<T: ?Sized + 'static> FromRef<AroState> for Dep<T>
62where
63    Arc<T>: Send + Sync + Clone + 'static,
64{
65    #[expect(
66        clippy::panic,
67        reason = "intentional boot-time crash for missing dependency registration"
68    )]
69    fn from_ref(state: &AroState) -> Self {
70        Self(state.get::<T>().unwrap_or_else(|| {
71            panic!(
72                "Dep<{}> not registered in AroState. \
73                 Call AroState::builder().register::<{}>(...) at startup.",
74                type_name::<T>(),
75                type_name::<T>(),
76            )
77        }))
78    }
79}
80
81impl<T: ?Sized + 'static> FromRequestParts<AroState> for Dep<T>
82where
83    Arc<T>: Send + Sync + Clone + 'static,
84{
85    type Rejection = std::convert::Infallible;
86
87    async fn from_request_parts(
88        _parts: &mut Parts,
89        state: &AroState,
90    ) -> Result<Self, Self::Rejection> {
91        Ok(Self::from_ref(state))
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::state::AroState;
99
100    trait UserRepo: Send + Sync {
101        fn find_name(&self, id: u64) -> Option<String>;
102    }
103
104    struct MockUserRepo {
105        users: Vec<(u64, String)>,
106    }
107
108    impl UserRepo for MockUserRepo {
109        fn find_name(&self, id: u64) -> Option<String> {
110            self.users
111                .iter()
112                .find(|(uid, _)| *uid == id)
113                .map(|(_, name)| name.clone())
114        }
115    }
116
117    trait Notifier: Send + Sync {
118        fn notify(&self, msg: &str) -> bool;
119    }
120
121    struct MockNotifier;
122    impl Notifier for MockNotifier {
123        fn notify(&self, _msg: &str) -> bool {
124            true
125        }
126    }
127
128    #[test]
129    fn dep_extracts_from_aro_state() {
130        let state = AroState::builder()
131            .register::<dyn UserRepo>(Arc::new(MockUserRepo {
132                users: vec![(1, "Alice".into())],
133            }))
134            .build();
135
136        let dep = Dep::<dyn UserRepo>::from_ref(&state);
137        assert_eq!(dep.find_name(1), Some("Alice".into()));
138    }
139
140    #[test]
141    #[should_panic(expected = "not registered in AroState")]
142    fn missing_dep_panics_with_clear_message() {
143        let state = AroState::builder().build();
144        let _dep = Dep::<dyn UserRepo>::from_ref(&state);
145    }
146
147    #[test]
148    fn dep_deref_calls_trait_methods() {
149        let dep = Dep::<dyn UserRepo>(Arc::new(MockUserRepo {
150            users: vec![(1, "Bob".into())],
151        }));
152        assert_eq!(dep.find_name(1), Some("Bob".into()));
153    }
154
155    #[test]
156    fn dep_direct_construction_for_testing() {
157        let repo: Arc<dyn UserRepo> = Arc::new(MockUserRepo {
158            users: vec![(42, "TestUser".into())],
159        });
160        let dep = Dep(repo);
161        assert_eq!(dep.find_name(42), Some("TestUser".into()));
162        assert!(dep.find_name(99).is_none());
163    }
164
165    #[test]
166    fn dep_is_clone() {
167        let repo: Arc<dyn UserRepo> = Arc::new(MockUserRepo {
168            users: vec![(1, "Alice".into())],
169        });
170        let dep = Dep(repo);
171        let cloned = dep.clone();
172        assert_eq!(cloned.find_name(1), Some("Alice".into()));
173    }
174
175    #[test]
176    fn multiple_deps_resolve_from_same_state() {
177        let state = AroState::builder()
178            .register::<dyn UserRepo>(Arc::new(MockUserRepo {
179                users: vec![(1, "Alice".into())],
180            }))
181            .register::<dyn Notifier>(Arc::new(MockNotifier))
182            .build();
183
184        let user_dep = Dep::<dyn UserRepo>::from_ref(&state);
185        let notifier_dep = Dep::<dyn Notifier>::from_ref(&state);
186
187        assert_eq!(user_dep.find_name(1), Some("Alice".into()));
188        assert!(notifier_dep.notify("test"));
189    }
190}