1use parking_lot::RwLock;
19use std::{any::Any, collections::HashMap, fmt, sync::Arc};
20
21#[derive(Clone, Eq, PartialEq, Hash)]
23pub struct TypeKey(&'static str);
24
25impl TypeKey {
26 #[inline]
27 fn of<T: ?Sized + 'static>() -> Self {
28 TypeKey(std::any::type_name::<T>())
29 }
30}
31
32impl fmt::Debug for TypeKey {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.write_str(self.0)
35 }
36}
37
38#[derive(Clone, Eq, PartialEq, Hash)]
43pub struct ClientScope(Arc<str>);
44
45impl ClientScope {
46 #[inline]
48 #[must_use]
49 pub fn new(scope: impl Into<Arc<str>>) -> Self {
50 Self(scope.into())
51 }
52
53 #[must_use]
57 pub fn gts_id(gts_id: &str) -> Self {
58 let mut s = String::with_capacity("gts:".len() + gts_id.len());
59 s.push_str("gts:");
60 s.push_str(gts_id);
61 Self(Arc::<str>::from(s))
62 }
63
64 #[inline]
65 #[must_use]
66 pub fn as_str(&self) -> &str {
67 &self.0
68 }
69}
70
71impl fmt::Debug for ClientScope {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 f.write_str(self.as_str())
74 }
75}
76
77#[derive(Clone, Eq, PartialEq, Hash)]
78struct ScopedKey {
79 type_key: TypeKey,
80 scope: ClientScope,
81}
82
83impl fmt::Debug for ScopedKey {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.debug_struct("ScopedKey")
86 .field("type_key", &self.type_key)
87 .field("scope", &self.scope)
88 .finish()
89 }
90}
91
92#[derive(Debug, thiserror::Error)]
93pub enum ClientHubError {
94 #[error("client not found: type={type_key:?}")]
95 NotFound { type_key: TypeKey },
96
97 #[error("type mismatch in hub for type={type_key:?}")]
98 TypeMismatch { type_key: TypeKey },
99
100 #[error("scoped client not found: type={type_key:?} scope={scope:?}")]
101 ScopedNotFound {
102 type_key: TypeKey,
103 scope: ClientScope,
104 },
105
106 #[error("type mismatch in hub for type={type_key:?} scope={scope:?}")]
107 ScopedTypeMismatch {
108 type_key: TypeKey,
109 scope: ClientScope,
110 },
111}
112
113type Boxed = Box<dyn Any + Send + Sync>;
114
115type ClientMap = HashMap<TypeKey, Boxed>;
117
118type ScopedClientMap = HashMap<ScopedKey, Boxed>;
120
121#[derive(Default)]
123pub struct ClientHub {
124 map: RwLock<ClientMap>,
125 scoped_map: RwLock<ScopedClientMap>,
126}
127
128impl ClientHub {
129 #[inline]
130 #[must_use]
131 pub fn new() -> Self {
132 Self {
133 map: RwLock::new(HashMap::new()),
134 scoped_map: RwLock::new(HashMap::new()),
135 }
136 }
137}
138
139impl ClientHub {
140 pub fn register<T>(&self, client: Arc<T>)
143 where
144 T: ?Sized + Send + Sync + 'static,
145 {
146 let type_key = TypeKey::of::<T>();
147 let mut w = self.map.write();
148 w.insert(type_key, Box::new(client));
149 }
150
151 pub fn register_scoped<T>(&self, scope: ClientScope, client: Arc<T>)
156 where
157 T: ?Sized + Send + Sync + 'static,
158 {
159 let key = ScopedKey {
160 type_key: TypeKey::of::<T>(),
161 scope,
162 };
163 let mut w = self.scoped_map.write();
164 w.insert(key, Box::new(client));
165 }
166
167 pub fn get<T>(&self) -> Result<Arc<T>, ClientHubError>
173 where
174 T: ?Sized + Send + Sync + 'static,
175 {
176 let type_key = TypeKey::of::<T>();
177 let r = self.map.read();
178
179 let boxed = r.get(&type_key).ok_or(ClientHubError::NotFound {
180 type_key: type_key.clone(),
181 })?;
182
183 if let Some(arc_t) = boxed.downcast_ref::<Arc<T>>() {
185 return Ok(arc_t.clone());
186 }
187 Err(ClientHubError::TypeMismatch { type_key })
188 }
189
190 pub fn get_scoped<T>(&self, scope: &ClientScope) -> Result<Arc<T>, ClientHubError>
196 where
197 T: ?Sized + Send + Sync + 'static,
198 {
199 let key = ScopedKey {
200 type_key: TypeKey::of::<T>(),
201 scope: scope.clone(),
202 };
203 let r = self.scoped_map.read();
204
205 let boxed = r.get(&key).ok_or_else(|| ClientHubError::ScopedNotFound {
206 type_key: key.type_key.clone(),
207 scope: key.scope.clone(),
208 })?;
209
210 if let Some(arc_t) = boxed.downcast_ref::<Arc<T>>() {
211 return Ok(arc_t.clone());
212 }
213 Err(ClientHubError::ScopedTypeMismatch {
214 type_key: key.type_key,
215 scope: key.scope,
216 })
217 }
218
219 pub fn try_get_scoped<T>(&self, scope: &ClientScope) -> Option<Arc<T>>
223 where
224 T: ?Sized + Send + Sync + 'static,
225 {
226 let key = ScopedKey {
227 type_key: TypeKey::of::<T>(),
228 scope: scope.clone(),
229 };
230 let r = self.scoped_map.read();
231 let boxed = r.get(&key)?;
232
233 boxed.downcast_ref::<Arc<T>>().cloned()
234 }
235
236 pub fn remove<T>(&self) -> Option<Arc<T>>
238 where
239 T: ?Sized + Send + Sync + 'static,
240 {
241 let type_key = TypeKey::of::<T>();
242 let mut w = self.map.write();
243 let boxed = w.remove(&type_key)?;
244 boxed.downcast::<Arc<T>>().ok().map(|b| *b)
245 }
246
247 pub fn remove_scoped<T>(&self, scope: &ClientScope) -> Option<Arc<T>>
249 where
250 T: ?Sized + Send + Sync + 'static,
251 {
252 let key = ScopedKey {
253 type_key: TypeKey::of::<T>(),
254 scope: scope.clone(),
255 };
256 let mut w = self.scoped_map.write();
257 let boxed = w.remove(&key)?;
258 boxed.downcast::<Arc<T>>().ok().map(|b| *b)
259 }
260
261 pub fn clear(&self) {
263 self.map.write().clear();
264 self.scoped_map.write().clear();
265 }
266
267 pub fn len(&self) -> usize {
269 self.map.read().len() + self.scoped_map.read().len()
270 }
271
272 pub fn is_empty(&self) -> bool {
274 self.map.read().is_empty() && self.scoped_map.read().is_empty()
275 }
276}
277
278#[cfg(test)]
279#[cfg_attr(coverage_nightly, coverage(off))]
280mod tests {
281 use super::*;
282
283 #[async_trait::async_trait]
284 trait TestApi: Send + Sync {
285 async fn id(&self) -> usize;
286 }
287
288 struct ImplA(usize);
289 #[async_trait::async_trait]
290 impl TestApi for ImplA {
291 async fn id(&self) -> usize {
292 self.0
293 }
294 }
295
296 #[tokio::test]
297 async fn register_and_get_dyn_trait() {
298 let hub = ClientHub::new();
299 let api: Arc<dyn TestApi> = Arc::new(ImplA(7));
300 hub.register::<dyn TestApi>(api.clone());
301
302 let got = hub.get::<dyn TestApi>().unwrap();
303 assert_eq!(got.id().await, 7);
304 assert_eq!(Arc::as_ptr(&api), Arc::as_ptr(&got));
305 }
306
307 #[tokio::test]
308 async fn remove_works() {
309 let hub = ClientHub::new();
310 let api: Arc<dyn TestApi> = Arc::new(ImplA(42));
311 hub.register::<dyn TestApi>(api);
312
313 assert!(hub.get::<dyn TestApi>().is_ok());
314
315 let removed = hub.remove::<dyn TestApi>();
316 assert!(removed.is_some());
317 assert!(hub.get::<dyn TestApi>().is_err());
318 }
319
320 #[tokio::test]
321 async fn overwrite_replaces_atomically() {
322 let hub = ClientHub::new();
323 hub.register::<dyn TestApi>(Arc::new(ImplA(1)));
324
325 let old = hub.get::<dyn TestApi>().unwrap();
326 assert_eq!(old.id().await, 1);
327
328 hub.register::<dyn TestApi>(Arc::new(ImplA(2)));
329
330 let new = hub.get::<dyn TestApi>().unwrap();
331 assert_eq!(new.id().await, 2);
332
333 assert_eq!(old.id().await, 1);
335 }
336
337 #[tokio::test]
338 async fn scoped_register_and_get_dyn_trait() {
339 let hub = ClientHub::new();
340 let scope_a = ClientScope::gts_id(
341 "gts.x.core.modkit.plugins.v1~x.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0",
342 );
343 let scope_b = ClientScope::gts_id(
344 "gts.x.core.modkit.plugins.v1~x.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0",
345 );
346
347 let api_a: Arc<dyn TestApi> = Arc::new(ImplA(1));
348 let api_b: Arc<dyn TestApi> = Arc::new(ImplA(2));
349
350 hub.register_scoped::<dyn TestApi>(scope_a.clone(), api_a.clone());
351 hub.register_scoped::<dyn TestApi>(scope_b.clone(), api_b.clone());
352
353 assert_eq!(
354 hub.get_scoped::<dyn TestApi>(&scope_a).unwrap().id().await,
355 1
356 );
357 assert_eq!(
358 hub.get_scoped::<dyn TestApi>(&scope_b).unwrap().id().await,
359 2
360 );
361 }
362
363 #[test]
364 fn scoped_get_is_independent_from_global_get() {
365 let hub = ClientHub::new();
366 let scope = ClientScope::gts_id(
367 "gts.x.core.modkit.plugins.v1~x.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0",
368 );
369 hub.register::<str>(Arc::from("global"));
370 hub.register_scoped::<str>(scope.clone(), Arc::from("scoped"));
371
372 assert_eq!(&*hub.get::<str>().unwrap(), "global");
373 assert_eq!(&*hub.get_scoped::<str>(&scope).unwrap(), "scoped");
374 }
375
376 #[test]
377 fn try_get_scoped_returns_some_on_hit() {
378 let hub = ClientHub::new();
379 let scope = ClientScope::gts_id(
380 "gts.x.core.modkit.plugins.v1~x.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0",
381 );
382 hub.register_scoped::<str>(scope.clone(), Arc::from("scoped"));
383
384 let got = hub.try_get_scoped::<str>(&scope);
385 assert_eq!(got.as_deref(), Some("scoped"));
386 }
387
388 #[test]
389 fn try_get_scoped_returns_none_on_miss() {
390 let hub = ClientHub::new();
391 let scope = ClientScope::gts_id(
392 "gts.x.core.modkit.plugins.v1~x.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0",
393 );
394
395 let got = hub.try_get_scoped::<str>(&scope);
396 assert!(got.is_none());
397 }
398}