use crate::config::LinksConfig;
use crate::core::events::EventBus;
use crate::core::{EntityCreator, EntityFetcher, service::LinkService};
use crate::events::log::EventLog;
use crate::events::sinks::SinkRegistry;
use crate::events::sinks::device_tokens::DeviceTokenStore;
use crate::events::sinks::in_app::NotificationStore;
use crate::events::sinks::preferences::NotificationPreferencesStore;
use crate::links::registry::LinkRouteRegistry;
use crate::server::entity_registry::EntityRegistry;
use anyhow::Result;
use std::collections::HashMap;
use std::sync::Arc;
pub struct ServerHost {
pub config: Arc<LinksConfig>,
pub link_service: Arc<dyn LinkService>,
pub registry: Arc<LinkRouteRegistry>,
pub entity_registry: EntityRegistry,
pub entity_fetchers: Arc<HashMap<String, Arc<dyn EntityFetcher>>>,
pub entity_creators: Arc<HashMap<String, Arc<dyn EntityCreator>>>,
pub event_bus: Option<Arc<EventBus>>,
pub event_log: Option<Arc<dyn EventLog>>,
pub sink_registry: Option<Arc<SinkRegistry>>,
pub notification_store: Option<Arc<NotificationStore>>,
pub device_token_store: Option<Arc<DeviceTokenStore>>,
pub preferences_store: Option<Arc<NotificationPreferencesStore>>,
}
impl ServerHost {
pub fn from_builder_components(
link_service: Arc<dyn LinkService>,
config: LinksConfig,
entity_registry: EntityRegistry,
fetchers: HashMap<String, Arc<dyn EntityFetcher>>,
creators: HashMap<String, Arc<dyn EntityCreator>>,
) -> Result<Self> {
let config = Arc::new(config);
let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
Ok(Self {
config,
link_service,
registry,
entity_registry,
entity_fetchers: Arc::new(fetchers),
entity_creators: Arc::new(creators),
event_bus: None,
event_log: None,
sink_registry: None,
notification_store: None,
device_token_store: None,
preferences_store: None,
})
}
pub fn entity_types(&self) -> Vec<&str> {
self.entity_registry.entity_types()
}
pub fn is_ready(&self) -> bool {
!self.entity_fetchers.is_empty()
}
pub fn with_event_bus(mut self, event_bus: EventBus) -> Self {
self.event_bus = Some(Arc::new(event_bus));
self
}
pub fn event_bus(&self) -> Option<&Arc<EventBus>> {
self.event_bus.as_ref()
}
pub fn with_event_log(mut self, event_log: Arc<dyn EventLog>) -> Self {
self.event_log = Some(event_log);
self
}
pub fn event_log(&self) -> Option<&Arc<dyn EventLog>> {
self.event_log.as_ref()
}
pub fn with_sink_registry(mut self, registry: SinkRegistry) -> Self {
self.sink_registry = Some(Arc::new(registry));
self
}
pub fn sink_registry(&self) -> Option<&Arc<SinkRegistry>> {
self.sink_registry.as_ref()
}
pub fn with_notification_store(mut self, store: Arc<NotificationStore>) -> Self {
self.notification_store = Some(store);
self
}
pub fn notification_store(&self) -> Option<&Arc<NotificationStore>> {
self.notification_store.as_ref()
}
pub fn with_device_token_store(mut self, store: Arc<DeviceTokenStore>) -> Self {
self.device_token_store = Some(store);
self
}
pub fn device_token_store(&self) -> Option<&Arc<DeviceTokenStore>> {
self.device_token_store.as_ref()
}
pub fn with_preferences_store(mut self, store: Arc<NotificationPreferencesStore>) -> Self {
self.preferences_store = Some(store);
self
}
pub fn preferences_store(&self) -> Option<&Arc<NotificationPreferencesStore>> {
self.preferences_store.as_ref()
}
#[cfg(test)]
pub fn minimal_for_test() -> Self {
use crate::core::link::LinkEntity;
struct NoopLinkService;
#[async_trait::async_trait]
impl crate::core::service::LinkService for NoopLinkService {
async fn create(&self, link: LinkEntity) -> anyhow::Result<LinkEntity> {
Ok(link)
}
async fn get(&self, _id: &uuid::Uuid) -> anyhow::Result<Option<LinkEntity>> {
Ok(None)
}
async fn list(&self) -> anyhow::Result<Vec<LinkEntity>> {
Ok(vec![])
}
async fn find_by_source(
&self,
_source_id: &uuid::Uuid,
_link_type: Option<&str>,
_target_type: Option<&str>,
) -> anyhow::Result<Vec<LinkEntity>> {
Ok(vec![])
}
async fn find_by_target(
&self,
_target_id: &uuid::Uuid,
_link_type: Option<&str>,
_source_type: Option<&str>,
) -> anyhow::Result<Vec<LinkEntity>> {
Ok(vec![])
}
async fn update(
&self,
_id: &uuid::Uuid,
link: LinkEntity,
) -> anyhow::Result<LinkEntity> {
Ok(link)
}
async fn delete(&self, _id: &uuid::Uuid) -> anyhow::Result<()> {
Ok(())
}
async fn delete_by_entity(&self, _entity_id: &uuid::Uuid) -> anyhow::Result<()> {
Ok(())
}
}
let config = LinksConfig {
entities: vec![],
links: vec![],
validation_rules: None,
events: None,
sinks: None,
};
let config = Arc::new(config);
let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
Self {
config,
link_service: Arc::new(NoopLinkService),
registry,
entity_registry: EntityRegistry::new(),
entity_fetchers: Arc::new(HashMap::new()),
entity_creators: Arc::new(HashMap::new()),
event_bus: None,
event_log: None,
sink_registry: None,
notification_store: None,
device_token_store: None,
preferences_store: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{EntityAuthConfig, EntityConfig};
use crate::core::link::LinkEntity;
struct MockLinkService;
#[async_trait::async_trait]
impl crate::core::service::LinkService for MockLinkService {
async fn create(&self, link: LinkEntity) -> anyhow::Result<LinkEntity> {
Ok(link)
}
async fn get(&self, _id: &uuid::Uuid) -> anyhow::Result<Option<LinkEntity>> {
Ok(None)
}
async fn list(&self) -> anyhow::Result<Vec<LinkEntity>> {
Ok(vec![])
}
async fn find_by_source(
&self,
_source_id: &uuid::Uuid,
_link_type: Option<&str>,
_target_type: Option<&str>,
) -> anyhow::Result<Vec<LinkEntity>> {
Ok(vec![])
}
async fn find_by_target(
&self,
_target_id: &uuid::Uuid,
_link_type: Option<&str>,
_source_type: Option<&str>,
) -> anyhow::Result<Vec<LinkEntity>> {
Ok(vec![])
}
async fn update(&self, _id: &uuid::Uuid, link: LinkEntity) -> anyhow::Result<LinkEntity> {
Ok(link)
}
async fn delete(&self, _id: &uuid::Uuid) -> anyhow::Result<()> {
Ok(())
}
async fn delete_by_entity(&self, _entity_id: &uuid::Uuid) -> anyhow::Result<()> {
Ok(())
}
}
fn test_config() -> LinksConfig {
LinksConfig {
entities: vec![EntityConfig {
singular: "order".to_string(),
plural: "orders".to_string(),
auth: EntityAuthConfig::default(),
}],
links: vec![],
validation_rules: None,
events: None,
sinks: None,
}
}
fn make_host() -> ServerHost {
ServerHost::from_builder_components(
Arc::new(MockLinkService),
test_config(),
EntityRegistry::new(),
HashMap::new(),
HashMap::new(),
)
.expect("should build host")
}
#[test]
fn test_from_builder_components_creates_host() {
let host = make_host();
assert!(host.event_bus.is_none());
}
#[test]
fn test_entity_types_empty_registry() {
let host = make_host();
assert!(host.entity_types().is_empty());
}
#[test]
fn test_is_ready_no_fetchers_returns_false() {
let host = make_host();
assert!(!host.is_ready());
}
#[test]
fn test_with_event_bus_sets_bus() {
let host = make_host();
let bus = EventBus::new(16);
let host = host.with_event_bus(bus);
assert!(host.event_bus().is_some());
}
#[test]
fn test_event_bus_none_by_default() {
let host = make_host();
assert!(host.event_bus().is_none());
}
#[test]
fn test_config_accessible_from_host() {
let host = make_host();
assert_eq!(host.config.entities.len(), 1);
assert_eq!(host.config.entities[0].singular, "order");
}
#[test]
fn test_registry_built_from_config() {
let host = make_host();
let routes = host.registry.list_routes_for_entity("order");
assert!(routes.is_empty());
}
#[test]
fn test_is_ready_with_fetchers_returns_true() {
use crate::core::EntityFetcher;
struct StubFetcher;
#[async_trait::async_trait]
impl EntityFetcher for StubFetcher {
async fn fetch_as_json(
&self,
_entity_id: &uuid::Uuid,
) -> anyhow::Result<serde_json::Value> {
Ok(serde_json::json!({}))
}
}
let mut fetchers: HashMap<String, Arc<dyn EntityFetcher>> = HashMap::new();
fetchers.insert("order".to_string(), Arc::new(StubFetcher));
let host = ServerHost::from_builder_components(
Arc::new(MockLinkService),
test_config(),
EntityRegistry::new(),
fetchers,
HashMap::new(),
)
.expect("should build host");
assert!(host.is_ready());
}
#[test]
fn test_entity_creators_accessible() {
let host = make_host();
assert!(host.entity_creators.is_empty());
}
#[test]
fn test_link_service_accessible() {
let host = make_host();
let _ = host.link_service.clone();
}
#[test]
fn test_new_fields_none_by_default() {
let host = make_host();
assert!(host.event_log().is_none());
assert!(host.sink_registry().is_none());
assert!(host.notification_store().is_none());
assert!(host.device_token_store().is_none());
assert!(host.preferences_store().is_none());
}
#[test]
fn test_with_notification_store() {
use crate::events::sinks::in_app::NotificationStore;
let host = make_host();
let store = Arc::new(NotificationStore::new());
let host = host.with_notification_store(store);
assert!(host.notification_store().is_some());
}
#[test]
fn test_with_device_token_store() {
use crate::events::sinks::device_tokens::DeviceTokenStore;
let host = make_host();
let store = Arc::new(DeviceTokenStore::new());
let host = host.with_device_token_store(store);
assert!(host.device_token_store().is_some());
}
#[test]
fn test_with_preferences_store() {
use crate::events::sinks::preferences::NotificationPreferencesStore;
let host = make_host();
let store = Arc::new(NotificationPreferencesStore::new());
let host = host.with_preferences_store(store);
assert!(host.preferences_store().is_some());
}
#[test]
fn test_with_sink_registry() {
use crate::events::sinks::SinkRegistry;
let host = make_host();
let registry = SinkRegistry::new();
let host = host.with_sink_registry(registry);
assert!(host.sink_registry().is_some());
}
#[test]
fn test_with_event_log() {
use crate::events::memory::InMemoryEventLog;
let host = make_host();
let log = Arc::new(InMemoryEventLog::new());
let host = host.with_event_log(log);
assert!(host.event_log().is_some());
}
}