khora_core/runtime/
services.rs1use std::any::{type_name, Any, TypeId};
22use std::collections::HashMap;
23use std::sync::Arc;
24
25#[derive(Default)]
36pub struct Services {
37 inner: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
38 parent: Option<Arc<Services>>,
39}
40
41impl Services {
42 #[must_use]
44 pub fn new() -> Self {
45 Self {
46 inner: HashMap::new(),
47 parent: None,
48 }
49 }
50
51 #[must_use]
54 pub fn with_parent(parent: Arc<Services>) -> Self {
55 Self {
56 inner: HashMap::new(),
57 parent: Some(parent),
58 }
59 }
60
61 pub fn insert<T: Send + Sync + 'static>(&mut self, service: T) {
64 self.inner.insert(TypeId::of::<T>(), Box::new(service));
65 }
66
67 #[must_use]
70 pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
71 self.inner
72 .get(&TypeId::of::<T>())
73 .and_then(|b| b.downcast_ref::<T>())
74 .or_else(|| self.parent.as_deref()?.get::<T>())
75 }
76
77 pub fn require<T: Send + Sync + 'static>(&self) -> &T {
79 self.get::<T>().unwrap_or_else(|| {
80 panic!(
81 "Services: required service `{}` is not registered",
82 type_name::<T>()
83 )
84 })
85 }
86
87 #[must_use]
89 pub fn contains<T: Send + Sync + 'static>(&self) -> bool {
90 self.inner.contains_key(&TypeId::of::<T>())
91 || self
92 .parent
93 .as_deref()
94 .map(|p| p.contains::<T>())
95 .unwrap_or(false)
96 }
97
98 #[must_use]
100 pub fn len(&self) -> usize {
101 self.inner.len()
102 }
103
104 #[must_use]
106 pub fn is_empty(&self) -> bool {
107 self.inner.is_empty()
108 }
109}
110
111impl std::fmt::Debug for Services {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.debug_struct("Services")
114 .field("registered", &self.inner.len())
115 .field("has_parent", &self.parent.is_some())
116 .finish()
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 struct FakeAssetService {
125 name: String,
126 }
127 struct FakeSerializationService;
128
129 #[test]
130 fn insert_and_get() {
131 let mut s = Services::new();
132 s.insert(FakeAssetService {
133 name: "assets".into(),
134 });
135 assert_eq!(s.get::<FakeAssetService>().unwrap().name, "assets");
136 }
137
138 #[test]
139 fn require_panics_when_absent() {
140 let s = Services::new();
141 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
142 let _ = s.require::<FakeAssetService>().name.len();
143 }));
144 assert!(result.is_err());
145 }
146
147 #[test]
148 fn parent_chain_delegates() {
149 let mut parent = Services::new();
150 parent.insert(FakeAssetService {
151 name: "from-parent".into(),
152 });
153 let child = Services::with_parent(Arc::new(parent));
154 assert_eq!(child.get::<FakeAssetService>().unwrap().name, "from-parent");
155 }
156
157 #[test]
158 fn local_shadows_parent() {
159 let mut parent = Services::new();
160 parent.insert(FakeAssetService {
161 name: "parent".into(),
162 });
163 let mut child = Services::with_parent(Arc::new(parent));
164 child.insert(FakeAssetService {
165 name: "child".into(),
166 });
167 assert_eq!(child.get::<FakeAssetService>().unwrap().name, "child");
168 }
169
170 #[test]
171 fn contains_walks_parent() {
172 let mut parent = Services::new();
173 parent.insert(FakeSerializationService);
174 let child = Services::with_parent(Arc::new(parent));
175 assert!(child.contains::<FakeSerializationService>());
176 }
177}