use crate::config::ConfigRepo;
use crate::container::Container;
use crate::env::EnvRepo;
use crate::log::{self, Log};
use anyhow::Result;
pub trait ServiceProvider: Send + Sync {
fn register(&self, container: &Container) -> Result<()>;
fn boot(&self, container: &Container) -> Result<()> {
let _ = container;
Ok(()) }
fn name(&self) -> &str {
std::any::type_name::<Self>()
}
}
pub struct Application {
container: Container,
config: ConfigRepo,
env: EnvRepo,
providers: Vec<Box<dyn ServiceProvider>>,
booted: bool,
}
impl Application {
pub fn new() -> Self {
let container = Container::new();
Self {
container,
config: ConfigRepo::new(),
env: EnvRepo::new(),
providers: Vec::new(),
booted: false,
}
}
pub fn load_config(mut self, dir: impl AsRef<std::path::Path>) -> Result<Self> {
let mut repo = ConfigRepo::load_dir(dir)?;
repo.apply_env_overrides();
self.config = repo.clone();
self.container.instance(repo);
Ok(self)
}
pub fn load_env(mut self, path: impl AsRef<std::path::Path>) -> Result<Self> {
let path = path.as_ref();
let repo = if path.is_dir() {
EnvRepo::load_from_dir(path)?
} else {
EnvRepo::load(path)?
};
self.env = repo.clone();
self.container.instance_or_replace(repo);
Ok(self)
}
pub fn register_provider<P: ServiceProvider + 'static>(mut self, provider: P) -> Self {
if self.booted {
if let Err(e) = provider.register(&self.container) {
log::error!("Error registering provider `{}`: {e}", provider.name());
}
if let Err(e) = provider.boot(&self.container) {
log::error!("Error booting provider `{}`: {e}", provider.name());
}
}
self.providers.push(Box::new(provider));
self
}
pub fn boot(mut self) -> Result<Self> {
if self.booted {
return Ok(self);
}
Log::init("info");
self.container.instance_or_replace(self.config.clone());
self.container.instance_or_replace(self.env.clone());
for provider in &self.providers {
provider.register(&self.container).map_err(|e| {
anyhow::anyhow!("Error registering provider `{}`: {e}", provider.name())
})?;
}
for provider in &self.providers {
provider.boot(&self.container).map_err(|e| {
anyhow::anyhow!("Error booting provider `{}`: {e}", provider.name())
})?;
}
self.booted = true;
self.container.freeze();
log::info!("Application booted successfully");
Ok(self)
}
pub fn container(&self) -> &Container {
&self.container
}
pub fn config(&self) -> &ConfigRepo {
&self.config
}
pub fn env(&self) -> &EnvRepo {
&self.env
}
pub fn is_booted(&self) -> bool {
self.booted
}
}
impl Default for Application {
fn default() -> Self {
Self::new()
}
}
impl Application {
pub fn singleton<T, F>(&self, factory: F)
where
T: Send + Sync + 'static,
F: Fn(&Container) -> T + Send + Sync + 'static,
{
self.container.singleton(factory);
}
pub fn instance<T>(&self, value: T)
where
T: Send + Sync + 'static,
{
self.container.instance(value);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
struct ProviderA {
call_order: Arc<AtomicUsize>,
}
struct ProviderB {
call_order: Arc<AtomicUsize>,
}
impl ServiceProvider for ProviderA {
fn register(&self, c: &Container) -> Result<()> {
self.call_order.fetch_add(1, Ordering::SeqCst);
c.instance("Registered A".to_string());
Ok(())
}
fn boot(&self, c: &Container) -> Result<()> {
self.call_order.fetch_add(1, Ordering::SeqCst);
let _val: std::sync::Arc<String> = c.resolve().unwrap();
Ok(())
}
}
impl ServiceProvider for ProviderB {
fn register(&self, c: &Container) -> Result<()> {
self.call_order.fetch_add(10, Ordering::SeqCst);
let _val: std::sync::Arc<String> = c.resolve().unwrap();
Ok(())
}
fn boot(&self, _c: &Container) -> Result<()> {
self.call_order.fetch_add(10, Ordering::SeqCst);
Ok(())
}
}
#[test]
fn test_register_boot_order() {
let order = Arc::new(AtomicUsize::new(0));
let app = Application::new()
.register_provider(ProviderA {
call_order: order.clone(),
})
.register_provider(ProviderB {
call_order: order.clone(),
})
.boot()
.unwrap();
assert!(app.is_booted());
assert_eq!(order.load(Ordering::SeqCst), 22);
}
#[test]
fn test_config_available_in_container() {
let mut app = Application::new();
app.config.set("app.name", "RavelTest");
app.container.instance(app.config.clone());
let repo: std::sync::Arc<ConfigRepo> = app.container.resolve().unwrap();
assert_eq!(repo.get::<String>("app.name").unwrap(), "RavelTest");
}
#[test]
fn test_env_available_in_container() {
let dir = std::env::temp_dir().join("ravel_app_env_test");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut f = std::fs::File::create(dir.join(".env")).unwrap();
use std::io::Write;
writeln!(f, "APP_ENV=testing").unwrap();
drop(f);
let app = Application::new().load_env(&dir).unwrap().boot().unwrap();
let env: std::sync::Arc<EnvRepo> = app.container().resolve().unwrap();
assert_eq!(env.get("APP_ENV"), Some("testing"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_late_provider_registration() {
let order = Arc::new(AtomicUsize::new(0));
let app = Application::new()
.register_provider(ProviderA {
call_order: order.clone(),
})
.boot()
.unwrap();
let _app = app.register_provider(ProviderB {
call_order: order.clone(),
});
assert_eq!(order.load(Ordering::SeqCst), 22);
}
}