use crate::config::{ConfigFactory, ConfigManager, ConfigSource, ConfigValue};
use crate::error::{ContextError, ContextResult};
use crate::event::{
ConfigurationChangedEvent, ContextAwareEventListener, ContextInitializedEvent,
ContextInitializingEvent, Event, EventListener, EventPublisher,
};
use dashmap::DashMap;
use std::path::Path;
use std::sync::Arc;
use verdure_ioc::{ComponentContainer, ComponentFactory, ComponentInstance};
#[derive(Debug)]
pub struct ApplicationContextBuilder {
config_sources: Vec<ConfigSource>,
properties: std::collections::HashMap<String, String>,
}
impl ApplicationContextBuilder {
pub fn new() -> Self {
Self {
config_sources: Vec::new(),
properties: std::collections::HashMap::new(),
}
}
pub fn with_config_source(mut self, source: ConfigSource) -> Self {
self.config_sources.push(source);
self
}
pub fn with_toml_config_file<P: AsRef<Path>>(mut self, path: P) -> Self {
let path_str = path.as_ref().to_string_lossy().to_string();
self.config_sources.push(ConfigSource::TomlFile(path_str));
self
}
pub fn with_yaml_config_file<P: AsRef<Path>>(mut self, path: P) -> Self {
let path_str = path.as_ref().to_string_lossy().to_string();
self.config_sources.push(ConfigSource::YamlFile(path_str));
self
}
pub fn with_properties_config_file<P: AsRef<Path>>(mut self, path: P) -> Self {
let path_str = path.as_ref().to_string_lossy().to_string();
self.config_sources
.push(ConfigSource::PropertiesFile(path_str));
self
}
pub fn with_config_file<P: AsRef<Path>>(mut self, path: P) -> Self {
let path_str = path.as_ref().to_string_lossy().to_string();
self.config_sources.push(ConfigSource::ConfigFile(path_str));
self
}
pub fn with_property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.properties.insert(key.into(), value.into());
self
}
pub fn build(self) -> ContextResult<ApplicationContext> {
let context = ApplicationContext::new();
for source in self.config_sources {
context.config_manager.add_source(source)?;
}
if !self.properties.is_empty() {
context
.config_manager
.add_source(ConfigSource::Properties(self.properties))?;
}
Ok(context)
}
}
impl Default for ApplicationContextBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct ApplicationContext {
config_manager: Arc<ConfigManager>,
event_publisher: EventPublisher,
container: Arc<ComponentContainer>,
properties_cache: DashMap<String, ConfigValue>,
}
impl ApplicationContext {
pub fn new() -> Self {
Self {
config_manager: Arc::new(ConfigManager::new()),
event_publisher: EventPublisher::new(),
container: Arc::new(ComponentContainer::new()),
properties_cache: DashMap::new(),
}
}
pub fn builder() -> ApplicationContextBuilder {
ApplicationContextBuilder::new()
}
fn initialize_early(&self) -> ContextResult<()> {
self.container.register_component(self.config_manager.clone());
for factory in inventory::iter::<ConfigFactory> {
let config_component = (factory.create_fn)(self.config_manager.clone())?;
self.container.register_component(config_component);
}
Ok(())
}
pub fn initialize(&self) -> ContextResult<()> {
self.initialize_early()?;
let initializing_event = ContextInitializingEvent {
config_sources_count: self.config_manager.sources_count(),
timestamp: std::time::SystemTime::now(),
};
self.event_publisher
.publish_with_context(&initializing_event, self);
self.container.initialize().map_err(|e| {
ContextError::initialization_failed(format!(
"IoC container initialization failed: {}",
e
))
})?;
let initialized_event = ContextInitializedEvent {
config_sources_count: self.config_manager.sources_count(),
timestamp: std::time::SystemTime::now(),
};
self.event_publisher
.publish_with_context(&initialized_event, self);
Ok(())
}
pub fn get_config(&self, key: &str) -> String {
self.config_manager.get_string_or_default(key, "")
}
pub fn get_config_as<T>(&self, key: &str) -> ContextResult<T>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let value = self.config_manager.get_string(key)?;
value
.parse::<T>()
.map_err(|e| ContextError::invalid_configuration(key, e.to_string()))
}
pub fn get_config_or_default(&self, key: &str, default: &str) -> String {
self.config_manager.get_string_or_default(key, default)
}
pub fn set_config(&self, key: &str, value: &str) {
let old_value = self.get_config(key);
let old_value_opt = if old_value.is_empty() {
None
} else {
Some(old_value)
};
self.config_manager
.set(key, ConfigValue::String(value.to_string()));
let event = ConfigurationChangedEvent {
key: key.to_string(),
old_value: old_value_opt,
new_value: value.to_string(),
timestamp: std::time::SystemTime::now(),
};
self.event_publisher.publish(&event);
}
pub fn add_config_source(&self, source: ConfigSource) -> ContextResult<()> {
self.config_manager.add_source(source)
}
pub fn container(&self) -> Arc<ComponentContainer> {
self.container.clone()
}
pub fn config_manager(&self) -> Arc<ConfigManager> {
self.config_manager.clone()
}
pub fn get_component<T: 'static + Send + Sync>(&self) -> Option<Arc<T>> {
self.container.get_component()
}
pub fn register_component(&self, instance: ComponentInstance) {
self.container.register_component(instance)
}
pub fn publish_event<T: Event + 'static>(&self, event: &T) {
self.event_publisher.publish(event);
}
pub fn subscribe_to_context_events<
T: Event + 'static,
L: ContextAwareEventListener<T> + 'static,
>(
&self,
listener: L,
) {
self.event_publisher.subscribe_context_aware(listener);
}
pub fn subscribe_to_events<T: Event + 'static, L: EventListener<T> + 'static>(
&self,
listener: L,
) {
self.event_publisher.subscribe(listener);
}
pub fn environment(&self) -> String {
"default".to_string()
}
}
impl Default for ApplicationContext {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_application_context_creation() {
let context = ApplicationContext::new();
assert_eq!(context.environment(), "default");
}
#[test]
fn test_application_context_builder() {
let context = ApplicationContext::builder()
.with_property("app.name", "TestApp")
.build()
.unwrap();
assert_eq!(context.get_config("app.name"), "TestApp");
}
#[test]
fn test_configuration_management() {
let context = ApplicationContext::new();
let mut props = HashMap::new();
props.insert(
"database.url".to_string(),
"postgres://localhost/test".to_string(),
);
props.insert("server.port".to_string(), "3000".to_string());
props.insert("debug.enabled".to_string(), "true".to_string());
context
.add_config_source(ConfigSource::Properties(props))
.unwrap();
assert_eq!(
context.get_config("database.url"),
"postgres://localhost/test"
);
let port: i64 = context.get_config_as("server.port").unwrap();
assert_eq!(port, 3000);
let debug: bool = context.get_config_as("debug.enabled").unwrap();
assert_eq!(debug, true);
}
#[test]
fn test_configuration_with_defaults() {
let context = ApplicationContext::new();
assert_eq!(context.get_config("missing.key"), "");
assert_eq!(
context.get_config_or_default("missing.key", "default"),
"default"
);
}
#[test]
fn test_runtime_configuration() {
let context = ApplicationContext::new();
context.set_config("runtime.property", "runtime.value");
assert_eq!(context.get_config("runtime.property"), "runtime.value");
}
#[test]
fn test_environment_default() {
let context = ApplicationContext::new();
assert_eq!(context.environment(), "default");
}
#[test]
fn test_container_integration() {
let context = ApplicationContext::new();
let container = context.container();
assert!(container.get_component::<String>().is_none());
}
use std::any::Any;
#[derive(Debug, Clone)]
struct TestContextEvent {
message: String,
}
impl Event for TestContextEvent {
fn name(&self) -> &'static str {
"TestContextEvent"
}
fn as_any(&self) -> &dyn Any {
self
}
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
}
struct TestContextListener {
received: Arc<std::sync::Mutex<Vec<String>>>,
}
impl EventListener<TestContextEvent> for TestContextListener {
fn on_event(&self, event: &TestContextEvent) {
let mut received = self.received.lock().unwrap();
received.push(event.message.clone());
}
}
#[test]
fn test_event_system_integration() {
let received = Arc::new(std::sync::Mutex::new(Vec::new()));
let listener = TestContextListener {
received: received.clone(),
};
let context = ApplicationContext::new();
context.subscribe_to_events(listener);
let event = TestContextEvent {
message: "test message".to_string(),
};
context.publish_event(&event);
let received_messages = received.lock().unwrap();
assert_eq!(received_messages.len(), 1);
assert_eq!(received_messages[0], "test message");
}
#[test]
fn test_built_in_context_events() {
use crate::event::{
ConfigurationChangedEvent, ContextInitializedEvent, ContextInitializingEvent,
};
use std::sync::{Arc, Mutex};
let initializing_events = Arc::new(Mutex::new(Vec::new()));
let initialized_events = Arc::new(Mutex::new(Vec::new()));
let config_events = Arc::new(Mutex::new(Vec::new()));
struct InitializingListener(Arc<Mutex<Vec<ContextInitializingEvent>>>);
impl EventListener<ContextInitializingEvent> for InitializingListener {
fn on_event(&self, event: &ContextInitializingEvent) {
let mut events = self.0.lock().unwrap();
events.push(event.clone());
}
}
struct InitializedListener(Arc<Mutex<Vec<ContextInitializedEvent>>>);
impl EventListener<ContextInitializedEvent> for InitializedListener {
fn on_event(&self, event: &ContextInitializedEvent) {
let mut events = self.0.lock().unwrap();
events.push(event.clone());
}
}
struct ConfigListener(Arc<Mutex<Vec<ConfigurationChangedEvent>>>);
impl EventListener<ConfigurationChangedEvent> for ConfigListener {
fn on_event(&self, event: &ConfigurationChangedEvent) {
let mut events = self.0.lock().unwrap();
events.push(event.clone());
}
}
let context = ApplicationContext::builder()
.with_property("initial.key", "initial.value")
.build()
.unwrap();
context.subscribe_to_events(InitializingListener(initializing_events.clone()));
context.subscribe_to_events(InitializedListener(initialized_events.clone()));
context.subscribe_to_events(ConfigListener(config_events.clone()));
context.initialize().unwrap();
context.set_config("runtime.key", "runtime.value");
context.set_config("initial.key", "updated.value");
let initializing_events = initializing_events.lock().unwrap();
assert_eq!(initializing_events.len(), 1);
assert!(initializing_events[0].config_sources_count > 0);
let initialized_events = initialized_events.lock().unwrap();
assert_eq!(initialized_events.len(), 1);
assert!(initialized_events[0].config_sources_count > 0);
assert!(initializing_events[0].timestamp <= initialized_events[0].timestamp);
let config_events = config_events.lock().unwrap();
assert_eq!(config_events.len(), 2);
assert_eq!(config_events[0].key, "runtime.key");
assert_eq!(config_events[0].old_value, None);
assert_eq!(config_events[0].new_value, "runtime.value");
assert_eq!(config_events[1].key, "initial.key");
assert_eq!(
config_events[1].old_value,
Some("initial.value".to_string())
);
assert_eq!(config_events[1].new_value, "updated.value");
}
#[test]
fn test_context_aware_event_listeners() {
use crate::event::{ContextAwareEventListener, ContextInitializedEvent};
use std::sync::{Arc, Mutex};
let events_received = Arc::new(Mutex::new(Vec::new()));
let context_data_accessed = Arc::new(Mutex::new(Vec::new()));
struct ContextAwareListener {
events: Arc<Mutex<Vec<String>>>,
context_data: Arc<Mutex<Vec<String>>>,
}
impl ContextAwareEventListener<ContextInitializedEvent> for ContextAwareListener {
fn on_context_event(
&self,
event: &ContextInitializedEvent,
context: &crate::context::ApplicationContext,
) {
let mut events = self.events.lock().unwrap();
events.push(format!(
"Initialized with {} sources",
event.config_sources_count
));
let mut context_data = self.context_data.lock().unwrap();
context_data.push(context.get_config("test.key"));
context_data.push(context.environment());
}
}
let context = ApplicationContext::builder()
.with_property("test.key", "test.value")
.with_property("app.name", "TestApp")
.build()
.unwrap();
let listener = ContextAwareListener {
events: events_received.clone(),
context_data: context_data_accessed.clone(),
};
context.subscribe_to_context_events(listener);
context.initialize().unwrap();
let events = events_received.lock().unwrap();
assert_eq!(events.len(), 1);
assert!(events[0].contains("Initialized with"));
let context_data = context_data_accessed.lock().unwrap();
assert_eq!(context_data.len(), 2);
assert_eq!(context_data[0], "test.value"); assert_eq!(context_data[1], "default"); }
}