testkit_core/handlers/
boxed.rs

1use std::fmt::Debug;
2use std::future::Future;
3use std::pin::Pin;
4
5use crate::DatabasePool;
6use crate::TestContext;
7use crate::handlers::TransactionHandler;
8use crate::testdb::DatabaseBackend;
9use crate::testdb::DatabaseConfig;
10use async_trait::async_trait;
11
12// Type aliases to simplify complex types
13/// Type for a boxed setup function that can be executed on a database pool connection
14pub type BoxedSetupFn<DB> = Box<
15    dyn for<'a> FnOnce(
16            &'a mut <<DB as DatabaseBackend>::Pool as crate::DatabasePool>::Connection,
17        ) -> Pin<
18            Box<dyn Future<Output = Result<(), <DB as DatabaseBackend>::Error>> + Send + 'a>,
19        > + Send
20        + Sync,
21>;
22
23/// Type for a boxed transaction function that can be executed on a database connection
24pub type BoxedTransactionFn<DB> = Box<
25    dyn for<'a> FnOnce(
26            &'a mut <DB as DatabaseBackend>::Connection,
27        ) -> Pin<
28            Box<dyn Future<Output = Result<(), <DB as DatabaseBackend>::Error>> + Send + 'a>,
29        > + Send
30        + Sync,
31>;
32
33/// Entry point for database operations with automatic boxing of closures
34///
35/// This provides functionality for database operations with automatic boxing
36/// of future closures to solve lifetime issues. Use the `boxed_async!` macro
37/// to easily create boxed async blocks.
38pub struct BoxedDatabaseEntryPoint<DB>
39where
40    DB: DatabaseBackend + Send + Sync + Debug + 'static,
41{
42    backend: DB,
43}
44
45/// Handler that stores a setup function
46pub struct BoxedSetupHandler<DB>
47where
48    DB: DatabaseBackend + Send + Sync + Debug + 'static,
49{
50    backend: DB,
51    setup_fn: BoxedSetupFn<DB>,
52}
53
54/// Handler that stores both setup and transaction functions
55pub struct BoxedTransactionHandler<DB>
56where
57    DB: DatabaseBackend + Send + Sync + Debug + 'static,
58{
59    backend: DB,
60    setup_fn: BoxedSetupFn<DB>,
61    transaction_fn: BoxedTransactionFn<DB>,
62}
63
64/// Handler that stores just a transaction function without setup
65pub struct BoxedTransactionOnlyHandler<DB>
66where
67    DB: DatabaseBackend + Send + Sync + Debug + 'static,
68{
69    backend: DB,
70    transaction_fn: BoxedTransactionFn<DB>,
71}
72
73impl<DB> BoxedDatabaseEntryPoint<DB>
74where
75    DB: DatabaseBackend + Send + Sync + Debug + 'static,
76{
77    /// Create a new entry point with the given backend
78    pub fn new(backend: DB) -> Self {
79        Self { backend }
80    }
81
82    /// Set up the database with the given function
83    ///
84    /// This method takes a closure that will be executed during setup.
85    /// Use the `boxed_async!` macro to create an async block that captures
86    /// variables without lifetime issues.
87    pub fn setup<F>(self, setup_fn: F) -> BoxedSetupHandler<DB>
88    where
89        F: for<'a> FnOnce(
90                &'a mut <DB::Pool as crate::DatabasePool>::Connection,
91            )
92                -> Pin<Box<dyn Future<Output = Result<(), DB::Error>> + Send + 'a>>
93            + Send
94            + Sync
95            + 'static,
96    {
97        BoxedSetupHandler {
98            backend: self.backend,
99            setup_fn: Box::new(setup_fn),
100        }
101    }
102
103    /// Initialize a database with a transaction
104    pub fn with_transaction<F>(self, transaction_fn: F) -> BoxedTransactionOnlyHandler<DB>
105    where
106        F: for<'a> FnOnce(
107                &'a mut <DB as DatabaseBackend>::Connection,
108            )
109                -> Pin<Box<dyn Future<Output = Result<(), DB::Error>> + Send + 'a>>
110            + Send
111            + Sync
112            + 'static,
113    {
114        BoxedTransactionOnlyHandler {
115            backend: self.backend,
116            transaction_fn: Box::new(transaction_fn),
117        }
118    }
119
120    /// Execute this handler
121    pub async fn execute(self) -> Result<crate::TestContext<DB>, DB::Error> {
122        // Create the database instance
123        let db_instance =
124            crate::testdb::TestDatabaseInstance::new(self.backend, DatabaseConfig::default())
125                .await?;
126
127        // Create and return the context
128        Ok(crate::TestContext::new(db_instance))
129    }
130
131    /// Add an async setup function without requiring boxed_async
132    ///
133    /// This method provides a more ergonomic API without requiring manual boxing
134    pub fn setup_async<F, Fut>(self, setup_fn: F) -> BoxedSetupHandler<DB>
135    where
136        F: FnOnce(&mut <DB::Pool as DatabasePool>::Connection) -> Fut + Send + Sync + 'static,
137        Fut: Future<Output = Result<(), DB::Error>> + Send + 'static,
138    {
139        self.setup(move |conn| {
140            Box::pin(setup_fn(conn))
141                as Pin<Box<dyn Future<Output = Result<(), DB::Error>> + Send + '_>>
142        })
143    }
144
145    /// Add a transaction function without requiring boxed_async
146    ///
147    /// This method provides a more ergonomic API without requiring manual boxing
148    pub fn transaction<F, Fut>(self, transaction_fn: F) -> BoxedTransactionOnlyHandler<DB>
149    where
150        F: FnOnce(&mut <DB as DatabaseBackend>::Connection) -> Fut + Send + Sync + 'static,
151        Fut: Future<Output = Result<(), DB::Error>> + Send + 'static,
152    {
153        self.with_transaction(move |conn| {
154            Box::pin(transaction_fn(conn))
155                as Pin<Box<dyn Future<Output = Result<(), DB::Error>> + Send + '_>>
156        })
157    }
158
159    /// Run the handler
160    ///
161    /// This is an alias for execute() with a more intuitive name
162    pub async fn run(self) -> Result<crate::TestContext<DB>, DB::Error> {
163        self.execute().await
164    }
165}
166
167#[async_trait]
168impl<DB> TransactionHandler<DB> for BoxedDatabaseEntryPoint<DB>
169where
170    DB: DatabaseBackend + Send + Sync + Debug + 'static,
171{
172    type Item = TestContext<DB>;
173    type Error = DB::Error;
174
175    async fn execute(self, _ctx: &mut TestContext<DB>) -> Result<Self::Item, Self::Error> {
176        // Create the database instance
177        let db_instance =
178            crate::testdb::TestDatabaseInstance::new(self.backend, DatabaseConfig::default())
179                .await?;
180
181        // Create and return the context
182        Ok(crate::TestContext::new(db_instance))
183    }
184}
185
186impl<DB> BoxedTransactionOnlyHandler<DB>
187where
188    DB: DatabaseBackend + Send + Sync + Debug + 'static,
189{
190    /// Execute this handler
191    pub async fn execute(self) -> Result<crate::TestContext<DB>, DB::Error> {
192        // Create the database instance
193        let db_instance =
194            crate::testdb::TestDatabaseInstance::new(self.backend, DatabaseConfig::default())
195                .await?;
196
197        // Create the context
198        let ctx = crate::TestContext::new(db_instance.clone());
199
200        // TRANSACTION: Get a connection for the transaction
201        let mut conn = ctx.db.pool.acquire().await?;
202
203        // Call the transaction function with a reference to the connection
204        (self.transaction_fn)(&mut conn).await?;
205
206        // Release the connection
207        ctx.db.pool.release(conn).await?;
208
209        // Return the context
210        Ok(ctx)
211    }
212
213    /// Run the handler
214    ///
215    /// This is an alias for execute() with a more intuitive name
216    pub async fn run(self) -> Result<crate::TestContext<DB>, DB::Error> {
217        self.execute().await
218    }
219}
220
221#[async_trait]
222impl<DB> TransactionHandler<DB> for BoxedTransactionOnlyHandler<DB>
223where
224    DB: DatabaseBackend + Send + Sync + Debug + 'static,
225{
226    type Item = TestContext<DB>;
227    type Error = DB::Error;
228
229    async fn execute(self, _ctx: &mut TestContext<DB>) -> Result<Self::Item, Self::Error> {
230        self.execute().await
231    }
232}
233
234impl<DB> BoxedSetupHandler<DB>
235where
236    DB: DatabaseBackend + Send + Sync + Debug + 'static,
237{
238    /// Add a transaction function
239    ///
240    /// This method takes a closure that will be executed during transaction.
241    /// Use the `boxed_async!` macro to create an async block that captures
242    /// variables without lifetime issues.
243    pub fn with_transaction<F>(self, transaction_fn: F) -> BoxedTransactionHandler<DB>
244    where
245        F: for<'a> FnOnce(
246                &'a mut <DB as DatabaseBackend>::Connection,
247            )
248                -> Pin<Box<dyn Future<Output = Result<(), DB::Error>> + Send + 'a>>
249            + Send
250            + Sync
251            + 'static,
252    {
253        BoxedTransactionHandler {
254            backend: self.backend,
255            setup_fn: self.setup_fn,
256            transaction_fn: Box::new(transaction_fn),
257        }
258    }
259
260    /// Execute this handler
261    pub async fn execute(self) -> Result<crate::TestContext<DB>, DB::Error> {
262        // Create the database instance
263        let db_instance =
264            crate::testdb::TestDatabaseInstance::new(self.backend, DatabaseConfig::default())
265                .await?;
266
267        // Create the context
268        let ctx = crate::TestContext::new(db_instance);
269
270        // Get a connection from the pool
271        let mut conn = ctx.db.pool.acquire().await?;
272
273        // Call the setup function with a reference to the connection
274        (self.setup_fn)(&mut conn).await?;
275
276        // Release the connection back to the pool
277        ctx.db.pool.release(conn).await?;
278
279        // Return the context
280        Ok(ctx)
281    }
282
283    /// Add a transaction function without requiring boxed_async
284    ///
285    /// This method provides a more ergonomic API without requiring manual boxing
286    pub fn transaction<F, Fut>(self, transaction_fn: F) -> BoxedTransactionHandler<DB>
287    where
288        F: FnOnce(&mut <DB as DatabaseBackend>::Connection) -> Fut + Send + Sync + 'static,
289        Fut: Future<Output = Result<(), DB::Error>> + Send + 'static,
290    {
291        self.with_transaction(move |conn| {
292            Box::pin(transaction_fn(conn))
293                as Pin<Box<dyn Future<Output = Result<(), DB::Error>> + Send + '_>>
294        })
295    }
296
297    /// Run the handler
298    ///
299    /// This is an alias for execute() with a more intuitive name
300    pub async fn run(self) -> Result<crate::TestContext<DB>, DB::Error> {
301        self.execute().await
302    }
303}
304
305#[async_trait]
306impl<DB> TransactionHandler<DB> for BoxedSetupHandler<DB>
307where
308    DB: DatabaseBackend + Send + Sync + Debug + 'static,
309{
310    type Item = TestContext<DB>;
311    type Error = DB::Error;
312
313    async fn execute(self, _ctx: &mut TestContext<DB>) -> Result<Self::Item, Self::Error> {
314        self.execute().await
315    }
316}
317
318impl<DB> BoxedTransactionHandler<DB>
319where
320    DB: DatabaseBackend + Send + Sync + Debug + 'static,
321{
322    /// Execute this handler
323    pub async fn execute(self) -> Result<crate::TestContext<DB>, DB::Error> {
324        // Create the database instance
325        let db_instance =
326            crate::testdb::TestDatabaseInstance::new(self.backend, DatabaseConfig::default())
327                .await?;
328
329        // Create the context
330        let ctx = crate::TestContext::new(db_instance);
331
332        // SETUP: Get a connection from the pool
333        let mut conn = ctx.db.pool.acquire().await?;
334
335        // Call the setup function with a reference to the connection
336        (self.setup_fn)(&mut conn).await?;
337
338        // Release the connection back to the pool
339        ctx.db.pool.release(conn).await?;
340
341        // TRANSACTION: Get a new connection for the transaction
342        let mut conn = ctx.db.pool.acquire().await?;
343
344        // Call the transaction function with a reference to the connection
345        (self.transaction_fn)(&mut conn).await?;
346
347        // Release the connection
348        ctx.db.pool.release(conn).await?;
349
350        // Return the context
351        Ok(ctx)
352    }
353
354    /// Run the handler
355    ///
356    /// This is an alias for execute() with a more intuitive name
357    pub async fn run(self) -> Result<crate::TestContext<DB>, DB::Error> {
358        self.execute().await
359    }
360}
361
362#[async_trait]
363impl<DB> TransactionHandler<DB> for BoxedTransactionHandler<DB>
364where
365    DB: DatabaseBackend + Send + Sync + Debug + 'static,
366{
367    type Item = TestContext<DB>;
368    type Error = DB::Error;
369
370    async fn execute(self, _ctx: &mut TestContext<DB>) -> Result<Self::Item, Self::Error> {
371        self.execute().await
372    }
373}
374
375/// Create a new database entry point with the given backend
376///
377/// This function creates a new entry point for working with databases.
378/// Use the `boxed_async!` macro with `setup` and `with_transaction` to avoid lifetime issues.
379pub fn with_boxed_database<DB>(backend: DB) -> BoxedDatabaseEntryPoint<DB>
380where
381    DB: DatabaseBackend + Send + Sync + Debug + 'static,
382{
383    BoxedDatabaseEntryPoint::new(backend)
384}
385
386/// Create a new database entry point with the given backend and config
387///
388/// This function creates a new entry point for working with databases.
389/// Use the `boxed_async!` macro with `setup` and `with_transaction` to avoid lifetime issues.
390pub fn with_boxed_database_config<DB>(
391    backend: DB,
392    _config: DatabaseConfig,
393) -> BoxedDatabaseEntryPoint<DB>
394where
395    DB: DatabaseBackend + Send + Sync + Debug + 'static,
396{
397    BoxedDatabaseEntryPoint::new(backend)
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::testdb::TestDatabaseConnection;
404
405    #[derive(Debug, Clone)]
406    struct MockError(String);
407
408    impl std::fmt::Display for MockError {
409        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410            write!(f, "Mock error: {}", self.0)
411        }
412    }
413
414    impl std::error::Error for MockError {}
415
416    impl From<String> for MockError {
417        fn from(s: String) -> Self {
418            MockError(s)
419        }
420    }
421
422    #[derive(Debug, Clone)]
423    struct MockConnection;
424
425    impl TestDatabaseConnection for MockConnection {
426        fn connection_string(&self) -> String {
427            "mock://test".to_string()
428        }
429    }
430
431    #[derive(Debug, Clone)]
432    struct MockPool;
433
434    #[async_trait]
435    impl crate::DatabasePool for MockPool {
436        type Connection = MockConnection;
437        type Error = MockError;
438
439        async fn acquire(&self) -> Result<Self::Connection, Self::Error> {
440            Ok(MockConnection)
441        }
442
443        async fn release(&self, _conn: Self::Connection) -> Result<(), Self::Error> {
444            Ok(())
445        }
446
447        fn connection_string(&self) -> String {
448            "mock://test".to_string()
449        }
450    }
451
452    #[derive(Debug, Clone)]
453    struct MockBackend;
454
455    impl MockBackend {
456        fn new() -> Self {
457            MockBackend
458        }
459    }
460
461    #[async_trait]
462    impl crate::DatabaseBackend for MockBackend {
463        type Connection = MockConnection;
464        type Pool = MockPool;
465        type Error = MockError;
466
467        async fn new(_config: crate::DatabaseConfig) -> Result<Self, Self::Error> {
468            Ok(Self)
469        }
470
471        async fn connect(
472            &self,
473            _name: &crate::DatabaseName,
474        ) -> Result<Self::Connection, Self::Error> {
475            Ok(MockConnection)
476        }
477
478        async fn connect_with_string(
479            &self,
480            _connection_string: &str,
481        ) -> Result<Self::Connection, Self::Error> {
482            Ok(MockConnection)
483        }
484
485        async fn create_pool(
486            &self,
487            _name: &crate::DatabaseName,
488            _config: &crate::DatabaseConfig,
489        ) -> Result<Self::Pool, Self::Error> {
490            Ok(MockPool)
491        }
492
493        async fn create_database(
494            &self,
495            _pool: &Self::Pool,
496            _name: &crate::DatabaseName,
497        ) -> Result<(), Self::Error> {
498            Ok(())
499        }
500
501        fn drop_database(&self, _name: &crate::DatabaseName) -> Result<(), Self::Error> {
502            Ok(())
503        }
504
505        fn connection_string(&self, _name: &crate::DatabaseName) -> String {
506            "mock://test".to_string()
507        }
508    }
509
510    #[tokio::test]
511    async fn test_boxed_database() {
512        let backend = MockBackend::new();
513
514        // Example with boxed_async macro
515        let ctx = with_boxed_database(backend)
516            .setup(|_conn| {
517                crate::boxed_async!(async move {
518                    // Use the connection to set up the database
519                    Ok(())
520                })
521            })
522            .with_transaction(|_conn| {
523                crate::boxed_async!(async move {
524                    // Use the connection to run a transaction
525                    Ok(())
526                })
527            })
528            .execute()
529            .await;
530
531        assert!(ctx.is_ok());
532    }
533}