khora_core/
service_registry.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A generic, type-safe service locator for engine subsystems.
16//!
17//! The [`ServiceRegistry`] provides a type-map where agents can store and
18//! retrieve shared references to services (e.g., `GraphicsDevice`,
19//! `RenderSystem`) without coupling the [`EngineContext`](crate::EngineContext)
20//! to any specific subsystem.
21//!
22//! # Design
23//!
24//! This follows the **Service Locator** pattern to satisfy the
25//! **Interface Segregation Principle**: each agent fetches only the services
26//! it needs, and adding new services never modifies `EngineContext`.
27
28use std::any::{Any, TypeId};
29use std::collections::HashMap;
30
31/// A generic service registry keyed by [`TypeId`].
32///
33/// Services are stored as `Arc<dyn Any + Send + Sync>` and can be retrieved
34/// by their concrete type via [`get`](ServiceRegistry::get).
35///
36/// # Example
37///
38/// ```rust
39/// use khora_core::service_registry::ServiceRegistry;
40///
41/// struct MyService { value: i32 }
42///
43/// let mut registry = ServiceRegistry::new();
44/// registry.insert(MyService { value: 42 });
45///
46/// let svc = registry.get::<MyService>().unwrap();
47/// assert_eq!(svc.value, 42);
48/// ```
49#[derive(Default)]
50pub struct ServiceRegistry {
51    services: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
52}
53
54impl ServiceRegistry {
55    /// Creates an empty service registry.
56    #[must_use]
57    pub fn new() -> Self {
58        Self {
59            services: HashMap::new(),
60        }
61    }
62
63    /// Inserts a service into the registry, keyed by `T`'s [`TypeId`].
64    ///
65    /// If a service of the same type was already registered, it is replaced.
66    pub fn insert<T: Send + Sync + 'static>(&mut self, service: T) {
67        self.services.insert(TypeId::of::<T>(), Box::new(service));
68    }
69
70    /// Retrieves a shared reference to a previously registered service.
71    ///
72    /// Returns `None` if no service of type `T` has been registered.
73    #[must_use]
74    pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
75        self.services
76            .get(&TypeId::of::<T>())
77            .and_then(|boxed| boxed.downcast_ref::<T>())
78    }
79
80    /// Returns `true` if a service of type `T` is registered.
81    #[must_use]
82    pub fn contains<T: Send + Sync + 'static>(&self) -> bool {
83        self.services.contains_key(&TypeId::of::<T>())
84    }
85
86    /// Returns the number of registered services.
87    #[must_use]
88    pub fn len(&self) -> usize {
89        self.services.len()
90    }
91
92    /// Returns `true` if no services are registered.
93    #[must_use]
94    pub fn is_empty(&self) -> bool {
95        self.services.is_empty()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    struct FakeDevice {
104        name: String,
105    }
106
107    struct FakeRenderer {}
108
109    #[test]
110    fn test_insert_and_get() {
111        let mut registry = ServiceRegistry::new();
112        let device = FakeDevice {
113            name: "GPU-0".to_string(),
114        };
115        registry.insert(device);
116
117        let retrieved = registry.get::<FakeDevice>().unwrap();
118        assert_eq!(retrieved.name, "GPU-0");
119    }
120
121    #[test]
122    fn test_get_missing_returns_none() {
123        let registry = ServiceRegistry::new();
124        assert!(registry.get::<FakeDevice>().is_none());
125    }
126
127    #[test]
128    fn test_multiple_services() {
129        let mut registry = ServiceRegistry::new();
130        registry.insert(FakeDevice {
131            name: "GPU".to_string(),
132        });
133        registry.insert(FakeRenderer {});
134
135        assert_eq!(registry.len(), 2);
136        assert!(registry.contains::<FakeDevice>());
137        assert!(registry.contains::<FakeRenderer>());
138    }
139
140    #[test]
141    fn test_replace_service() {
142        let mut registry = ServiceRegistry::new();
143        registry.insert(FakeDevice {
144            name: "old".to_string(),
145        });
146        registry.insert(FakeDevice {
147            name: "new".to_string(),
148        });
149
150        let retrieved = registry.get::<FakeDevice>().unwrap();
151        assert_eq!(retrieved.name, "new");
152        assert_eq!(registry.len(), 1);
153    }
154
155    #[test]
156    fn test_default_is_empty() {
157        let registry = ServiceRegistry::default();
158        assert!(registry.is_empty());
159    }
160}