//! Runtime-neutral singleton registration for native `GridClient` composition. use std::sync::{Arc, Mutex}; use crate::{Error, GridClient, Settings}; /// Factory stored by service collections for lazy `GridClient` construction. pub type GridClientFactory = Arc Result + Send + Sync>; /// Object-safe service collection contract used by the mapped DI extension. pub trait IServiceCollection: Send { /// Registers the concrete `GridClient` singleton factory. fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error>; /// Registers `IGridClient` as an alias of the concrete singleton. fn add_i_grid_client_alias(&mut self) -> Result<(), Error>; } /// Small native service collection suitable for applications without a DI crate. #[derive(Default)] pub struct ServiceCollection { client_factory: Option, client: Mutex>, interface_alias: bool, } impl ServiceCollection { /// Resolves the lazily-created concrete client singleton. pub fn resolve_grid_client(&self) -> Result { let mut singleton = self .client .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if let Some(client) = singleton.as_ref() { return Ok(client.clone()); } let factory = self .client_factory .as_ref() .ok_or(Error::InvalidOperation)?; let client = factory()?; *singleton = Some(client.clone()); Ok(client) } /// Resolves the interface registration to the same native singleton. pub fn resolve_i_grid_client(&self) -> Result { if !self.interface_alias { return Err(Error::InvalidOperation); } self.resolve_grid_client() } } impl IServiceCollection for ServiceCollection { fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error> { if self.client_factory.is_some() { return Err(Error::InvalidOperation); } self.client_factory = Some(factory); Ok(()) } fn add_i_grid_client_alias(&mut self) -> Result<(), Error> { if self.client_factory.is_none() || self.interface_alias { return Err(Error::InvalidOperation); } self.interface_alias = true; Ok(()) } } pub(crate) fn add_grid_client( mut services: Box, configure: Option>, ) -> Result, Error> { let configure: Option> = configure.map(Arc::from); let factory: GridClientFactory = Arc::new(move || { let mut settings = Settings::default(); if let Some(configure) = &configure { configure(&mut settings); } GridClient::builder() .with_settings(settings) .build() .map_err(Into::into) }); services.add_grid_client_singleton(factory)?; services.add_i_grid_client_alias()?; Ok(services) } #[cfg(test)] mod tests { use super::*; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; #[derive(Default)] struct RecordingCollection { factory: Arc>>, alias_registered: Arc, } impl IServiceCollection for RecordingCollection { fn add_grid_client_singleton(&mut self, factory: GridClientFactory) -> Result<(), Error> { *self .factory .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(factory); Ok(()) } fn add_i_grid_client_alias(&mut self) -> Result<(), Error> { self.alias_registered.store(true, Ordering::Relaxed); Ok(()) } } #[test] fn registers_one_lazy_client_and_interface_alias() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); let collection = RecordingCollection::default(); let recorded_factory = Arc::clone(&collection.factory); let alias_registered = Arc::clone(&collection.alias_registered); let _services = add_grid_client( Box::new(collection), Some(Box::new(move |settings| { observed.fetch_add(1, Ordering::Relaxed); settings.default_effect_color.r = 0.25; })), ) .unwrap(); assert!(alias_registered.load(Ordering::Relaxed)); assert_eq!(calls.load(Ordering::Relaxed), 0); let factory = recorded_factory .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() .expect("GridClient factory must be registered"); let client = factory().unwrap(); assert_eq!(calls.load(Ordering::Relaxed), 1); assert_eq!(client.settings_ref().default_effect_color.r, 0.25); } #[test] fn concrete_collection_resolves_same_runtime() { let calls = Arc::new(AtomicUsize::new(0)); let observed = Arc::clone(&calls); let mut services = ServiceCollection::default(); services .add_grid_client_singleton(Arc::new(move || { observed.fetch_add(1, Ordering::Relaxed); GridClient::builder().build().map_err(Into::into) })) .unwrap(); services.add_i_grid_client_alias().unwrap(); let concrete = services.resolve_grid_client().unwrap(); let interface = services.resolve_i_grid_client().unwrap(); assert_eq!(concrete.settings_ref(), interface.settings_ref()); assert_eq!(calls.load(Ordering::Relaxed), 1); } }