Skip to main content

starweaver_context/
dependency.rs

1//! Type-indexed dependency container for runtime and tool contexts.
2
3use std::{
4    any::{Any, TypeId},
5    collections::{BTreeMap, BTreeSet},
6    sync::Arc,
7};
8
9/// Type-indexed dependency container for runtime and tool contexts.
10#[derive(Clone, Default)]
11pub struct DependencyStore {
12    values: BTreeMap<String, Arc<dyn Any + Send + Sync>>,
13    type_keys: BTreeMap<TypeId, String>,
14}
15
16impl DependencyStore {
17    /// Create an empty dependency store.
18    #[must_use]
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    /// Insert a dependency using its Rust type as the lookup key.
24    pub fn insert<T>(&mut self, value: T)
25    where
26        T: Send + Sync + 'static,
27    {
28        self.insert_named(std::any::type_name::<T>(), value);
29    }
30
31    /// Insert an already shared dependency using its Rust type as the lookup key.
32    pub fn insert_arc<T>(&mut self, value: Arc<T>)
33    where
34        T: Send + Sync + 'static,
35    {
36        self.insert_named_arc(std::any::type_name::<T>(), value);
37    }
38
39    /// Insert a dependency with a caller-provided stable name.
40    ///
41    /// The inserted name becomes the preferred typed lookup for `T`; older
42    /// names remain available through named lookup.
43    pub fn insert_named<T>(&mut self, name: impl Into<String>, value: T)
44    where
45        T: Send + Sync + 'static,
46    {
47        self.insert_named_arc(name, Arc::new(value));
48    }
49
50    /// Insert an already shared dependency with a caller-provided stable name.
51    ///
52    /// Replacing a preferred name with another type reindexes any remaining
53    /// alias for the previous type instead of leaving a stale typed mapping.
54    pub fn insert_named_arc<T>(&mut self, name: impl Into<String>, value: Arc<T>)
55    where
56        T: Send + Sync + 'static,
57    {
58        let name = name.into();
59        if let Some(previous) = self.values.get(&name) {
60            let previous_type = previous.as_ref().type_id();
61            if self.type_keys.get(&previous_type) == Some(&name) {
62                self.type_keys.remove(&previous_type);
63                if let Some(fallback_name) = self
64                    .values
65                    .iter()
66                    .filter(|(candidate_name, candidate)| {
67                        *candidate_name != &name && candidate.as_ref().type_id() == previous_type
68                    })
69                    .map(|(candidate_name, _)| candidate_name.clone())
70                    .next_back()
71                {
72                    self.type_keys.insert(previous_type, fallback_name);
73                }
74            }
75        }
76        self.type_keys.insert(TypeId::of::<T>(), name.clone());
77        self.values.insert(name, value);
78    }
79
80    /// Get a dependency by Rust type.
81    #[must_use]
82    pub fn get<T>(&self) -> Option<Arc<T>>
83    where
84        T: Send + Sync + 'static,
85    {
86        self.type_keys
87            .get(&TypeId::of::<T>())
88            .and_then(|name| self.get_named(name))
89    }
90
91    /// Get a dependency by stable name.
92    #[must_use]
93    pub fn get_named<T>(&self, name: &str) -> Option<Arc<T>>
94    where
95        T: Send + Sync + 'static,
96    {
97        self.values
98            .get(name)
99            .cloned()
100            .and_then(|value| value.downcast::<T>().ok())
101    }
102
103    /// Clone a dependency subset selected by stable names.
104    ///
105    /// Typed lookup remains available when the selected name is not the source
106    /// store's preferred alias for that type. If several selected names contain
107    /// the same type, the source store's preferred alias wins when selected;
108    /// otherwise stable name order determines the fallback alias.
109    #[must_use]
110    pub fn subset(&self, names: &BTreeSet<String>) -> Self {
111        let values: BTreeMap<_, _> = self
112            .values
113            .iter()
114            .filter(|(name, _)| names.contains(*name))
115            .map(|(name, value)| (name.clone(), Arc::clone(value)))
116            .collect();
117        let mut type_keys = BTreeMap::new();
118        for (name, value) in &values {
119            type_keys.insert(value.as_ref().type_id(), name.clone());
120        }
121        for (type_id, name) in &self.type_keys {
122            if values.contains_key(name) {
123                type_keys.insert(*type_id, name.clone());
124            }
125        }
126        Self { values, type_keys }
127    }
128
129    /// Merge dependencies from another store, replacing matching stable names and typed lookups.
130    pub fn extend(&mut self, other: Self) {
131        for (name, value) in other.values {
132            if let Some(previous) = self.values.insert(name.clone(), value.clone()) {
133                let previous_type = previous.as_ref().type_id();
134                if self.type_keys.get(&previous_type) == Some(&name) {
135                    self.type_keys.remove(&previous_type);
136                }
137            }
138            self.type_keys.insert(value.as_ref().type_id(), name);
139        }
140        for (type_id, name) in other.type_keys {
141            if self.values.contains_key(&name) {
142                self.type_keys.insert(type_id, name);
143            }
144        }
145    }
146
147    /// Return all named dependency keys.
148    #[must_use]
149    pub fn keys(&self) -> Vec<String> {
150        self.values.keys().cloned().collect()
151    }
152
153    /// Return whether the store has no dependencies.
154    #[must_use]
155    pub fn is_empty(&self) -> bool {
156        self.values.is_empty()
157    }
158}
159
160impl std::fmt::Debug for DependencyStore {
161    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        formatter
163            .debug_struct("DependencyStore")
164            .field("keys", &self.keys())
165            .finish()
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use std::collections::BTreeSet;
172
173    use super::DependencyStore;
174
175    #[derive(Debug, Eq, PartialEq)]
176    struct First(u32);
177
178    #[derive(Debug, Eq, PartialEq)]
179    struct Second(u32);
180
181    #[test]
182    fn subset_preserves_selected_typed_and_named_lookups() {
183        let mut store = DependencyStore::new();
184        store.insert(First(1));
185        store.insert_named("second", Second(2));
186        let names = BTreeSet::from([
187            std::any::type_name::<First>().to_string(),
188            "second".to_string(),
189        ]);
190
191        let subset = store.subset(&names);
192
193        assert_eq!(subset.get::<First>().as_deref(), Some(&First(1)));
194        assert_eq!(subset.get::<Second>().as_deref(), Some(&Second(2)));
195        assert_eq!(
196            subset.get_named::<Second>("second").as_deref(),
197            Some(&Second(2))
198        );
199    }
200
201    #[test]
202    fn subset_removes_omitted_type_keys_and_values() {
203        let mut store = DependencyStore::new();
204        store.insert(First(1));
205        store.insert(Second(2));
206        let names = BTreeSet::from([std::any::type_name::<First>().to_string()]);
207
208        let subset = store.subset(&names);
209
210        assert!(subset.get::<First>().is_some());
211        assert!(subset.get::<Second>().is_none());
212        assert!(
213            subset
214                .get_named::<Second>(std::any::type_name::<Second>())
215                .is_none()
216        );
217    }
218
219    #[test]
220    fn replacing_a_name_with_another_type_removes_the_stale_typed_lookup() {
221        let mut store = DependencyStore::new();
222        store.insert_named("shared", First(1));
223        store.insert_named("shared", Second(2));
224
225        assert!(store.get::<First>().is_none());
226        assert_eq!(store.get::<Second>().as_deref(), Some(&Second(2)));
227        assert_eq!(
228            store.get_named::<Second>("shared").as_deref(),
229            Some(&Second(2))
230        );
231    }
232
233    #[test]
234    fn replacing_the_preferred_alias_reindexes_an_existing_alias() {
235        let mut store = DependencyStore::new();
236        store.insert_named("fallback", First(1));
237        store.insert_named("preferred", First(2));
238        store.insert_named("preferred", Second(3));
239
240        assert_eq!(store.get::<First>().as_deref(), Some(&First(1)));
241        assert_eq!(store.get::<Second>().as_deref(), Some(&Second(3)));
242    }
243
244    #[test]
245    fn subset_reindexes_a_selected_non_preferred_alias_for_typed_lookup() {
246        let mut store = DependencyStore::new();
247        store.insert_named("first-alias", First(1));
248        store.insert_named("preferred-alias", First(2));
249        assert_eq!(store.get::<First>().as_deref(), Some(&First(2)));
250
251        let subset = store.subset(&BTreeSet::from(["first-alias".to_string()]));
252
253        assert_eq!(subset.get::<First>().as_deref(), Some(&First(1)));
254        assert_eq!(
255            subset.get_named::<First>("first-alias").as_deref(),
256            Some(&First(1))
257        );
258    }
259}