Skip to main content

this/server/
host.rs

1//! Server host for transport-agnostic API exposure
2//!
3//! This module provides a `ServerHost` structure that contains all framework state
4//! needed to expose the API via any protocol (REST, GraphQL, gRPC, etc.)
5//!
6//! The host is completely agnostic to the transport protocol and serves as the
7//! single source of truth for the application state.
8
9use crate::config::LinksConfig;
10use crate::core::events::EventBus;
11use crate::core::{EntityCreator, EntityFetcher, service::LinkService};
12use crate::events::log::EventLog;
13use crate::events::sinks::SinkRegistry;
14use crate::events::sinks::device_tokens::DeviceTokenStore;
15use crate::events::sinks::in_app::NotificationStore;
16use crate::events::sinks::preferences::NotificationPreferencesStore;
17use crate::links::registry::LinkRouteRegistry;
18use crate::server::entity_registry::EntityRegistry;
19use anyhow::Result;
20use std::collections::HashMap;
21use std::sync::Arc;
22
23/// Host context containing all framework state
24///
25/// This structure is transport-agnostic and contains all the information
26/// needed to expose the API via any protocol (REST, GraphQL, gRPC, etc.)
27///
28/// # Example
29///
30/// ```rust,ignore
31/// let host = ServerHost::from_builder_components(
32///     link_service,
33///     config,
34///     entity_registry,
35///     fetchers,
36///     creators,
37/// )?;
38///
39/// // Use host with any exposure
40/// let host_arc = Arc::new(host);
41/// let rest_app = RestExposure::build_router(host_arc.clone())?;
42/// let graphql_app = GraphQLExposure::build_router(host_arc)?;
43/// ```
44pub struct ServerHost {
45    /// Merged configuration from all modules
46    pub config: Arc<LinksConfig>,
47
48    /// Link service for relationship management
49    pub link_service: Arc<dyn LinkService>,
50
51    /// Link route registry for semantic URL resolution
52    pub registry: Arc<LinkRouteRegistry>,
53
54    /// Entity registry for CRUD routes
55    pub entity_registry: EntityRegistry,
56
57    /// Entity fetchers map (for link enrichment)
58    pub entity_fetchers: Arc<HashMap<String, Arc<dyn EntityFetcher>>>,
59
60    /// Entity creators map (for automatic entity + link creation)
61    pub entity_creators: Arc<HashMap<String, Arc<dyn EntityCreator>>>,
62
63    /// Optional event bus for real-time notifications (WebSocket, SSE)
64    ///
65    /// When present, REST/GraphQL handlers will publish events for mutations.
66    /// WebSocket and other real-time exposures subscribe to this bus.
67    pub event_bus: Option<Arc<EventBus>>,
68
69    /// Optional persistent event log for durable event storage
70    ///
71    /// When present, the EventBus bridges events to this log for replay,
72    /// consumer groups, and FlowRuntime processing.
73    pub event_log: Option<Arc<dyn EventLog>>,
74
75    /// Optional sink registry for event delivery pipelines
76    ///
77    /// Contains all registered sinks (in_app, push, webhook, websocket, counter).
78    /// The FlowRuntime's `deliver` operator uses this to dispatch payloads.
79    pub sink_registry: Option<Arc<SinkRegistry>>,
80
81    /// Optional in-app notification store
82    ///
83    /// Provides list, mark_as_read, unread_count operations for notifications.
84    /// Used by REST/GraphQL/gRPC notification endpoints.
85    pub notification_store: Option<Arc<NotificationStore>>,
86
87    /// Optional device token store for push notifications
88    ///
89    /// Stores push notification tokens (Expo, APNs, FCM) per user.
90    /// Used by the push notification sink and device token endpoints.
91    pub device_token_store: Option<Arc<DeviceTokenStore>>,
92
93    /// Optional notification preferences store
94    ///
95    /// Stores per-user notification preferences (mute, disable types).
96    /// Used by sinks to filter notifications and by preference endpoints.
97    pub preferences_store: Option<Arc<NotificationPreferencesStore>>,
98}
99
100impl ServerHost {
101    /// Build the host from builder components
102    ///
103    /// This method takes all the components that have been registered with
104    /// the builder and constructs the host structure.
105    ///
106    /// # Arguments
107    ///
108    /// * `link_service` - The link service for relationship management
109    /// * `config` - Merged configuration from all modules
110    /// * `entity_registry` - Registry of all entity descriptors
111    /// * `fetchers` - Map of entity type to fetcher implementation
112    /// * `creators` - Map of entity type to creator implementation
113    ///
114    /// # Returns
115    ///
116    /// Returns a `ServerHost` ready to be used with any exposure (REST, GraphQL, gRPC, etc.)
117    pub fn from_builder_components(
118        link_service: Arc<dyn LinkService>,
119        config: LinksConfig,
120        entity_registry: EntityRegistry,
121        fetchers: HashMap<String, Arc<dyn EntityFetcher>>,
122        creators: HashMap<String, Arc<dyn EntityCreator>>,
123    ) -> Result<Self> {
124        let config = Arc::new(config);
125        let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
126
127        Ok(Self {
128            config,
129            link_service,
130            registry,
131            entity_registry,
132            entity_fetchers: Arc::new(fetchers),
133            entity_creators: Arc::new(creators),
134            event_bus: None,
135            event_log: None,
136            sink_registry: None,
137            notification_store: None,
138            device_token_store: None,
139            preferences_store: None,
140        })
141    }
142
143    /// Get entity types registered in the host
144    pub fn entity_types(&self) -> Vec<&str> {
145        self.entity_registry.entity_types()
146    }
147
148    /// Check if host is properly initialized
149    pub fn is_ready(&self) -> bool {
150        !self.entity_fetchers.is_empty()
151    }
152
153    /// Set the event bus for real-time notifications
154    pub fn with_event_bus(mut self, event_bus: EventBus) -> Self {
155        self.event_bus = Some(Arc::new(event_bus));
156        self
157    }
158
159    /// Get a reference to the event bus (if configured)
160    pub fn event_bus(&self) -> Option<&Arc<EventBus>> {
161        self.event_bus.as_ref()
162    }
163
164    /// Set the persistent event log
165    pub fn with_event_log(mut self, event_log: Arc<dyn EventLog>) -> Self {
166        self.event_log = Some(event_log);
167        self
168    }
169
170    /// Get a reference to the event log (if configured)
171    pub fn event_log(&self) -> Option<&Arc<dyn EventLog>> {
172        self.event_log.as_ref()
173    }
174
175    /// Set the sink registry
176    pub fn with_sink_registry(mut self, registry: SinkRegistry) -> Self {
177        self.sink_registry = Some(Arc::new(registry));
178        self
179    }
180
181    /// Get a reference to the sink registry (if configured)
182    pub fn sink_registry(&self) -> Option<&Arc<SinkRegistry>> {
183        self.sink_registry.as_ref()
184    }
185
186    /// Set the notification store
187    pub fn with_notification_store(mut self, store: Arc<NotificationStore>) -> Self {
188        self.notification_store = Some(store);
189        self
190    }
191
192    /// Get a reference to the notification store (if configured)
193    pub fn notification_store(&self) -> Option<&Arc<NotificationStore>> {
194        self.notification_store.as_ref()
195    }
196
197    /// Set the device token store
198    pub fn with_device_token_store(mut self, store: Arc<DeviceTokenStore>) -> Self {
199        self.device_token_store = Some(store);
200        self
201    }
202
203    /// Get a reference to the device token store (if configured)
204    pub fn device_token_store(&self) -> Option<&Arc<DeviceTokenStore>> {
205        self.device_token_store.as_ref()
206    }
207
208    /// Set the notification preferences store
209    pub fn with_preferences_store(mut self, store: Arc<NotificationPreferencesStore>) -> Self {
210        self.preferences_store = Some(store);
211        self
212    }
213
214    /// Get a reference to the notification preferences store (if configured)
215    pub fn preferences_store(&self) -> Option<&Arc<NotificationPreferencesStore>> {
216        self.preferences_store.as_ref()
217    }
218
219    /// Create a minimal `ServerHost` for unit tests.
220    ///
221    /// Has empty registries and a mock `LinkService`. Useful for testing
222    /// services that only need the `EventBus` (e.g., `EventServiceImpl`).
223    #[cfg(test)]
224    pub fn minimal_for_test() -> Self {
225        use crate::core::link::LinkEntity;
226
227        struct NoopLinkService;
228
229        #[async_trait::async_trait]
230        impl crate::core::service::LinkService for NoopLinkService {
231            async fn create(&self, link: LinkEntity) -> anyhow::Result<LinkEntity> {
232                Ok(link)
233            }
234            async fn get(&self, _id: &uuid::Uuid) -> anyhow::Result<Option<LinkEntity>> {
235                Ok(None)
236            }
237            async fn list(&self) -> anyhow::Result<Vec<LinkEntity>> {
238                Ok(vec![])
239            }
240            async fn find_by_source(
241                &self,
242                _source_id: &uuid::Uuid,
243                _link_type: Option<&str>,
244                _target_type: Option<&str>,
245            ) -> anyhow::Result<Vec<LinkEntity>> {
246                Ok(vec![])
247            }
248            async fn find_by_target(
249                &self,
250                _target_id: &uuid::Uuid,
251                _link_type: Option<&str>,
252                _source_type: Option<&str>,
253            ) -> anyhow::Result<Vec<LinkEntity>> {
254                Ok(vec![])
255            }
256            async fn update(
257                &self,
258                _id: &uuid::Uuid,
259                link: LinkEntity,
260            ) -> anyhow::Result<LinkEntity> {
261                Ok(link)
262            }
263            async fn delete(&self, _id: &uuid::Uuid) -> anyhow::Result<()> {
264                Ok(())
265            }
266            async fn delete_by_entity(&self, _entity_id: &uuid::Uuid) -> anyhow::Result<()> {
267                Ok(())
268            }
269        }
270
271        let config = LinksConfig {
272            entities: vec![],
273            links: vec![],
274            validation_rules: None,
275            events: None,
276            sinks: None,
277        };
278        let config = Arc::new(config);
279        let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
280
281        Self {
282            config,
283            link_service: Arc::new(NoopLinkService),
284            registry,
285            entity_registry: EntityRegistry::new(),
286            entity_fetchers: Arc::new(HashMap::new()),
287            entity_creators: Arc::new(HashMap::new()),
288            event_bus: None,
289            event_log: None,
290            sink_registry: None,
291            notification_store: None,
292            device_token_store: None,
293            preferences_store: None,
294        }
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use crate::config::{EntityAuthConfig, EntityConfig};
302    use crate::core::link::LinkEntity;
303
304    /// Minimal mock LinkService for testing
305    struct MockLinkService;
306
307    #[async_trait::async_trait]
308    impl crate::core::service::LinkService for MockLinkService {
309        async fn create(&self, link: LinkEntity) -> anyhow::Result<LinkEntity> {
310            Ok(link)
311        }
312        async fn get(&self, _id: &uuid::Uuid) -> anyhow::Result<Option<LinkEntity>> {
313            Ok(None)
314        }
315        async fn list(&self) -> anyhow::Result<Vec<LinkEntity>> {
316            Ok(vec![])
317        }
318        async fn find_by_source(
319            &self,
320            _source_id: &uuid::Uuid,
321            _link_type: Option<&str>,
322            _target_type: Option<&str>,
323        ) -> anyhow::Result<Vec<LinkEntity>> {
324            Ok(vec![])
325        }
326        async fn find_by_target(
327            &self,
328            _target_id: &uuid::Uuid,
329            _link_type: Option<&str>,
330            _source_type: Option<&str>,
331        ) -> anyhow::Result<Vec<LinkEntity>> {
332            Ok(vec![])
333        }
334        async fn update(&self, _id: &uuid::Uuid, link: LinkEntity) -> anyhow::Result<LinkEntity> {
335            Ok(link)
336        }
337        async fn delete(&self, _id: &uuid::Uuid) -> anyhow::Result<()> {
338            Ok(())
339        }
340        async fn delete_by_entity(&self, _entity_id: &uuid::Uuid) -> anyhow::Result<()> {
341            Ok(())
342        }
343    }
344
345    fn test_config() -> LinksConfig {
346        LinksConfig {
347            entities: vec![EntityConfig {
348                singular: "order".to_string(),
349                plural: "orders".to_string(),
350                auth: EntityAuthConfig::default(),
351            }],
352            links: vec![],
353            validation_rules: None,
354            events: None,
355            sinks: None,
356        }
357    }
358
359    fn make_host() -> ServerHost {
360        ServerHost::from_builder_components(
361            Arc::new(MockLinkService),
362            test_config(),
363            EntityRegistry::new(),
364            HashMap::new(),
365            HashMap::new(),
366        )
367        .expect("should build host")
368    }
369
370    #[test]
371    fn test_from_builder_components_creates_host() {
372        let host = make_host();
373        assert!(host.event_bus.is_none());
374    }
375
376    #[test]
377    fn test_entity_types_empty_registry() {
378        let host = make_host();
379        assert!(host.entity_types().is_empty());
380    }
381
382    #[test]
383    fn test_is_ready_no_fetchers_returns_false() {
384        let host = make_host();
385        assert!(!host.is_ready());
386    }
387
388    #[test]
389    fn test_with_event_bus_sets_bus() {
390        let host = make_host();
391        let bus = EventBus::new(16);
392        let host = host.with_event_bus(bus);
393        assert!(host.event_bus().is_some());
394    }
395
396    #[test]
397    fn test_event_bus_none_by_default() {
398        let host = make_host();
399        assert!(host.event_bus().is_none());
400    }
401
402    #[test]
403    fn test_config_accessible_from_host() {
404        let host = make_host();
405        assert_eq!(host.config.entities.len(), 1);
406        assert_eq!(host.config.entities[0].singular, "order");
407    }
408
409    #[test]
410    fn test_registry_built_from_config() {
411        let host = make_host();
412        // Registry should exist and be built from config
413        let routes = host.registry.list_routes_for_entity("order");
414        // No links → no routes, but it shouldn't panic
415        assert!(routes.is_empty());
416    }
417
418    #[test]
419    fn test_is_ready_with_fetchers_returns_true() {
420        use crate::core::EntityFetcher;
421
422        struct StubFetcher;
423
424        #[async_trait::async_trait]
425        impl EntityFetcher for StubFetcher {
426            async fn fetch_as_json(
427                &self,
428                _entity_id: &uuid::Uuid,
429            ) -> anyhow::Result<serde_json::Value> {
430                Ok(serde_json::json!({}))
431            }
432        }
433
434        let mut fetchers: HashMap<String, Arc<dyn EntityFetcher>> = HashMap::new();
435        fetchers.insert("order".to_string(), Arc::new(StubFetcher));
436
437        let host = ServerHost::from_builder_components(
438            Arc::new(MockLinkService),
439            test_config(),
440            EntityRegistry::new(),
441            fetchers,
442            HashMap::new(),
443        )
444        .expect("should build host");
445
446        assert!(host.is_ready());
447    }
448
449    #[test]
450    fn test_entity_creators_accessible() {
451        let host = make_host();
452        assert!(host.entity_creators.is_empty());
453    }
454
455    #[test]
456    fn test_link_service_accessible() {
457        let host = make_host();
458        // link_service should be accessible (Arc<dyn LinkService>)
459        let _ = host.link_service.clone();
460    }
461
462    #[test]
463    fn test_new_fields_none_by_default() {
464        let host = make_host();
465        assert!(host.event_log().is_none());
466        assert!(host.sink_registry().is_none());
467        assert!(host.notification_store().is_none());
468        assert!(host.device_token_store().is_none());
469        assert!(host.preferences_store().is_none());
470    }
471
472    #[test]
473    fn test_with_notification_store() {
474        use crate::events::sinks::in_app::NotificationStore;
475        let host = make_host();
476        let store = Arc::new(NotificationStore::new());
477        let host = host.with_notification_store(store);
478        assert!(host.notification_store().is_some());
479    }
480
481    #[test]
482    fn test_with_device_token_store() {
483        use crate::events::sinks::device_tokens::DeviceTokenStore;
484        let host = make_host();
485        let store = Arc::new(DeviceTokenStore::new());
486        let host = host.with_device_token_store(store);
487        assert!(host.device_token_store().is_some());
488    }
489
490    #[test]
491    fn test_with_preferences_store() {
492        use crate::events::sinks::preferences::NotificationPreferencesStore;
493        let host = make_host();
494        let store = Arc::new(NotificationPreferencesStore::new());
495        let host = host.with_preferences_store(store);
496        assert!(host.preferences_store().is_some());
497    }
498
499    #[test]
500    fn test_with_sink_registry() {
501        use crate::events::sinks::SinkRegistry;
502        let host = make_host();
503        let registry = SinkRegistry::new();
504        let host = host.with_sink_registry(registry);
505        assert!(host.sink_registry().is_some());
506    }
507
508    #[test]
509    fn test_with_event_log() {
510        use crate::events::memory::InMemoryEventLog;
511        let host = make_host();
512        let log = Arc::new(InMemoryEventLog::new());
513        let host = host.with_event_log(log);
514        assert!(host.event_log().is_some());
515    }
516}