a3s-boot 0.1.3

Adapter-first modular Rust web framework for A3S inspired by Nest.js
Documentation
#![cfg(feature = "database")]

use a3s_boot::{
    BootApplication, BootError, Database, DatabaseModule, DatabaseRow, InMemoryDatabaseBackend,
    Module, ModuleRef, ProviderDefinition, Result,
};
use serde_json::json;
use std::sync::Arc;

#[tokio::test]
async fn database_executes_and_queries_through_in_memory_backend() {
    let backend = InMemoryDatabaseBackend::new()
        .with_query_result(
            "select id, name from cats where id = ?",
            [DatabaseRow::new()
                .with("id", &1_u64)
                .unwrap()
                .with("name", &"Milo")
                .unwrap()],
        )
        .unwrap();
    let database = Database::new(backend.clone());

    let result = database
        .execute("insert into cats(name) values (?)", [json!("Milo")])
        .await
        .unwrap();
    let rows = database
        .query("select id, name from cats where id = ?", [json!(1)])
        .await
        .unwrap();

    assert_eq!(result.rows_affected(), 1);
    assert_eq!(rows.len(), 1);
    assert_eq!(rows[0].get::<u64>("id").unwrap(), Some(1));
    assert_eq!(rows[0].get::<String>("name").unwrap(), Some("Milo".into()));

    let executed = backend.executed().unwrap();
    let queried = backend.queried().unwrap();
    assert_eq!(executed[0].sql(), "insert into cats(name) values (?)");
    assert_eq!(executed[0].params(), &[json!("Milo")]);
    assert_eq!(queried[0].sql(), "select id, name from cats where id = ?");
    assert_eq!(queried[0].params(), &[json!(1)]);
}

#[tokio::test]
async fn database_transactions_commit_and_rollback() {
    let backend = InMemoryDatabaseBackend::new();
    let database = Database::new(backend.clone());

    let value = database
        .transaction(|transaction| async move {
            transaction
                .execute("insert into cats(name) values (?)", [json!("Milo")])
                .await?;
            transaction
                .execute("insert into cats(name) values (?)", [json!("Otis")])
                .await?;
            Ok(2_u64)
        })
        .await
        .unwrap();
    let error = database
        .transaction(|transaction| async move {
            transaction
                .execute("insert into cats(name) values (?)", [json!("Bad")])
                .await?;
            Err::<(), _>(BootError::BadRequest("invalid cat".to_string()))
        })
        .await
        .unwrap_err();

    let transactions = backend.transactions().unwrap();
    assert_eq!(value, 2);
    assert!(matches!(error, BootError::BadRequest(message) if message == "invalid cat"));
    assert_eq!(transactions.len(), 2);
    assert!(transactions[0].committed());
    assert!(!transactions[0].rolled_back());
    assert_eq!(transactions[0].statements().len(), 2);
    assert!(!transactions[1].committed());
    assert!(transactions[1].rolled_back());
    assert_eq!(transactions[1].statements().len(), 1);
}

#[tokio::test]
async fn database_module_exports_database_to_importing_modules() {
    #[derive(Debug)]
    struct CatsRepository {
        database: Arc<Database>,
    }

    impl CatsRepository {
        async fn names(&self) -> Result<Vec<String>> {
            let rows = self
                .database
                .query("select name from cats", Vec::<serde_json::Value>::new())
                .await?;
            rows.into_iter()
                .map(|row| {
                    row.get::<String>("name")?
                        .ok_or_else(|| BootError::Internal("missing name column".to_string()))
                })
                .collect()
        }
    }

    #[derive(Debug)]
    struct CatsModule {
        database: DatabaseModule,
    }

    impl Module for CatsModule {
        fn name(&self) -> &'static str {
            "cats"
        }

        fn imports(&self) -> Vec<Arc<dyn Module>> {
            vec![Arc::new(self.database.clone())]
        }

        fn providers(&self) -> Result<Vec<ProviderDefinition>> {
            Ok(vec![ProviderDefinition::factory::<CatsRepository, _>(
                |module_ref: &ModuleRef| {
                    Ok(CatsRepository {
                        database: module_ref.get::<Database>()?,
                    })
                },
            )])
        }
    }

    let backend = InMemoryDatabaseBackend::new()
        .with_query_result(
            "select name from cats",
            [DatabaseRow::new().with("name", &"Milo").unwrap()],
        )
        .unwrap();
    let app = BootApplication::builder()
        .import(CatsModule {
            database: DatabaseModule::from_backend("database", backend.clone()),
        })
        .build()
        .unwrap();

    let repository = app.get::<CatsRepository>().unwrap();
    assert_eq!(repository.names().await.unwrap(), ["Milo".to_string()]);
    assert_eq!(backend.queried().unwrap()[0].sql(), "select name from cats");
}

#[tokio::test]
async fn database_module_supports_named_and_global_exports() {
    #[derive(Debug)]
    struct UsesNamedDatabase {
        database: DatabaseModule,
    }

    impl Module for UsesNamedDatabase {
        fn name(&self) -> &'static str {
            "uses-named-database"
        }

        fn imports(&self) -> Vec<Arc<dyn Module>> {
            vec![Arc::new(self.database.clone())]
        }

        fn providers(&self) -> Result<Vec<ProviderDefinition>> {
            Ok(vec![ProviderDefinition::factory::<UsesDatabase, _>(
                |module_ref: &ModuleRef| {
                    Ok(UsesDatabase {
                        database: module_ref.get_named::<Database>("analytics")?,
                    })
                },
            )])
        }
    }

    #[derive(Debug)]
    struct UsesGlobalDatabase;

    impl Module for UsesGlobalDatabase {
        fn name(&self) -> &'static str {
            "uses-global-database"
        }

        fn providers(&self) -> Result<Vec<ProviderDefinition>> {
            Ok(vec![ProviderDefinition::factory::<UsesDatabase, _>(
                |module_ref: &ModuleRef| {
                    Ok(UsesDatabase {
                        database: module_ref.get::<Database>()?,
                    })
                },
            )])
        }
    }

    #[derive(Debug)]
    struct UsesDatabase {
        database: Arc<Database>,
    }

    let named = BootApplication::builder()
        .import(UsesNamedDatabase {
            database: DatabaseModule::in_memory("analytics").named("analytics"),
        })
        .build()
        .unwrap();
    let global = BootApplication::builder()
        .import(DatabaseModule::in_memory("global-database").global())
        .import(UsesGlobalDatabase)
        .build()
        .unwrap();

    let named_service = named.get::<UsesDatabase>().unwrap();
    named_service
        .database
        .query("select 1", Vec::<serde_json::Value>::new())
        .await
        .unwrap();
    assert!(named.get_named::<Database>("analytics").is_ok());
    assert!(named.get_optional::<Database>().unwrap().is_none());
    let global_service = global.get::<UsesDatabase>().unwrap();
    global_service
        .database
        .query("select 1", Vec::<serde_json::Value>::new())
        .await
        .unwrap();
    assert!(global.get::<Database>().is_ok());
}