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 remote_proxies: RwLock<std::collections::HashSet<TypeKey>>,
135}
136
137impl ClientHub {
138 #[inline]
139 #[must_use]
140 pub fn new() -> Self {
141 Self {
142 map: RwLock::new(HashMap::new()),
143 scoped_map: RwLock::new(HashMap::new()),
144 remote_proxies: RwLock::new(std::collections::HashSet::new()),
145 }
146 }
147}
148
149impl ClientHub {
150 pub fn register<T>(&self, client: Arc<T>)
156 where
157 T: ?Sized + Send + Sync + 'static,
158 {
159 let type_key = TypeKey::of::<T>();
160 self.remote_proxies.write().remove(&type_key);
161 let mut w = self.map.write();
162 w.insert(type_key, Box::new(client));
163 }
164
165 pub fn register_scoped<T>(&self, scope: ClientScope, client: Arc<T>)
170 where
171 T: ?Sized + Send + Sync + 'static,
172 {
173 let key = ScopedKey {
174 type_key: TypeKey::of::<T>(),
175 scope,
176 };
177 let mut w = self.scoped_map.write();
178 w.insert(key, Box::new(client));
179 }
180
181 pub fn get<T>(&self) -> Result<Arc<T>, ClientHubError>
187 where
188 T: ?Sized + Send + Sync + 'static,
189 {
190 let type_key = TypeKey::of::<T>();
191 let r = self.map.read();
192
193 let boxed = r.get(&type_key).ok_or(ClientHubError::NotFound {
194 type_key: type_key.clone(),
195 })?;
196
197 if let Some(arc_t) = boxed.downcast_ref::<Arc<T>>() {
199 return Ok(arc_t.clone());
200 }
201 Err(ClientHubError::TypeMismatch { type_key })
202 }
203
204 pub fn try_get<T>(&self) -> Option<Arc<T>>
211 where
212 T: ?Sized + Send + Sync + 'static,
213 {
214 let type_key = TypeKey::of::<T>();
215 let r = self.map.read();
216 let boxed = r.get(&type_key)?;
217 boxed.downcast_ref::<Arc<T>>().cloned()
218 }
219
220 pub fn register_remote_proxy<T>(&self, client: Arc<T>)
227 where
228 T: ?Sized + Send + Sync + 'static,
229 {
230 let type_key = TypeKey::of::<T>();
231 self.remote_proxies.write().insert(type_key.clone());
232 self.map.write().insert(type_key, Box::new(client));
233 }
234
235 pub fn try_get_local<T>(&self) -> Option<Arc<T>>
248 where
249 T: ?Sized + Send + Sync + 'static,
250 {
251 let type_key = TypeKey::of::<T>();
252 if self.remote_proxies.read().contains(&type_key) {
253 return None;
254 }
255 let r = self.map.read();
256 let boxed = r.get(&type_key)?;
257 boxed.downcast_ref::<Arc<T>>().cloned()
258 }
259
260 #[must_use]
265 pub fn has_remote_proxy<T>(&self) -> bool
266 where
267 T: ?Sized + Send + Sync + 'static,
268 {
269 self.remote_proxies.read().contains(&TypeKey::of::<T>())
270 }
271
272 pub fn get_scoped<T>(&self, scope: &ClientScope) -> Result<Arc<T>, ClientHubError>
278 where
279 T: ?Sized + Send + Sync + 'static,
280 {
281 let key = ScopedKey {
282 type_key: TypeKey::of::<T>(),
283 scope: scope.clone(),
284 };
285 let r = self.scoped_map.read();
286
287 let boxed = r.get(&key).ok_or_else(|| ClientHubError::ScopedNotFound {
288 type_key: key.type_key.clone(),
289 scope: key.scope.clone(),
290 })?;
291
292 if let Some(arc_t) = boxed.downcast_ref::<Arc<T>>() {
293 return Ok(arc_t.clone());
294 }
295 Err(ClientHubError::ScopedTypeMismatch {
296 type_key: key.type_key,
297 scope: key.scope,
298 })
299 }
300
301 pub fn try_get_scoped<T>(&self, scope: &ClientScope) -> Option<Arc<T>>
305 where
306 T: ?Sized + Send + Sync + 'static,
307 {
308 let key = ScopedKey {
309 type_key: TypeKey::of::<T>(),
310 scope: scope.clone(),
311 };
312 let r = self.scoped_map.read();
313 let boxed = r.get(&key)?;
314
315 boxed.downcast_ref::<Arc<T>>().cloned()
316 }
317
318 pub fn remove<T>(&self) -> Option<Arc<T>>
320 where
321 T: ?Sized + Send + Sync + 'static,
322 {
323 let type_key = TypeKey::of::<T>();
324 let mut w = self.map.write();
325 let boxed = w.remove(&type_key)?;
326 boxed.downcast::<Arc<T>>().ok().map(|b| *b)
327 }
328
329 pub fn remove_scoped<T>(&self, scope: &ClientScope) -> Option<Arc<T>>
331 where
332 T: ?Sized + Send + Sync + 'static,
333 {
334 let key = ScopedKey {
335 type_key: TypeKey::of::<T>(),
336 scope: scope.clone(),
337 };
338 let mut w = self.scoped_map.write();
339 let boxed = w.remove(&key)?;
340 boxed.downcast::<Arc<T>>().ok().map(|b| *b)
341 }
342
343 pub fn clear(&self) {
345 self.map.write().clear();
346 self.scoped_map.write().clear();
347 }
348
349 pub fn len(&self) -> usize {
351 self.map.read().len() + self.scoped_map.read().len()
352 }
353
354 pub fn is_empty(&self) -> bool {
356 self.map.read().is_empty() && self.scoped_map.read().is_empty()
357 }
358}
359
360#[cfg(test)]
361#[cfg_attr(coverage_nightly, coverage(off))]
362mod tests {
363 use super::*;
364 use toolkit_gts::gts_id;
365
366 #[async_trait::async_trait]
367 trait TestApi: Send + Sync {
368 async fn id(&self) -> usize;
369 }
370
371 struct ImplA(usize);
372 #[async_trait::async_trait]
373 impl TestApi for ImplA {
374 async fn id(&self) -> usize {
375 self.0
376 }
377 }
378
379 #[tokio::test]
380 async fn register_and_get_dyn_trait() {
381 let hub = ClientHub::new();
382 let api: Arc<dyn TestApi> = Arc::new(ImplA(7));
383 hub.register::<dyn TestApi>(api.clone());
384
385 let got = hub.get::<dyn TestApi>().unwrap();
386 assert_eq!(got.id().await, 7);
387 assert_eq!(Arc::as_ptr(&api), Arc::as_ptr(&got));
388 }
389
390 #[tokio::test]
391 async fn remove_works() {
392 let hub = ClientHub::new();
393 let api: Arc<dyn TestApi> = Arc::new(ImplA(42));
394 hub.register::<dyn TestApi>(api);
395
396 assert!(hub.get::<dyn TestApi>().is_ok());
397
398 let removed = hub.remove::<dyn TestApi>();
399 assert!(removed.is_some());
400 assert!(hub.get::<dyn TestApi>().is_err());
401 }
402
403 #[tokio::test]
404 async fn try_get_local_ignores_a_remote_proxy() {
405 let hub = ClientHub::new();
406 let proxy: Arc<dyn TestApi> = Arc::new(ImplA(1));
407 hub.register_remote_proxy::<dyn TestApi>(proxy);
408
409 assert!(hub.try_get::<dyn TestApi>().is_some());
411 assert!(hub.get::<dyn TestApi>().is_ok());
412 assert!(hub.try_get_local::<dyn TestApi>().is_none());
417 assert!(hub.has_remote_proxy::<dyn TestApi>());
418 }
419
420 #[tokio::test]
421 async fn try_get_local_sees_a_plain_registration() {
422 let hub = ClientHub::new();
423 let local: Arc<dyn TestApi> = Arc::new(ImplA(2));
424 hub.register::<dyn TestApi>(local);
425
426 assert!(hub.try_get_local::<dyn TestApi>().is_some());
427 assert!(!hub.has_remote_proxy::<dyn TestApi>());
428 }
429
430 #[tokio::test]
431 async fn a_late_local_registration_clears_the_remote_mark_and_wins() {
432 let hub = ClientHub::new();
434 hub.register_remote_proxy::<dyn TestApi>(Arc::new(ImplA(1)));
435 assert!(hub.has_remote_proxy::<dyn TestApi>());
436 assert!(hub.try_get_local::<dyn TestApi>().is_none());
437
438 hub.register::<dyn TestApi>(Arc::new(ImplA(2)));
441 assert!(!hub.has_remote_proxy::<dyn TestApi>());
442 let local = hub
443 .try_get_local::<dyn TestApi>()
444 .expect("the late local registration must be visible as local");
445 assert_eq!(local.id().await, 2);
446 }
447
448 #[tokio::test]
449 async fn overwrite_replaces_atomically() {
450 let hub = ClientHub::new();
451 hub.register::<dyn TestApi>(Arc::new(ImplA(1)));
452
453 let old = hub.get::<dyn TestApi>().unwrap();
454 assert_eq!(old.id().await, 1);
455
456 hub.register::<dyn TestApi>(Arc::new(ImplA(2)));
457
458 let new = hub.get::<dyn TestApi>().unwrap();
459 assert_eq!(new.id().await, 2);
460
461 assert_eq!(old.id().await, 1);
463 }
464
465 #[tokio::test]
466 async fn scoped_register_and_get_dyn_trait() {
467 let hub = ClientHub::new();
468 let scope_a = ClientScope::gts_id(gts_id!(
469 "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0"
470 ));
471 let scope_b = ClientScope::gts_id(gts_id!(
472 "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0"
473 ));
474
475 let api_a: Arc<dyn TestApi> = Arc::new(ImplA(1));
476 let api_b: Arc<dyn TestApi> = Arc::new(ImplA(2));
477
478 hub.register_scoped::<dyn TestApi>(scope_a.clone(), api_a.clone());
479 hub.register_scoped::<dyn TestApi>(scope_b.clone(), api_b.clone());
480
481 assert_eq!(
482 hub.get_scoped::<dyn TestApi>(&scope_a).unwrap().id().await,
483 1
484 );
485 assert_eq!(
486 hub.get_scoped::<dyn TestApi>(&scope_b).unwrap().id().await,
487 2
488 );
489 }
490
491 #[test]
492 fn scoped_get_is_independent_from_global_get() {
493 let hub = ClientHub::new();
494 let scope = ClientScope::gts_id(gts_id!(
495 "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0"
496 ));
497 hub.register::<str>(Arc::from("global"));
498 hub.register_scoped::<str>(scope.clone(), Arc::from("scoped"));
499
500 assert_eq!(&*hub.get::<str>().unwrap(), "global");
501 assert_eq!(&*hub.get_scoped::<str>(&scope).unwrap(), "scoped");
502 }
503
504 #[test]
505 fn try_get_scoped_returns_some_on_hit() {
506 let hub = ClientHub::new();
507 let scope = ClientScope::gts_id(gts_id!(
508 "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0"
509 ));
510 hub.register_scoped::<str>(scope.clone(), Arc::from("scoped"));
511
512 let got = hub.try_get_scoped::<str>(&scope);
513 assert_eq!(got.as_deref(), Some("scoped"));
514 }
515
516 #[test]
517 fn try_get_scoped_returns_none_on_miss() {
518 let hub = ClientHub::new();
519 let scope = ClientScope::gts_id(gts_id!(
520 "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0"
521 ));
522
523 let got = hub.try_get_scoped::<str>(&scope);
524 assert!(got.is_none());
525 }
526}