Skip to main content

rullst_core/
di.rs

1//! Type-indexed dependency injection container (`rullst::di`).
2//!
3//! Provides type-indexed registration and resolution for services,
4//! repositories, and background handlers without runtime reflection. The
5//! container still performs synchronization and type lookup at runtime.
6
7use axum::{
8    extract::FromRequestParts,
9    http::{StatusCode, request::Parts},
10};
11use std::any::{Any, TypeId};
12use std::collections::HashMap;
13use std::ops::Deref;
14use std::sync::Arc;
15
16/// Strongly-typed error domain for Rullst Dependency Injection.
17#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
18pub enum DiError {
19    /// The requested service type was not registered in the DI Container.
20    #[error("Dependency of type '{0}' is not registered in the DI Container")]
21    NotRegistered(&'static str),
22
23    /// Downcasting the registered service to the target type failed.
24    #[error("Failed to downcast dependency of type '{0}'")]
25    DowncastFailed(&'static str),
26
27    /// Injection constructor failed.
28    #[error("Dependency injection failed: {0}")]
29    InjectionFailed(String),
30}
31
32/// Trait implemented by types that can be automatically injected by Rullst DI.
33pub trait Injectable: Sized + Send + Sync + 'static {
34    /// Factory constructor for the service instance.
35    fn inject(container: &Container) -> Result<Self, DiError>;
36}
37
38/// Thread-safe Dependency Injection Container.
39#[derive(Clone, Default)]
40pub struct Container {
41    services: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
42}
43
44impl Container {
45    /// Creates a new empty DI container.
46    pub fn new() -> Self {
47        Self {
48            services: HashMap::new(),
49        }
50    }
51
52    /// Registers a concrete service instance in the container.
53    pub fn register<T: Send + Sync + 'static>(&mut self, service: T) {
54        self.services.insert(TypeId::of::<T>(), Arc::new(service));
55    }
56
57    /// Registers an `Arc`-wrapped service instance in the container.
58    pub fn register_arc<T: Send + Sync + 'static>(&mut self, service: Arc<T>) {
59        self.services.insert(TypeId::of::<T>(), service);
60    }
61
62    /// Resolves and retrieves a reference-counted service from the container.
63    pub fn resolve<T: Send + Sync + 'static>(&self) -> Result<Arc<T>, DiError> {
64        let type_id = TypeId::of::<T>();
65        if let Some(service) = self.services.get(&type_id) {
66            if let Ok(downcasted) = service.clone().downcast::<T>() {
67                return Ok(downcasted);
68            }
69            return Err(DiError::DowncastFailed(std::any::type_name::<T>()));
70        }
71        Err(DiError::NotRegistered(std::any::type_name::<T>()))
72    }
73}
74
75/// Axum extractor for dependencies managed by Rullst DI Container.
76///
77/// Usage in route handlers:
78/// ```rust,no_run
79/// use axum::Json;
80/// use rullst_core::di::Inject;
81///
82/// #[derive(Clone)]
83/// struct User;
84/// struct UserService;
85///
86/// impl UserService {
87///     async fn list_users(&self) -> Vec<User> { vec![] }
88/// }
89///
90/// async fn index(Inject(user_svc): Inject<UserService>) -> Json<Vec<User>> {
91///     Json(user_svc.list_users().await)
92/// }
93/// ```
94pub struct Inject<T: Send + Sync + 'static>(pub Arc<T>);
95
96impl<T: Send + Sync + 'static> Deref for Inject<T> {
97    type Target = T;
98
99    fn deref(&self) -> &Self::Target {
100        &self.0
101    }
102}
103
104impl<T, S> FromRequestParts<S> for Inject<T>
105where
106    T: Send + Sync + 'static,
107    S: Send + Sync,
108{
109    type Rejection = (StatusCode, String);
110
111    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
112        if let Some(container) = parts.extensions.get::<Arc<Container>>() {
113            match container.resolve::<T>() {
114                Ok(service) => Ok(Inject(service)),
115                Err(err) => Err((StatusCode::INTERNAL_SERVER_ERROR, err.to_string())),
116            }
117        } else if let Some(service) = parts.extensions.get::<Arc<T>>() {
118            Ok(Inject(service.clone()))
119        } else {
120            Err((
121                StatusCode::INTERNAL_SERVER_ERROR,
122                format!(
123                    "Rullst DI Container or Extension for type '{}' not found in request state",
124                    std::any::type_name::<T>()
125                ),
126            ))
127        }
128    }
129}
130
131#[cfg(test)]
132#[allow(clippy::unwrap_used, clippy::expect_used)]
133mod tests {
134    use super::*;
135    use axum::extract::FromRequestParts;
136    use axum::http::Request;
137
138    struct DatabaseService {
139        connection_string: String,
140    }
141
142    struct UserService {
143        db: Arc<DatabaseService>,
144    }
145
146    #[test]
147    fn test_di_container_registration_and_resolution() {
148        let mut container = Container::new();
149        container.register(DatabaseService {
150            connection_string: "sqlite::memory:".to_string(),
151        });
152
153        let db = container.resolve::<DatabaseService>().unwrap();
154        assert_eq!(db.connection_string, "sqlite::memory:");
155
156        let user_svc = UserService { db: db.clone() };
157        container.register(user_svc);
158
159        let resolved_user_svc = container.resolve::<UserService>().unwrap();
160        assert_eq!(resolved_user_svc.db.connection_string, "sqlite::memory:");
161    }
162
163    #[tokio::test]
164    async fn extractor_resolves_container_direct_extension_and_missing_state() {
165        let mut container = Container::new();
166        container.register(DatabaseService {
167            connection_string: "sqlite://container".to_string(),
168        });
169        let mut parts = Request::new(()).into_parts().0;
170        parts.extensions.insert(Arc::new(container));
171
172        let injected = Inject::<DatabaseService>::from_request_parts(&mut parts, &())
173            .await
174            .expect("registered container service");
175        assert_eq!(injected.connection_string, "sqlite://container");
176
177        let direct = Arc::new(DatabaseService {
178            connection_string: "sqlite://extension".to_string(),
179        });
180        let mut direct_parts = Request::new(()).into_parts().0;
181        direct_parts.extensions.insert(direct.clone());
182        let injected = Inject::<DatabaseService>::from_request_parts(&mut direct_parts, &())
183            .await
184            .expect("direct typed extension");
185        assert!(Arc::ptr_eq(&injected.0, &direct));
186
187        let mut missing_parts = Request::new(()).into_parts().0;
188        let Err((status, message)) =
189            Inject::<DatabaseService>::from_request_parts(&mut missing_parts, &()).await
190        else {
191            panic!("missing dependency must fail closed");
192        };
193        assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
194        assert!(message.contains(std::any::type_name::<DatabaseService>()));
195        assert!(!message.contains("sqlite://"));
196    }
197
198    #[test]
199    fn typed_errors_do_not_hide_the_requested_dependency() {
200        let missing = DiError::NotRegistered(std::any::type_name::<DatabaseService>());
201        assert!(missing.to_string().contains("DatabaseService"));
202
203        let injection = DiError::InjectionFailed("invalid service graph".to_string());
204        assert_eq!(
205            injection.to_string(),
206            "Dependency injection failed: invalid service graph"
207        );
208    }
209}