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}