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 use toolkit_gts::gts_id;
283
284 #[async_trait::async_trait]
285 trait TestApi: Send + Sync {
286 async fn id(&self) -> usize;
287 }
288
289 struct ImplA(usize);
290 #[async_trait::async_trait]
291 impl TestApi for ImplA {
292 async fn id(&self) -> usize {
293 self.0
294 }
295 }
296
297 #[tokio::test]
298 async fn register_and_get_dyn_trait() {
299 let hub = ClientHub::new();
300 let api: Arc<dyn TestApi> = Arc::new(ImplA(7));
301 hub.register::<dyn TestApi>(api.clone());
302
303 let got = hub.get::<dyn TestApi>().unwrap();
304 assert_eq!(got.id().await, 7);
305 assert_eq!(Arc::as_ptr(&api), Arc::as_ptr(&got));
306 }
307
308 #[tokio::test]
309 async fn remove_works() {
310 let hub = ClientHub::new();
311 let api: Arc<dyn TestApi> = Arc::new(ImplA(42));
312 hub.register::<dyn TestApi>(api);
313
314 assert!(hub.get::<dyn TestApi>().is_ok());
315
316 let removed = hub.remove::<dyn TestApi>();
317 assert!(removed.is_some());
318 assert!(hub.get::<dyn TestApi>().is_err());
319 }
320
321 #[tokio::test]
322 async fn overwrite_replaces_atomically() {
323 let hub = ClientHub::new();
324 hub.register::<dyn TestApi>(Arc::new(ImplA(1)));
325
326 let old = hub.get::<dyn TestApi>().unwrap();
327 assert_eq!(old.id().await, 1);
328
329 hub.register::<dyn TestApi>(Arc::new(ImplA(2)));
330
331 let new = hub.get::<dyn TestApi>().unwrap();
332 assert_eq!(new.id().await, 2);
333
334 assert_eq!(old.id().await, 1);
336 }
337
338 #[tokio::test]
339 async fn scoped_register_and_get_dyn_trait() {
340 let hub = ClientHub::new();
341 let scope_a = ClientScope::gts_id(gts_id!(
342 "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0"
343 ));
344 let scope_b = ClientScope::gts_id(gts_id!(
345 "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0"
346 ));
347
348 let api_a: Arc<dyn TestApi> = Arc::new(ImplA(1));
349 let api_b: Arc<dyn TestApi> = Arc::new(ImplA(2));
350
351 hub.register_scoped::<dyn TestApi>(scope_a.clone(), api_a.clone());
352 hub.register_scoped::<dyn TestApi>(scope_b.clone(), api_b.clone());
353
354 assert_eq!(
355 hub.get_scoped::<dyn TestApi>(&scope_a).unwrap().id().await,
356 1
357 );
358 assert_eq!(
359 hub.get_scoped::<dyn TestApi>(&scope_b).unwrap().id().await,
360 2
361 );
362 }
363
364 #[test]
365 fn scoped_get_is_independent_from_global_get() {
366 let hub = ClientHub::new();
367 let scope = ClientScope::gts_id(gts_id!(
368 "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0"
369 ));
370 hub.register::<str>(Arc::from("global"));
371 hub.register_scoped::<str>(scope.clone(), Arc::from("scoped"));
372
373 assert_eq!(&*hub.get::<str>().unwrap(), "global");
374 assert_eq!(&*hub.get_scoped::<str>(&scope).unwrap(), "scoped");
375 }
376
377 #[test]
378 fn try_get_scoped_returns_some_on_hit() {
379 let hub = ClientHub::new();
380 let scope = ClientScope::gts_id(gts_id!(
381 "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0"
382 ));
383 hub.register_scoped::<str>(scope.clone(), Arc::from("scoped"));
384
385 let got = hub.try_get_scoped::<str>(&scope);
386 assert_eq!(got.as_deref(), Some("scoped"));
387 }
388
389 #[test]
390 fn try_get_scoped_returns_none_on_miss() {
391 let hub = ClientHub::new();
392 let scope = ClientScope::gts_id(gts_id!(
393 "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0"
394 ));
395
396 let got = hub.try_get_scoped::<str>(&scope);
397 assert!(got.is_none());
398 }
399}