use crate::Error;
use crate::injector::Injector;
use crate::module::Module;
use crate::runtime::Shared;
#[cfg(feature = "tracing")]
use tracing::{debug, info};
#[cfg(not(feature = "thread-safe"))]
type ModuleObject = Box<dyn Module>;
#[cfg(feature = "thread-safe")]
type ModuleObject = Box<dyn Module>;
struct LoadedModule {
module: ModuleObject,
injector: Shared<Injector>,
}
pub struct Application {
root: Option<ModuleObject>,
injector: Shared<Injector>,
started_modules: Vec<LoadedModule>,
}
#[cfg(feature = "debug")]
impl std::fmt::Debug for Application {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Application")
.field("injector", &"...")
.field("root", &"<dyn Module>")
.field("started_modules", &self.started_modules.len())
.finish()
}
}
impl Application {
pub fn new(root: impl Module + 'static) -> Self {
#[cfg(feature = "tracing")]
info!("Creating new Application instance with root module");
Self {
root: Some(Box::new(root)),
injector: Shared::new(Injector::root()),
started_modules: Vec::new(),
}
}
pub fn bootstrap_sync(&mut self) -> Result<(), Error> {
let root = self.root.take().expect("Application already bootstrapped");
#[cfg(feature = "tracing")]
info!("Starting application bootstrap process");
Self::load_module(self.injector.clone(), root)?;
#[cfg(feature = "tracing")]
info!("Application bootstrap completed successfully");
Ok(())
}
pub async fn bootstrap(&mut self) -> Result<(), Error> {
let root = self.root.take().expect("Application already bootstrapped");
#[cfg(feature = "tracing")]
info!("Starting async application bootstrap process");
self.started_modules = Self::load_module_async(self.injector.clone(), root).await?;
#[cfg(feature = "tracing")]
info!("Async application bootstrap completed successfully");
Ok(())
}
pub async fn bootstrap_async(&mut self) -> Result<(), Error> {
self.bootstrap().await
}
pub async fn shutdown(&mut self) -> Result<(), Error> {
#[cfg(feature = "tracing")]
info!("Starting async application shutdown process");
while let Some(loaded) = self.started_modules.pop() {
let module_name = std::any::type_name_of_val(&*loaded.module);
loaded
.module
.on_stop(loaded.injector.clone())
.await
.map_err(|err| {
Error::module_lifecycle_failed(module_name, "on_stop", &err.to_string())
})?;
}
#[cfg(feature = "tracing")]
info!("Async application shutdown completed successfully");
Ok(())
}
pub async fn shutdown_async(&mut self) -> Result<(), Error> {
self.shutdown().await
}
pub async fn start(&mut self) -> Result<(), Error> {
self.bootstrap().await
}
pub async fn stop(&mut self) -> Result<(), Error> {
self.shutdown().await
}
pub fn injector(&self) -> Shared<Injector> {
#[cfg(feature = "tracing")]
debug!("Accessing root injector");
#[cfg(feature = "tracing")]
{
if self.is_bootstrapped() {
debug!("Injector is available and application is bootstrapped");
} else {
debug!("Injector is available but application is not bootstrapped yet");
}
}
self.injector.clone()
}
pub fn is_bootstrapped(&self) -> bool {
let bootstrapped = self.root.is_none();
#[cfg(feature = "tracing")]
debug!("Checking application bootstrap state: {}", bootstrapped);
bootstrapped
}
fn load_module(parent: Shared<Injector>, module: ModuleObject) -> Result<(), Error> {
#[cfg(feature = "tracing")]
debug!("Loading module into injector hierarchy");
let module_injector = Shared::new(Injector::child(parent.clone()));
#[cfg(feature = "tracing")]
debug!("Created child injector for module");
let imports = module.imports();
#[cfg(feature = "tracing")]
if !imports.is_empty() {
debug!("Module has {} imports, loading them first", imports.len());
}
#[allow(unused_variables)]
for (index, import) in imports.into_iter().enumerate() {
#[cfg(feature = "tracing")]
debug!("Loading import {}", index + 1);
Self::load_module(module_injector.clone(), import)?;
}
#[cfg(feature = "tracing")]
debug!("Registering module providers");
let module_name = std::any::type_name_of_val(&*module);
module.configure(&module_injector).map_err(|err| {
Error::module_lifecycle_failed(module_name, "configure", &err.to_string())
})?;
#[cfg(feature = "tracing")]
debug!("Module loaded successfully");
Ok(())
}
async fn load_module_async(
parent: Shared<Injector>,
module: ModuleObject,
) -> Result<Vec<LoadedModule>, Error> {
enum Frame {
Enter {
parent: Shared<Injector>,
module: ModuleObject,
},
Exit {
module_injector: Shared<Injector>,
module: ModuleObject,
},
}
let mut stack = vec![Frame::Enter { parent, module }];
let mut loaded = Vec::new();
while let Some(frame) = stack.pop() {
match frame {
Frame::Enter { parent, module } => {
let module_injector = Shared::new(Injector::child(parent));
let imports = module.imports();
stack.push(Frame::Exit {
module_injector: module_injector.clone(),
module,
});
for import in imports.into_iter().rev() {
stack.push(Frame::Enter {
parent: module_injector.clone(),
module: import,
});
}
}
Frame::Exit {
module_injector,
module,
} => {
let module_name = std::any::type_name_of_val(&*module);
module.configure(&module_injector).map_err(|err| {
Error::module_lifecycle_failed(module_name, "configure", &err.to_string())
})?;
module
.on_start(module_injector.clone())
.await
.map_err(|err| {
Error::module_lifecycle_failed(
module_name,
"on_start",
&err.to_string(),
)
})?;
loaded.push(LoadedModule {
module,
injector: module_injector,
});
}
}
}
Ok(loaded)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::module::ModuleLifecycleFuture;
use crate::{Error, ErrorKind};
use futures::executor::block_on;
#[cfg(not(feature = "thread-safe"))]
use std::cell::RefCell;
#[cfg(not(feature = "thread-safe"))]
use std::rc::Rc;
#[cfg(feature = "thread-safe")]
use std::sync::{Arc, Mutex};
struct EmptyModule;
impl Module for EmptyModule {
fn providers(&self, _injector: &Injector) {}
}
#[cfg(not(feature = "thread-safe"))]
struct CountingModule {
counter: Rc<RefCell<usize>>,
}
#[cfg(not(feature = "thread-safe"))]
impl Module for CountingModule {
fn providers(&self, _injector: &Injector) {
*self.counter.borrow_mut() += 1;
}
}
#[cfg(feature = "thread-safe")]
struct CountingModule {
counter: Arc<Mutex<usize>>,
}
#[cfg(feature = "thread-safe")]
impl Module for CountingModule {
fn providers(&self, _injector: &Injector) {
*self.counter.lock().unwrap() += 1;
}
}
#[cfg(not(feature = "thread-safe"))]
struct ModuleWithImports {
counter: Rc<RefCell<usize>>,
}
#[cfg(not(feature = "thread-safe"))]
impl Module for ModuleWithImports {
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![
Box::new(CountingModule {
counter: self.counter.clone(),
}),
Box::new(CountingModule {
counter: self.counter.clone(),
}),
]
}
fn providers(&self, _injector: &Injector) {
*self.counter.borrow_mut() += 1;
}
}
#[cfg(feature = "thread-safe")]
struct ModuleWithImports {
counter: Arc<Mutex<usize>>,
}
#[cfg(feature = "thread-safe")]
impl Module for ModuleWithImports {
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![
Box::new(CountingModule {
counter: self.counter.clone(),
}),
Box::new(CountingModule {
counter: self.counter.clone(),
}),
]
}
fn providers(&self, _injector: &Injector) {
*self.counter.lock().unwrap() += 1;
}
}
#[cfg(not(feature = "thread-safe"))]
type EventLog = Rc<RefCell<Vec<String>>>;
#[cfg(feature = "thread-safe")]
type EventLog = Arc<Mutex<Vec<String>>>;
#[cfg(not(feature = "thread-safe"))]
fn push_event(log: &EventLog, event: String) {
log.borrow_mut().push(event);
}
#[cfg(feature = "thread-safe")]
fn push_event(log: &EventLog, event: String) {
log.lock().unwrap().push(event);
}
#[cfg(not(feature = "thread-safe"))]
fn event_snapshot(log: &EventLog) -> Vec<String> {
log.borrow().clone()
}
#[cfg(feature = "thread-safe")]
fn event_snapshot(log: &EventLog) -> Vec<String> {
log.lock().unwrap().clone()
}
struct LifecycleModule {
name: &'static str,
log: EventLog,
import_child: bool,
}
impl Module for LifecycleModule {
fn imports(&self) -> Vec<Box<dyn Module>> {
if !self.import_child {
return vec![];
}
vec![Box::new(LifecycleModule {
name: "import",
log: self.log.clone(),
import_child: false,
})]
}
fn providers(&self, _injector: &Injector) {
push_event(&self.log, format!("providers:{}", self.name));
}
fn on_start(&self, _injector: Shared<Injector>) -> ModuleLifecycleFuture {
let log = self.log.clone();
let name = self.name.to_string();
Box::pin(async move {
push_event(&log, format!("on_start:{}", name));
Ok(())
})
}
fn on_stop(&self, _injector: Shared<Injector>) -> ModuleLifecycleFuture {
let log = self.log.clone();
let name = self.name.to_string();
Box::pin(async move {
push_event(&log, format!("on_stop:{}", name));
Ok(())
})
}
}
struct FailingLifecycleModule;
impl Module for FailingLifecycleModule {
fn on_start(&self, _injector: Shared<Injector>) -> ModuleLifecycleFuture {
Box::pin(async {
Err(Error::module_lifecycle_failed(
"FailingLifecycleModule",
"on_start",
"intentional test failure",
))
})
}
}
#[test]
fn test_new_creates_unbootstrapped_application() {
let app = Application::new(EmptyModule);
assert!(
!app.is_bootstrapped(),
"New application should not be bootstrapped"
);
}
#[test]
fn test_bootstrap_changes_state() {
let mut app = Application::new(EmptyModule);
assert!(!app.is_bootstrapped());
app.bootstrap_sync().unwrap();
assert!(
app.is_bootstrapped(),
"Application should be bootstrapped after bootstrap()"
);
}
#[test]
#[should_panic(expected = "Application already bootstrapped")]
fn test_bootstrap_twice_panics() {
let mut app = Application::new(EmptyModule);
app.bootstrap_sync().unwrap();
app.bootstrap_sync().unwrap(); }
#[test]
fn test_injector_returns_shared_reference() {
let mut app = Application::new(EmptyModule);
app.bootstrap_sync().unwrap();
let injector1 = app.injector();
let _injector2 = app.injector();
#[cfg(feature = "thread-safe")]
assert_eq!(std::sync::Arc::strong_count(&injector1), 3);
#[cfg(not(feature = "thread-safe"))]
assert_eq!(std::rc::Rc::strong_count(&injector1), 3); }
#[test]
fn test_bootstrap_calls_module_providers() {
#[cfg(not(feature = "thread-safe"))]
let counter = Rc::new(RefCell::new(0));
#[cfg(feature = "thread-safe")]
let counter = Arc::new(Mutex::new(0));
let module = CountingModule {
counter: counter.clone(),
};
let mut app = Application::new(module);
#[cfg(not(feature = "thread-safe"))]
assert_eq!(*counter.borrow(), 0);
#[cfg(feature = "thread-safe")]
assert_eq!(*counter.lock().unwrap(), 0);
app.bootstrap_sync().unwrap();
#[cfg(not(feature = "thread-safe"))]
assert_eq!(
*counter.borrow(),
1,
"Module providers should be called during bootstrap"
);
#[cfg(feature = "thread-safe")]
assert_eq!(
*counter.lock().unwrap(),
1,
"Module providers should be called during bootstrap"
);
}
#[test]
fn test_bootstrap_loads_imports_first() {
#[cfg(not(feature = "thread-safe"))]
let counter = Rc::new(RefCell::new(0));
#[cfg(feature = "thread-safe")]
let counter = Arc::new(Mutex::new(0));
let module = ModuleWithImports {
counter: counter.clone(),
};
let mut app = Application::new(module);
app.bootstrap_sync().unwrap();
#[cfg(not(feature = "thread-safe"))]
assert_eq!(*counter.borrow(), 3, "All modules should be loaded");
#[cfg(feature = "thread-safe")]
assert_eq!(*counter.lock().unwrap(), 3, "All modules should be loaded");
}
#[test]
fn test_bootstrap_async_runs_lifecycle_hooks_and_shutdown_reverse_order() {
#[cfg(not(feature = "thread-safe"))]
let log: EventLog = Rc::new(RefCell::new(Vec::new()));
#[cfg(feature = "thread-safe")]
let log: EventLog = Arc::new(Mutex::new(Vec::new()));
let root = LifecycleModule {
name: "root",
log: log.clone(),
import_child: true,
};
let mut app = Application::new(root);
block_on(app.bootstrap_async()).unwrap();
assert_eq!(
event_snapshot(&log),
vec![
"providers:import".to_string(),
"on_start:import".to_string(),
"providers:root".to_string(),
"on_start:root".to_string(),
]
);
block_on(app.shutdown_async()).unwrap();
assert_eq!(
event_snapshot(&log),
vec![
"providers:import".to_string(),
"on_start:import".to_string(),
"providers:root".to_string(),
"on_start:root".to_string(),
"on_stop:root".to_string(),
"on_stop:import".to_string(),
]
);
}
#[test]
fn test_bootstrap_async_propagates_lifecycle_errors() {
let mut app = Application::new(FailingLifecycleModule);
let error = block_on(app.bootstrap_async()).unwrap_err();
assert_eq!(error.kind, ErrorKind::ModuleLifecycleFailed);
assert!(error.message.contains("on_start"));
}
#[test]
fn test_start_stop_unified_api() {
#[cfg(not(feature = "thread-safe"))]
let log: EventLog = Rc::new(RefCell::new(Vec::new()));
#[cfg(feature = "thread-safe")]
let log: EventLog = Arc::new(Mutex::new(Vec::new()));
let root = LifecycleModule {
name: "root",
log: log.clone(),
import_child: false,
};
let mut app = Application::new(root);
block_on(app.start()).unwrap();
block_on(app.stop()).unwrap();
assert_eq!(
event_snapshot(&log),
vec![
"providers:root".to_string(),
"on_start:root".to_string(),
"on_stop:root".to_string(),
]
);
}
#[test]
fn test_application_can_be_created_with_different_modules() {
let _app1 = Application::new(EmptyModule);
#[cfg(not(feature = "thread-safe"))]
let _app2 = Application::new(CountingModule {
counter: Rc::new(RefCell::new(0)),
});
#[cfg(feature = "thread-safe")]
let _app2 = Application::new(CountingModule {
counter: Arc::new(Mutex::new(0)),
});
}
#[test]
fn test_injector_accessible_before_bootstrap() {
let app = Application::new(EmptyModule);
let _injector = app.injector();
}
#[test]
fn test_multiple_injector_clones() {
let mut app = Application::new(EmptyModule);
app.bootstrap_sync().unwrap();
let injectors: Vec<_> = (0..5).map(|_| app.injector()).collect();
assert_eq!(injectors.len(), 5);
#[cfg(feature = "thread-safe")]
assert_eq!(std::sync::Arc::strong_count(&injectors[0]), 6);
#[cfg(not(feature = "thread-safe"))]
assert_eq!(std::rc::Rc::strong_count(&injectors[0]), 6); }
#[cfg(feature = "debug")]
#[test]
fn test_debug_implementation() {
let app = Application::new(EmptyModule);
let debug_str = format!("{:?}", app);
assert!(
debug_str.contains("Application"),
"Debug output should contain 'Application'"
);
}
#[cfg(not(feature = "thread-safe"))]
struct NestedImportModule {
counter: Rc<RefCell<usize>>,
depth: usize,
}
#[cfg(not(feature = "thread-safe"))]
impl Module for NestedImportModule {
fn imports(&self) -> Vec<Box<dyn Module>> {
if self.depth > 0 {
vec![Box::new(NestedImportModule {
counter: self.counter.clone(),
depth: self.depth - 1,
})]
} else {
vec![]
}
}
fn providers(&self, _injector: &Injector) {
*self.counter.borrow_mut() += 1;
}
}
#[cfg(feature = "thread-safe")]
struct NestedImportModule {
counter: Arc<Mutex<usize>>,
depth: usize,
}
#[cfg(feature = "thread-safe")]
impl Module for NestedImportModule {
fn imports(&self) -> Vec<Box<dyn Module>> {
if self.depth > 0 {
vec![Box::new(NestedImportModule {
counter: self.counter.clone(),
depth: self.depth - 1,
})]
} else {
vec![]
}
}
fn providers(&self, _injector: &Injector) {
*self.counter.lock().unwrap() += 1;
}
}
#[test]
fn test_deeply_nested_modules() {
#[cfg(not(feature = "thread-safe"))]
let counter = Rc::new(RefCell::new(0));
#[cfg(feature = "thread-safe")]
let counter = Arc::new(Mutex::new(0));
let module = NestedImportModule {
counter: counter.clone(),
depth: 5,
};
let mut app = Application::new(module);
app.bootstrap_sync().unwrap();
#[cfg(not(feature = "thread-safe"))]
assert_eq!(*counter.borrow(), 6, "All nested modules should be loaded");
#[cfg(feature = "thread-safe")]
assert_eq!(
*counter.lock().unwrap(),
6,
"All nested modules should be loaded"
);
}
}