Skip to main content

spawned_concurrency/
registry.rs

1use std::any::Any;
2use std::collections::HashMap;
3use std::sync::{OnceLock, RwLock};
4
5type Store = RwLock<HashMap<String, Box<dyn Any + Send + Sync>>>;
6
7fn global_store() -> &'static Store {
8    static STORE: OnceLock<Store> = OnceLock::new();
9    STORE.get_or_init(|| RwLock::new(HashMap::new()))
10}
11
12/// Errors that can occur when registering a value in the registry.
13#[derive(Debug, thiserror::Error)]
14pub enum RegistryError {
15    /// A value with this name is already registered.
16    #[error("name '{0}' is already registered")]
17    AlreadyRegistered(String),
18}
19
20/// Register a value by name in the global registry.
21///
22/// Returns `Err(AlreadyRegistered)` if the name is already taken.
23/// Use [`unregister`] first if you need to replace an existing entry.
24pub fn register<T: Clone + Send + Sync + 'static>(
25    name: &str,
26    value: T,
27) -> Result<(), RegistryError> {
28    use std::collections::hash_map::Entry;
29    let mut store = global_store().write().unwrap_or_else(|p| p.into_inner());
30    match store.entry(name.to_string()) {
31        Entry::Occupied(e) => Err(RegistryError::AlreadyRegistered(e.key().clone())),
32        Entry::Vacant(e) => {
33            e.insert(Box::new(value));
34            Ok(())
35        }
36    }
37}
38
39/// Look up a value by name. Returns `None` if not found or if the stored
40/// type doesn't match `T`.
41pub fn whereis<T: Clone + Send + Sync + 'static>(name: &str) -> Option<T> {
42    let store = global_store().read().unwrap_or_else(|p| p.into_inner());
43    store.get(name)?.downcast_ref::<T>().cloned()
44}
45
46/// Remove a registration by name. No-op if the name is not registered.
47pub fn unregister(name: &str) {
48    let mut store = global_store().write().unwrap_or_else(|p| p.into_inner());
49    store.remove(name);
50}
51
52/// List all registered names.
53pub fn registered() -> Vec<String> {
54    let store = global_store().read().unwrap_or_else(|p| p.into_inner());
55    store.keys().cloned().collect()
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    // Use unique names per test to avoid cross-test interference with global state.
63
64    #[test]
65    fn register_and_whereis() {
66        register("test_rw_1", 42u64).unwrap();
67        let val: Option<u64> = whereis("test_rw_1");
68        assert_eq!(val, Some(42));
69    }
70
71    #[test]
72    fn whereis_wrong_type_returns_none() {
73        register("test_wt_1", 42u64).unwrap();
74        let val: Option<String> = whereis("test_wt_1");
75        assert_eq!(val, None);
76    }
77
78    #[test]
79    fn whereis_missing_returns_none() {
80        let val: Option<u64> = whereis("nonexistent_key");
81        assert_eq!(val, None);
82    }
83
84    #[test]
85    fn duplicate_register_fails() {
86        register("test_dup_1", 1u32).unwrap();
87        let result = register("test_dup_1", 2u32);
88        assert!(result.is_err());
89    }
90
91    #[test]
92    fn unregister_removes_entry() {
93        register("test_unreg_1", "hello".to_string()).unwrap();
94        unregister("test_unreg_1");
95        let val: Option<String> = whereis("test_unreg_1");
96        assert_eq!(val, None);
97    }
98
99    #[test]
100    fn registered_lists_names() {
101        register("test_list_a", 1u32).unwrap();
102        register("test_list_b", 2u32).unwrap();
103        let names = registered();
104        assert!(names.contains(&"test_list_a".to_string()));
105        assert!(names.contains(&"test_list_b".to_string()));
106    }
107}