Skip to main content

backbone_core/
registry.rs

1//! Module Service Registry - Discovery and access to module services
2//!
3//! The Service Registry provides a centralized way for bounded contexts (modules)
4//! to expose services that other modules can discover and use. This enables
5//! loose-coupled inter-module communication beyond events.
6//!
7//! # Architecture
8//!
9//! ```text
10//!                    ┌────────────────────────────┐
11//!                    │     ServiceRegistry        │
12//!                    │  (Application Bootstrap)   │
13//!                    └────────────────────────────┘
14//!                              │
15//!              ┌───────────────┼───────────────┐
16//!              │               │               │
17//!              ▼               ▼               ▼
18//!     ┌────────────┐  ┌────────────┐  ┌────────────┐
19//!     │  Sapiens   │  │  Postman   │  │  Bucket  │
20//!     │  Services  │  │  Services  │  │  Services  │
21//!     └────────────┘  └────────────┘  └────────────┘
22//! ```
23//!
24//! # Usage
25//!
26//! ```rust,ignore
27//! use backbone_core::registry::{ServiceRegistry, ModuleService};
28//!
29//! // Create registry in main app
30//! let registry = ServiceRegistry::new();
31//!
32//! // Register services from modules
33//! registry.register(user_query_service).await;
34//! registry.register(email_service).await;
35//!
36//! // Look up services
37//! if let Some(service) = registry.get("sapiens.user_query").await {
38//!     // Use the service
39//! }
40//! ```
41
42use std::any::Any;
43use std::collections::HashMap;
44use std::sync::Arc;
45use tokio::sync::RwLock;
46use async_trait::async_trait;
47
48// ============================================================
49// Core Traits
50// ============================================================
51
52/// Trait for module services that can be registered and discovered
53///
54/// Services implement this trait to be registered in the ServiceRegistry.
55/// The registry provides service discovery and health checking.
56///
57/// # Example
58///
59/// ```rust,ignore
60/// use backbone_core::registry::ModuleService;
61///
62/// struct UserQueryService { /* ... */ }
63///
64/// #[async_trait]
65/// impl ModuleService for UserQueryService {
66///     fn service_id(&self) -> &'static str {
67///         "sapiens.user_query"
68///     }
69///
70///     fn service_type(&self) -> &'static str {
71///         "query"
72///     }
73///
74///     fn module_name(&self) -> &'static str {
75///         "sapiens"
76///     }
77///
78///     async fn health_check(&self) -> Result<ServiceHealth, String> {
79///         Ok(ServiceHealth::healthy())
80///     }
81/// }
82/// ```
83#[async_trait]
84pub trait ModuleService: Send + Sync {
85    /// Unique service identifier (e.g., "sapiens.user_query", "postman.email_sender")
86    ///
87    /// Format: `{module}.{service_name}`
88    fn service_id(&self) -> &'static str;
89
90    /// Service type (e.g., "query", "command", "notification")
91    fn service_type(&self) -> &'static str;
92
93    /// Module that owns this service (e.g., "sapiens", "postman")
94    fn module_name(&self) -> &'static str;
95
96    /// Health check for the service
97    async fn health_check(&self) -> Result<ServiceHealth, String>;
98
99    /// Optional: Get the service as Any for downcasting
100    ///
101    /// Override this to enable type-safe service retrieval via `get_typed`.
102    fn as_any(&self) -> Option<&dyn Any> {
103        None
104    }
105}
106
107/// Health status of a service
108#[derive(Debug, Clone, serde::Serialize)]
109pub struct ServiceHealth {
110    pub status: HealthStatus,
111    pub message: Option<String>,
112    pub last_check: chrono::DateTime<chrono::Utc>,
113    pub details: HashMap<String, String>,
114}
115
116impl ServiceHealth {
117    pub fn healthy() -> Self {
118        Self {
119            status: HealthStatus::Healthy,
120            message: None,
121            last_check: chrono::Utc::now(),
122            details: HashMap::new(),
123        }
124    }
125
126    pub fn unhealthy(message: impl Into<String>) -> Self {
127        Self {
128            status: HealthStatus::Unhealthy,
129            message: Some(message.into()),
130            last_check: chrono::Utc::now(),
131            details: HashMap::new(),
132        }
133    }
134
135    pub fn degraded(message: impl Into<String>) -> Self {
136        Self {
137            status: HealthStatus::Degraded,
138            message: Some(message.into()),
139            last_check: chrono::Utc::now(),
140            details: HashMap::new(),
141        }
142    }
143
144    pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
145        self.details.insert(key.into(), value.into());
146        self
147    }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
151#[serde(rename_all = "lowercase")]
152pub enum HealthStatus {
153    Healthy,
154    Degraded,
155    Unhealthy,
156}
157
158// ============================================================
159// Service Registry
160// ============================================================
161
162/// Registry for discovering and accessing module services
163///
164/// The ServiceRegistry provides:
165/// - Service registration from modules
166/// - Service discovery by ID or pattern
167/// - Health checking for all services
168/// - Module-level service grouping
169#[derive(Clone)]
170pub struct ServiceRegistry {
171    services: Arc<RwLock<HashMap<String, Arc<dyn ModuleService>>>>,
172}
173
174impl ServiceRegistry {
175    /// Create a new empty service registry
176    pub fn new() -> Self {
177        Self {
178            services: Arc::new(RwLock::new(HashMap::new())),
179        }
180    }
181
182    /// Register a service
183    ///
184    /// # Arguments
185    /// * `service` - The service to register (must implement ModuleService)
186    pub async fn register<S: ModuleService + 'static>(&self, service: S) {
187        let service_id = service.service_id().to_string();
188        tracing::info!(
189            service_id = %service_id,
190            module = %service.module_name(),
191            service_type = %service.service_type(),
192            "Registering service"
193        );
194        self.services.write().await.insert(service_id, Arc::new(service));
195    }
196
197    /// Register a service wrapped in Arc
198    pub async fn register_arc(&self, service: Arc<dyn ModuleService>) {
199        let service_id = service.service_id().to_string();
200        tracing::info!(
201            service_id = %service_id,
202            module = %service.module_name(),
203            service_type = %service.service_type(),
204            "Registering service (Arc)"
205        );
206        self.services.write().await.insert(service_id, service);
207    }
208
209    /// Get a service by ID
210    ///
211    /// Returns None if the service is not registered.
212    pub async fn get(&self, service_id: &str) -> Option<Arc<dyn ModuleService>> {
213        self.services.read().await.get(service_id).cloned()
214    }
215
216    /// Unregister a service
217    pub async fn unregister(&self, service_id: &str) -> bool {
218        self.services.write().await.remove(service_id).is_some()
219    }
220
221    /// List all registered service IDs
222    pub async fn list(&self) -> Vec<String> {
223        self.services.read().await.keys().cloned().collect()
224    }
225
226    /// List services by module
227    pub async fn list_by_module(&self, module_name: &str) -> Vec<Arc<dyn ModuleService>> {
228        self.services
229            .read()
230            .await
231            .values()
232            .filter(|s| s.module_name() == module_name)
233            .cloned()
234            .collect()
235    }
236
237    /// List services by type
238    pub async fn list_by_type(&self, service_type: &str) -> Vec<Arc<dyn ModuleService>> {
239        self.services
240            .read()
241            .await
242            .values()
243            .filter(|s| s.service_type() == service_type)
244            .cloned()
245            .collect()
246    }
247
248    /// Check if a service is registered
249    pub async fn has(&self, service_id: &str) -> bool {
250        self.services.read().await.contains_key(service_id)
251    }
252
253    /// Get the count of registered services
254    pub async fn count(&self) -> usize {
255        self.services.read().await.len()
256    }
257
258    /// Health check all services
259    pub async fn health_check_all(&self) -> RegistryHealth {
260        let services = self.services.read().await;
261        let mut results = HashMap::new();
262        let mut healthy_count = 0;
263        let mut unhealthy_count = 0;
264
265        for (id, service) in services.iter() {
266            match service.health_check().await {
267                Ok(health) => {
268                    if health.status == HealthStatus::Healthy {
269                        healthy_count += 1;
270                    } else {
271                        unhealthy_count += 1;
272                    }
273                    results.insert(id.clone(), Ok(health));
274                }
275                Err(e) => {
276                    unhealthy_count += 1;
277                    results.insert(id.clone(), Err(e));
278                }
279            }
280        }
281
282        let overall_status = if unhealthy_count == 0 {
283            HealthStatus::Healthy
284        } else if healthy_count > 0 {
285            HealthStatus::Degraded
286        } else {
287            HealthStatus::Unhealthy
288        };
289
290        RegistryHealth {
291            status: overall_status,
292            total_services: services.len(),
293            healthy_count,
294            unhealthy_count,
295            services: results,
296            checked_at: chrono::Utc::now(),
297        }
298    }
299
300    /// Get registry statistics
301    pub async fn statistics(&self) -> RegistryStatistics {
302        let services = self.services.read().await;
303
304        let mut by_module: HashMap<String, usize> = HashMap::new();
305        let mut by_type: HashMap<String, usize> = HashMap::new();
306
307        for service in services.values() {
308            *by_module.entry(service.module_name().to_string()).or_default() += 1;
309            *by_type.entry(service.service_type().to_string()).or_default() += 1;
310        }
311
312        RegistryStatistics {
313            total_services: services.len(),
314            services_by_module: by_module,
315            services_by_type: by_type,
316        }
317    }
318}
319
320impl Default for ServiceRegistry {
321    fn default() -> Self {
322        Self::new()
323    }
324}
325
326/// Health report for the entire registry
327#[derive(Debug, Clone, serde::Serialize)]
328pub struct RegistryHealth {
329    pub status: HealthStatus,
330    pub total_services: usize,
331    pub healthy_count: usize,
332    pub unhealthy_count: usize,
333    #[serde(skip_serializing_if = "HashMap::is_empty")]
334    pub services: HashMap<String, Result<ServiceHealth, String>>,
335    pub checked_at: chrono::DateTime<chrono::Utc>,
336}
337
338/// Statistics about the registry
339#[derive(Debug, Clone, serde::Serialize)]
340pub struct RegistryStatistics {
341    pub total_services: usize,
342    pub services_by_module: HashMap<String, usize>,
343    pub services_by_type: HashMap<String, usize>,
344}
345
346// ============================================================
347// Service Descriptor (for documentation/discovery)
348// ============================================================
349
350/// Metadata about a registered service
351#[derive(Debug, Clone, serde::Serialize)]
352pub struct ServiceDescriptor {
353    pub service_id: String,
354    pub service_type: String,
355    pub module_name: String,
356    pub description: Option<String>,
357    pub version: Option<String>,
358}
359
360impl ServiceDescriptor {
361    pub fn from_service(service: &dyn ModuleService) -> Self {
362        Self {
363            service_id: service.service_id().to_string(),
364            service_type: service.service_type().to_string(),
365            module_name: service.module_name().to_string(),
366            description: None,
367            version: None,
368        }
369    }
370}
371
372// ============================================================
373// Tests
374// ============================================================
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    struct TestService {
381        id: &'static str,
382        healthy: bool,
383    }
384
385    #[async_trait]
386    impl ModuleService for TestService {
387        fn service_id(&self) -> &'static str {
388            self.id
389        }
390
391        fn service_type(&self) -> &'static str {
392            "test"
393        }
394
395        fn module_name(&self) -> &'static str {
396            "test_module"
397        }
398
399        async fn health_check(&self) -> Result<ServiceHealth, String> {
400            if self.healthy {
401                Ok(ServiceHealth::healthy())
402            } else {
403                Err("Service unhealthy".to_string())
404            }
405        }
406    }
407
408    #[tokio::test]
409    async fn test_register_and_get() {
410        let registry = ServiceRegistry::new();
411
412        let service = TestService {
413            id: "test.service",
414            healthy: true,
415        };
416
417        registry.register(service).await;
418
419        assert!(registry.has("test.service").await);
420        assert!(!registry.has("nonexistent").await);
421
422        let retrieved = registry.get("test.service").await;
423        assert!(retrieved.is_some());
424        assert_eq!(retrieved.unwrap().service_id(), "test.service");
425    }
426
427    #[tokio::test]
428    async fn test_list_services() {
429        let registry = ServiceRegistry::new();
430
431        registry.register(TestService { id: "test.a", healthy: true }).await;
432        registry.register(TestService { id: "test.b", healthy: true }).await;
433
434        let list = registry.list().await;
435        assert_eq!(list.len(), 2);
436        assert!(list.contains(&"test.a".to_string()));
437        assert!(list.contains(&"test.b".to_string()));
438    }
439
440    #[tokio::test]
441    async fn test_unregister() {
442        let registry = ServiceRegistry::new();
443
444        registry.register(TestService { id: "test.service", healthy: true }).await;
445        assert!(registry.has("test.service").await);
446
447        let removed = registry.unregister("test.service").await;
448        assert!(removed);
449        assert!(!registry.has("test.service").await);
450    }
451
452    #[tokio::test]
453    async fn test_health_check_all() {
454        let registry = ServiceRegistry::new();
455
456        registry.register(TestService { id: "test.healthy", healthy: true }).await;
457        registry.register(TestService { id: "test.unhealthy", healthy: false }).await;
458
459        let health = registry.health_check_all().await;
460
461        assert_eq!(health.total_services, 2);
462        assert_eq!(health.healthy_count, 1);
463        assert_eq!(health.unhealthy_count, 1);
464        assert_eq!(health.status, HealthStatus::Degraded);
465    }
466
467    #[tokio::test]
468    async fn test_statistics() {
469        let registry = ServiceRegistry::new();
470
471        registry.register(TestService { id: "test.a", healthy: true }).await;
472        registry.register(TestService { id: "test.b", healthy: true }).await;
473
474        let stats = registry.statistics().await;
475
476        assert_eq!(stats.total_services, 2);
477        assert_eq!(stats.services_by_module.get("test_module"), Some(&2));
478        assert_eq!(stats.services_by_type.get("test"), Some(&2));
479    }
480
481    #[tokio::test]
482    async fn test_list_by_module() {
483        let registry = ServiceRegistry::new();
484
485        registry.register(TestService { id: "test.a", healthy: true }).await;
486
487        let by_module = registry.list_by_module("test_module").await;
488        assert_eq!(by_module.len(), 1);
489
490        let by_other = registry.list_by_module("other_module").await;
491        assert!(by_other.is_empty());
492    }
493
494    #[test]
495    fn test_service_health_builders() {
496        let healthy = ServiceHealth::healthy();
497        assert_eq!(healthy.status, HealthStatus::Healthy);
498        assert!(healthy.message.is_none());
499
500        let unhealthy = ServiceHealth::unhealthy("Something went wrong");
501        assert_eq!(unhealthy.status, HealthStatus::Unhealthy);
502        assert_eq!(unhealthy.message, Some("Something went wrong".to_string()));
503
504        let degraded = ServiceHealth::degraded("Slow response")
505            .with_detail("latency_ms", "500");
506        assert_eq!(degraded.status, HealthStatus::Degraded);
507        assert_eq!(degraded.details.get("latency_ms"), Some(&"500".to_string()));
508    }
509}