Skip to main content

minco_core/
contribution.rs

1use crate::{ContributionRegistration, ContributionTypeRegistration, RegistrationOwner, Shared};
2use std::{
3    any::{Any, TypeId},
4    collections::HashMap,
5    fmt,
6    sync::Arc,
7};
8
9/// Mutable, typed multi-binding registry used while plugins are installed.
10///
11/// Services represent a single authoritative implementation for a type. Contributions
12/// represent zero or more independent implementations, such as HTTP modules, notification
13/// sinks, health checks, or event observers.
14#[derive(Default)]
15pub struct ContributionCollection {
16    values: HashMap<TypeId, Vec<Arc<dyn Any + Send + Sync>>>,
17    registrations: HashMap<TypeId, ContributionTypeRegistration>,
18    next_installation_index: usize,
19}
20
21impl std::fmt::Debug for ContributionCollection {
22    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        formatter
24            .debug_struct("ContributionCollection")
25            .field("contribution_types", &self.values.len())
26            .field(
27                "contribution_count",
28                &self.values.values().map(Vec::len).sum::<usize>(),
29            )
30            .field("registration_types", &self.registrations.len())
31            .field("next_installation_index", &self.next_installation_index)
32            .finish()
33    }
34}
35
36impl ContributionCollection {
37    /// Adds one concrete contribution.
38    pub fn push<T>(&mut self, value: Arc<T>)
39    where
40        T: Any + Send + Sync,
41    {
42        self.push_owned(value, RegistrationOwner::application());
43    }
44
45    pub(crate) fn push_owned<T>(&mut self, value: Arc<T>, owner: RegistrationOwner)
46    where
47        T: Any + Send + Sync,
48    {
49        let type_id = TypeId::of::<T>();
50        self.values.entry(type_id).or_default().push(value);
51        self.registrations
52            .entry(type_id)
53            .or_insert_with(|| ContributionTypeRegistration {
54                rust_type: std::any::type_name::<T>(),
55                registrations: Vec::new(),
56            })
57            .registrations
58            .push(ContributionRegistration {
59                owner,
60                installation_index: self.next_installation_index,
61            });
62        self.next_installation_index += 1;
63    }
64
65    /// Adds one trait-object contribution while retaining a typed retrieval API.
66    pub fn push_shared<T>(&mut self, value: Arc<T>)
67    where
68        T: ?Sized + Send + Sync + 'static,
69    {
70        self.push_shared_owned(value, RegistrationOwner::application());
71    }
72
73    pub(crate) fn push_shared_owned<T>(&mut self, value: Arc<T>, owner: RegistrationOwner)
74    where
75        T: ?Sized + Send + Sync + 'static,
76    {
77        self.push_owned(Arc::new(Shared::new(value)), owner);
78    }
79
80    /// Returns all concrete contributions currently registered for `T`.
81    pub fn get<T>(&self) -> Vec<Arc<T>>
82    where
83        T: Any + Send + Sync,
84    {
85        self.values
86            .get(&TypeId::of::<T>())
87            .into_iter()
88            .flatten()
89            .filter_map(|value| Arc::clone(value).downcast::<T>().ok())
90            .collect()
91    }
92
93    /// Returns all trait-object contributions currently registered for `T`.
94    pub fn get_shared<T>(&self) -> Vec<Arc<T>>
95    where
96        T: ?Sized + Send + Sync + 'static,
97    {
98        self.get::<Shared<T>>()
99            .into_iter()
100            .map(|value| value.inner())
101            .collect()
102    }
103
104    pub fn freeze(self) -> FrozenContributions {
105        let mut registrations = self.registrations.into_values().collect::<Vec<_>>();
106        registrations.sort_by(|left, right| {
107            let left_index = left
108                .registrations
109                .first()
110                .map_or(usize::MAX, |registration| registration.installation_index);
111            let right_index = right
112                .registrations
113                .first()
114                .map_or(usize::MAX, |registration| registration.installation_index);
115            left.rust_type
116                .cmp(right.rust_type)
117                .then_with(|| left_index.cmp(&right_index))
118        });
119        FrozenContributions {
120            values: self.values,
121            registrations,
122        }
123    }
124}
125
126/// Owner-bound contribution registrar exposed by `PluginContext`.
127///
128/// Registration order is global across contribution types and deterministic for a repeated
129/// composition of the same application and plugin graph.
130pub struct ContributionRegistrar<'a> {
131    pub(crate) contributions: &'a mut ContributionCollection,
132    pub(crate) owner: RegistrationOwner,
133}
134
135impl fmt::Debug for ContributionRegistrar<'_> {
136    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137        formatter
138            .debug_struct("ContributionRegistrar")
139            .field("owner", &self.owner)
140            .finish_non_exhaustive()
141    }
142}
143
144impl ContributionRegistrar<'_> {
145    pub fn push<T>(&mut self, value: Arc<T>)
146    where
147        T: Any + Send + Sync,
148    {
149        self.contributions.push_owned(value, self.owner.clone());
150    }
151
152    pub fn push_shared<T>(&mut self, value: Arc<T>)
153    where
154        T: ?Sized + Send + Sync + 'static,
155    {
156        self.contributions
157            .push_shared_owned(value, self.owner.clone());
158    }
159
160    pub fn get<T>(&self) -> Vec<Arc<T>>
161    where
162        T: Any + Send + Sync,
163    {
164        self.contributions.get::<T>()
165    }
166
167    pub fn get_shared<T>(&self) -> Vec<Arc<T>>
168    where
169        T: ?Sized + Send + Sync + 'static,
170    {
171        self.contributions.get_shared::<T>()
172    }
173}
174
175/// Immutable contribution registry exposed by a composed application.
176#[derive(Clone, Default)]
177pub struct FrozenContributions {
178    values: HashMap<TypeId, Vec<Arc<dyn Any + Send + Sync>>>,
179    registrations: Vec<ContributionTypeRegistration>,
180}
181
182impl std::fmt::Debug for FrozenContributions {
183    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        formatter
185            .debug_struct("FrozenContributions")
186            .field("contribution_types", &self.values.len())
187            .field(
188                "contribution_count",
189                &self.values.values().map(Vec::len).sum::<usize>(),
190            )
191            .field("registration_types", &self.registrations.len())
192            .finish()
193    }
194}
195
196impl FrozenContributions {
197    /// Returns metadata grouped by Rust type while preserving registration order.
198    pub fn registrations(&self) -> &[ContributionTypeRegistration] {
199        &self.registrations
200    }
201
202    pub fn get<T>(&self) -> Vec<Arc<T>>
203    where
204        T: Any + Send + Sync,
205    {
206        self.values
207            .get(&TypeId::of::<T>())
208            .into_iter()
209            .flatten()
210            .filter_map(|value| Arc::clone(value).downcast::<T>().ok())
211            .collect()
212    }
213
214    pub fn get_shared<T>(&self) -> Vec<Arc<T>>
215    where
216        T: ?Sized + Send + Sync + 'static,
217    {
218        self.get::<Shared<T>>()
219            .into_iter()
220            .map(|value| value.inner())
221            .collect()
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    trait Named: Send + Sync {
230        fn name(&self) -> &'static str;
231    }
232
233    #[derive(Debug)]
234    struct First;
235    impl Named for First {
236        fn name(&self) -> &'static str {
237            "first"
238        }
239    }
240
241    #[derive(Debug)]
242    struct Second;
243    impl Named for Second {
244        fn name(&self) -> &'static str {
245            "second"
246        }
247    }
248
249    #[test]
250    fn shared_contributions_preserve_registration_order() {
251        let mut values = ContributionCollection::default();
252        values.push_shared::<dyn Named>(Arc::new(First));
253        values.push_shared::<dyn Named>(Arc::new(Second));
254
255        let frozen = values.freeze();
256        let names = frozen
257            .get_shared::<dyn Named>()
258            .into_iter()
259            .map(|value| value.name())
260            .collect::<Vec<_>>();
261
262        assert_eq!(names, ["first", "second"]);
263        assert_eq!(frozen.registrations().len(), 1);
264        let metadata = &frozen.registrations()[0];
265        assert_eq!(
266            metadata.rust_type,
267            std::any::type_name::<Shared<dyn Named>>()
268        );
269        assert_eq!(
270            metadata
271                .registrations
272                .iter()
273                .map(|registration| registration.installation_index)
274                .collect::<Vec<_>>(),
275            [0, 1]
276        );
277        assert!(
278            metadata
279                .registrations
280                .iter()
281                .all(|registration| registration.owner.is_application())
282        );
283    }
284}