a3s-boot 0.1.3

Adapter-first modular Rust web framework for A3S inspired by Nest.js
Documentation
use a3s_boot::{
    BootApplication, BootError, Module, ModuleRef, ProviderDefinition, ProviderToken, Result,
};
use std::sync::{Arc, Mutex};

#[derive(Debug)]
struct LaterAsyncGlobalConfig {
    value: &'static str,
}

#[derive(Debug)]
struct EarlyAsyncConsumer {
    config: Arc<LaterAsyncGlobalConfig>,
}

#[derive(Debug)]
struct EarlyAsyncConsumerModule {
    log: Arc<Mutex<Vec<&'static str>>>,
}

impl Module for EarlyAsyncConsumerModule {
    fn name(&self) -> &'static str {
        "early-async-consumer"
    }

    fn providers(&self) -> Result<Vec<ProviderDefinition>> {
        let log = Arc::clone(&self.log);
        Ok(vec![ProviderDefinition::async_factory::<
            EarlyAsyncConsumer,
            _,
            _,
        >(move |module_ref| {
            let log = Arc::clone(&log);
            async move {
                let config = module_ref.get::<LaterAsyncGlobalConfig>()?;
                log.lock().unwrap().push("consumer-factory");
                Ok(EarlyAsyncConsumer { config })
            }
        })
        .depends_on::<LaterAsyncGlobalConfig>()])
    }

    fn on_module_init(&self, _module_ref: &ModuleRef) -> Result<()> {
        self.log.lock().unwrap().push("consumer-init");
        Ok(())
    }
}

#[derive(Debug)]
struct LaterAsyncGlobalModule {
    log: Arc<Mutex<Vec<&'static str>>>,
}

impl Module for LaterAsyncGlobalModule {
    fn name(&self) -> &'static str {
        "later-async-global"
    }

    fn providers(&self) -> Result<Vec<ProviderDefinition>> {
        let log = Arc::clone(&self.log);
        Ok(vec![ProviderDefinition::async_factory::<
            LaterAsyncGlobalConfig,
            _,
            _,
        >(move |_| {
            let log = Arc::clone(&log);
            async move {
                log.lock().unwrap().push("config-factory");
                Ok(LaterAsyncGlobalConfig { value: "ready" })
            }
        })])
    }

    fn exports(&self) -> Result<Vec<ProviderToken>> {
        Ok(vec![ProviderToken::of::<LaterAsyncGlobalConfig>()])
    }

    fn is_global(&self) -> bool {
        true
    }

    fn on_module_init(&self, _module_ref: &ModuleRef) -> Result<()> {
        self.log.lock().unwrap().push("global-init");
        Ok(())
    }
}

#[derive(Debug)]
struct LaterRequestGlobal;

#[derive(Debug)]
struct InvalidAsyncContextConsumer;

#[derive(Debug)]
struct InvalidAsyncContextConsumerModule {
    calls: Arc<std::sync::atomic::AtomicUsize>,
}

impl Module for InvalidAsyncContextConsumerModule {
    fn name(&self) -> &'static str {
        "invalid-async-context-consumer"
    }

    fn providers(&self) -> Result<Vec<ProviderDefinition>> {
        let calls = Arc::clone(&self.calls);
        Ok(vec![ProviderDefinition::async_factory::<
            InvalidAsyncContextConsumer,
            _,
            _,
        >(move |module_ref| {
            let calls = Arc::clone(&calls);
            async move {
                calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
                let _ = module_ref.get::<LaterRequestGlobal>()?;
                Ok(InvalidAsyncContextConsumer)
            }
        })
        .depends_on::<LaterRequestGlobal>()])
    }
}

#[derive(Debug)]
struct LaterRequestGlobalModule;

impl Module for LaterRequestGlobalModule {
    fn name(&self) -> &'static str {
        "later-request-global"
    }

    fn providers(&self) -> Result<Vec<ProviderDefinition>> {
        Ok(vec![ProviderDefinition::request_scoped::<
            LaterRequestGlobal,
            _,
        >(|_| Ok(LaterRequestGlobal))])
    }

    fn exports(&self) -> Result<Vec<ProviderToken>> {
        Ok(vec![ProviderToken::of::<LaterRequestGlobal>()])
    }

    fn is_global(&self) -> bool {
        true
    }
}

#[tokio::test]
async fn async_finalization_seeds_later_global_dependencies_before_consumers() {
    let log = Arc::new(Mutex::new(Vec::new()));
    let app = BootApplication::builder()
        .import(EarlyAsyncConsumerModule {
            log: Arc::clone(&log),
        })
        .import(LaterAsyncGlobalModule {
            log: Arc::clone(&log),
        })
        .build_async()
        .await
        .unwrap();

    let config = app.get::<LaterAsyncGlobalConfig>().unwrap();
    let consumer = app.get::<EarlyAsyncConsumer>().unwrap();

    assert_eq!(config.value, "ready");
    assert!(Arc::ptr_eq(&config, &consumer.config));
    assert_eq!(
        log.lock().unwrap().as_slice(),
        [
            "config-factory",
            "consumer-factory",
            "consumer-init",
            "global-init",
        ]
    );
}

#[tokio::test]
async fn full_graph_validation_rejects_contextual_async_providers_before_factories_run() {
    let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
    let result = BootApplication::builder()
        .import(InvalidAsyncContextConsumerModule {
            calls: Arc::clone(&calls),
        })
        .import(LaterRequestGlobalModule)
        .build_async()
        .await;

    assert!(
        matches!(result, Err(BootError::Internal(message)) if message.contains("cannot depend on a request-context provider"))
    );
    assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 0);
}