coi_actix_web/lib.rs
1#![deny(missing_docs)]
2
3//! This crate provides a simple Dependency Injection framework for `actix-web`.
4//!
5//! ## Example
6//!
7//! Note that the following example is heavily minified. Files names don't really matter. For a
8//! more involved example, please see the [`coi-actix-sample`] repository.
9//!
10//! [`coi-actix-sample`]: https://github.com/Nashenas88/coi-actix-sample
11//!
12//! In your main binary:
13//! ```rust,ignore
14//! use crate::infrastructure::{RepositoryProvider, PoolProvider};
15//! use crate::service::ServiceProvider;
16//! use coi::container;
17//! use actix_web::{App, HttpServer};
18//!
19//! mod traits;
20//! mod infrastructure;
21//! mod routes;
22//! mod service;
23//!
24//! # let _s = "
25//! fn main() {
26//! # ";
27//! // container! only expects identifiers, so construct this provider outside
28//! let postgres_pool = PoolProvider::<NoTls>::new(/* construct actual pool */);
29//!
30//! // Build your container
31//! let container = container! {
32//! pool => postgres_pool.singleton,
33//! service => ServiceProvider.scoped,
34//! repository => RepositoryProvider.scoped,
35//! };
36//!
37//! HttpServer::new(move || {
38//! App::new()
39//! // Make sure to assign it to `app_data` and not `data`
40//! .app_data(container.clone())
41//! .configure(routes::data::route_config)
42//! })
43//! ...
44//! # let _ = "
45//! }
46//! # ";
47//! ```
48//!
49//! `traits.rs`:
50//! ```rust,ignore
51//! use coi::Inject;
52//!
53//! // Ensure all of your traits inherit from `Inject`
54//! pub trait IService: Inject {
55//! ...
56//! }
57//!
58//! pub trait IRepository: Inject {
59//! ...
60//! }
61//! ```
62//!
63//! `service.rs`
64//! ```rust,ignore
65//! use crate::traits::IService;
66//! use coi::Inject;
67//! use std::sync::Arc;
68//!
69//! // derive `Inject` for all structs that will provide the injectable traits.
70//! #[derive(Inject)]
71//! #[coi(provides pub dyn IService with Service::new(repository))]
72//! struct Service {
73//! #[coi(inject)]
74//! repository: Arc<dyn IRepository>,
75//! }
76//!
77//! impl IService for Service {
78//! ...
79//! }
80//! ```
81//!
82//! > **Note**: See [`coi::Inject`] for more examples on how to use `#[derive(Inject)]`
83//!
84//! [`coi::Inject`]: derive.Inject.html
85//!
86//! `infrastructure.rs`
87//! ```rust,ignore
88//! use crate::traits::IRepository;
89//! use coi::Inject;
90//! use ...::PostgresPool;
91//! #[cfg(feature = "notls")]
92//! use ...::NoTls;
93//! #[cfg(not(feature = "notls"))]
94//! use ...::Tls;
95//!
96//! #[derive(Inject)]
97//! #[coi(provides pub dyn IRepository with Repository::new(pool))]
98//! struct Repository {
99//! #[cfg(feature = "notls")]
100//! #[coi(inject)]
101//! pool: PostgresPool<NoTls>,
102//!
103//! #[cfg(not(feature = "notls"))]
104//! #[coi(inject)]
105//! pool: PostgresPool<Tls>,
106//! }
107//!
108//! impl IRepository for Repository {
109//! ...
110//! }
111//!
112//! #[derive(Inject)]
113//! struct Pool<T> where T: ... {
114//! pool: PostgresPool<T>
115//! }
116//!
117//! #[derive(Provide)]
118//! #[coi(provides pub Pool<T> with Pool::new(self.0.pool))]
119//! struct PoolProvider<T> where T: ... {
120//! pool: PostgresPool<T>
121//! }
122//!
123//! impl<T> PoolProvider<T> where T: ... {
124//! fn new(PostgresPool<T>) -> Self { ... }
125//! }
126//! ```
127//!
128//! `routes.rs`
129//! ```rust,ignore
130//! use crate::service::IService;
131//! use actix_web::{Error, HttpResponse, Responder};
132//! use actix_web::web::{self, ServiceConfig};
133//! use coi_actix_web::inject;
134//! use std::sync::Arc;
135//!
136//! #[inject(coi_crate = "coi")]
137//! async fn get(
138//! id: web::Path<i64>,
139//! #[inject] service: Arc<dyn IService>,
140//! ) -> Result<impl Responder, Error> {
141//! let name: String = service.get(*id).await?;
142//! Ok(HttpResponse::Ok().json(name))
143//! }
144//!
145//! #[inject(coi_crate = "coi")]
146//! async fn get_all(#[inject] service: Arc<dyn IService>) -> Result<impl Responder, Error> {
147//! let data: Vec<String> = service.get_all().await?;
148//! Ok(HttpResponse::Ok().json(data))
149//! }
150//!
151//! pub fn route_config(config: &mut ServiceConfig) {
152//! config.service(
153//! web::scope("/data")
154//! .route("", web::get().to(get_all))
155//! .route("/", web::get().to(get_all))
156//! .route("/{id}", web::get().to(get)),
157//! );
158//! }
159//! ```
160
161use actix_service::ServiceFactory;
162use actix_web::dev::ServiceRequest;
163use actix_web::Error;
164
165/// Extensions to `actix-web`'s `App` struct
166pub trait AppExt {
167 /// A helper extension method to ensure the `Container` is
168 /// properly registered to work with the `inject` attribute macro.
169 ///
170 /// # Examples
171 ///
172 /// ```no_run
173 /// use coi_actix_web::AppExt as _;
174 ///
175 /// // Your general server setup in "main". The name here is different
176 /// #[actix_rt::main]
177 /// async fn main() -> std::io::Result<()> {
178 /// use actix_web::{App, HttpServer};
179 /// use coi::container;
180 ///
181 /// // Construct your coi container with your keys and providers
182 /// // See the coi crate for more details
183 /// let container = container!{
184 /// service => ServiceImplProvider; scoped
185 /// };
186 ///
187 /// HttpServer::new(move || {
188 /// App::new()
189 /// .register_container(container.clone())
190 /// // ^^^^^^^^^^^^^^^^
191 /// .service(get)
192 /// })
193 /// .bind("127.0.0.1:8000")?
194 /// .run()
195 /// .await
196 /// }
197 ///
198 /// # use coi::{Container, Inject, Provide};
199 /// # use std::sync::Arc;
200 /// #
201 /// # struct ServiceImpl;
202 /// #
203 /// # impl Inject for ServiceImpl {}
204 /// #
205 /// # struct ServiceImplProvider;
206 /// #
207 /// # impl Provide for ServiceImplProvider {
208 /// #
209 /// # type Output = ServiceImpl;
210 /// #
211 /// # fn provide(&self, _: &Container) -> coi::Result<Arc<Self::Output>> {
212 /// # Ok(Arc::new(ServiceImpl))
213 /// # }
214 /// # }
215 /// #
216 /// # use actix_web::{get, web, Error, HttpResponse, Responder};
217 /// #
218 /// # // Add the `inject` attribute to the function you want to inject
219 /// # #[get("/{id}")]
220 /// # #[coi_actix_web::inject]
221 /// # async fn get(
222 /// # id: web::Path<u64>,
223 /// # // Add the `inject` field attribute to each attribute you want
224 /// # // injected
225 /// # #[inject] service: Arc<ServiceImpl>
226 /// # ) -> Result<impl Responder, Error> {
227 /// # let _ = service;
228 /// # Ok(HttpResponse::Ok())
229 /// # }
230 ///
231 /// ```
232 fn register_container(self, container: Container) -> Self;
233}
234
235impl<T> AppExt for actix_web::App<T>
236where
237 T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
238{
239 fn register_container(self, container: Container) -> Self {
240 self.app_data(container)
241 }
242}
243
244use coi::{Container, Inject};
245pub use coi_actix_web_derive::*;
246
247use actix_web::dev::Payload;
248use actix_web::error::ErrorInternalServerError;
249use actix_web::{Error as WebError, FromRequest, HttpRequest};
250use futures::future::{err, ok, ready, Ready};
251use std::marker::PhantomData;
252use std::sync::Arc;
253
254#[doc(hidden)]
255pub trait ContainerKey<T>
256where
257 T: Inject + ?Sized,
258{
259 const KEY: &'static str;
260}
261
262#[doc(hidden)]
263pub struct Injected<T, K>(pub T, pub PhantomData<K>);
264
265impl<T, K> Injected<T, K> {
266 #[doc(hidden)]
267 pub fn new(injected: T) -> Self {
268 Self(injected, PhantomData)
269 }
270}
271
272impl<T, K> FromRequest for Injected<Arc<T>, K>
273where
274 T: Inject + ?Sized,
275 K: ContainerKey<T>,
276{
277 type Error = WebError;
278 type Future = Ready<Result<Self, Self::Error>>;
279
280 fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
281 match req.app_data::<Container>() {
282 Some(container) => {
283 let container = container.scoped();
284 ready(
285 container
286 .resolve::<T>(K::KEY)
287 .map(Injected::new)
288 .map_err(ErrorInternalServerError),
289 )
290 }
291 None => err(ErrorInternalServerError("Container not registered")),
292 }
293 }
294}
295
296macro_rules! injected_tuples {
297 ($(($T:ident, $K:ident)),+) => {
298 impl<$($T, $K),+> FromRequest for Injected<($(Arc<$T>),+), ($($K),+)>
299 where $(
300 $T: Inject + ?Sized + 'static,
301 $K: ContainerKey<$T>,
302 )+
303 {
304 type Error = WebError;
305 type Future = Ready<Result<Self, Self::Error>>;
306
307 fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
308 match req.app_data::<Container>() {
309 Some(container) => {
310 let container = container.scoped();
311 ok(Injected::new(($(
312 {
313 let resolved = container.resolve::<$T>(<$K as ContainerKey<$T>>::KEY)
314 .map_err(ErrorInternalServerError);
315 match resolved {
316 Ok(r) => r,
317 Err(e) => return err(e),
318 }
319 },
320 )+)))
321 },
322 None => err(ErrorInternalServerError("Container not registered"))
323 }
324 }
325 }
326 }
327}
328
329injected_tuples!((TA, KA), (TB, KB));
330injected_tuples!((TA, KA), (TB, KB), (TC, KC));
331injected_tuples!((TA, KA), (TB, KB), (TC, KC), (TD, KD));
332injected_tuples!((TA, KA), (TB, KB), (TC, KC), (TD, KD), (TE, KE));
333injected_tuples!((TA, KA), (TB, KB), (TC, KC), (TD, KD), (TE, KE), (TF, KF));
334injected_tuples!(
335 (TA, KA),
336 (TB, KB),
337 (TC, KC),
338 (TD, KD),
339 (TE, KE),
340 (TF, KF),
341 (TG, KG)
342);
343injected_tuples!(
344 (TA, KA),
345 (TB, KB),
346 (TC, KC),
347 (TD, KD),
348 (TE, KE),
349 (TF, KF),
350 (TG, KG),
351 (TH, KH)
352);
353injected_tuples!(
354 (TA, KA),
355 (TB, KB),
356 (TC, KC),
357 (TD, KD),
358 (TE, KE),
359 (TF, KF),
360 (TG, KG),
361 (TH, KH),
362 (TI, KI)
363);
364injected_tuples!(
365 (TA, KA),
366 (TB, KB),
367 (TC, KC),
368 (TD, KD),
369 (TE, KE),
370 (TF, KF),
371 (TG, KG),
372 (TH, KH),
373 (TI, KI),
374 (TJ, KJ)
375);
376injected_tuples!(
377 (TA, KA),
378 (TB, KB),
379 (TC, KC),
380 (TD, KD),
381 (TE, KE),
382 (TF, KF),
383 (TG, KG),
384 (TH, KH),
385 (TI, KI),
386 (TJ, KJ),
387 (TK, KK)
388);