Skip to main content

laterite_core/
registry.rs

1//! The extension registry.
2//!
3//! Modules contribute typed items in their [`register`](crate::module::Module::register);
4//! the framework collects across all modules in dependency order and hands the
5//! typed sets to consumers. It is keyed by Rust type, so a plugin can define its
6//! own extension point (a trait or struct) and any other module can fill it with
7//! no core change: the definer reads `items::<MyExtension>()`, contributors call
8//! `add(my_extension)`.
9//!
10//! Framework-known surfaces (resources, permissions, ...) get typed, discoverable
11//! sugar through extension traits defined in the layer that owns those types
12//! (`laterite-admin`), so a wrong-type contribution is a compile error rather than
13//! a silently invisible one; the generic `add`/`items` underneath is for open,
14//! plugin-defined points.
15
16use std::any::{Any, TypeId};
17use std::collections::HashMap;
18
19use crate::module::ModuleId;
20
21/// How a contribution combines with earlier ones of the same type.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum ContributeMode {
24    /// Add alongside earlier contributions (the default).
25    #[default]
26    Append,
27    /// Supersede all earlier contributions of the same type, for a singleton
28    /// extension point where one contribution should win (a site overriding a
29    /// default).
30    Replace,
31}
32
33/// Priority and mode for a contribution.
34#[derive(Debug, Clone, Copy, Default)]
35pub struct ContributeOpts {
36    /// Higher sorts first among same-type contributions; default 0.
37    pub priority: i32,
38    pub mode: ContributeMode,
39}
40
41struct Entry {
42    owner: ModuleId,
43    priority: i32,
44    mode: ContributeMode,
45    item: Box<dyn Any>,
46}
47
48/// A borrowed contribution: the item plus the envelope the registry stamped.
49pub struct Contribution<'a, T> {
50    pub owner: ModuleId,
51    pub priority: i32,
52    pub mode: ContributeMode,
53    pub item: &'a T,
54}
55
56/// Collects typed contributions from modules, keyed by Rust type.
57#[derive(Default)]
58pub struct Registry {
59    current: Option<ModuleId>,
60    by_type: HashMap<TypeId, Vec<Entry>>,
61}
62
63impl Registry {
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    /// Sets which module is registering; its contributions are stamped with this
69    /// owner. The framework calls this before each module's `register()`, so an
70    /// author never passes an owner and cannot forge one.
71    pub fn set_owner(&mut self, owner: ModuleId) {
72        self.current = Some(owner);
73    }
74
75    /// Contributes `item`, keyed by its type, with the default envelope.
76    pub fn add<T: 'static>(&mut self, item: T) {
77        self.add_with(item, ContributeOpts::default());
78    }
79
80    /// Contributes `item` with an explicit priority and mode.
81    pub fn add_with<T: 'static>(&mut self, item: T, opts: ContributeOpts) {
82        let owner = self
83            .current
84            .expect("Registry::add called outside a module register() pass");
85        self.by_type
86            .entry(TypeId::of::<T>())
87            .or_default()
88            .push(Entry {
89                owner,
90                priority: opts.priority,
91                mode: opts.mode,
92                item: Box::new(item),
93            });
94    }
95
96    /// The contributions of type `T`, resolved: a `Replace` supersedes everything
97    /// before it, then higher priority sorts first, ties keeping dependency order.
98    pub fn contributions<T: 'static>(&self) -> Vec<Contribution<'_, T>> {
99        let Some(entries) = self.by_type.get(&TypeId::of::<T>()) else {
100            return Vec::new();
101        };
102        let start = entries
103            .iter()
104            .rposition(|e| e.mode == ContributeMode::Replace)
105            .unwrap_or(0);
106        let mut resolved: Vec<&Entry> = entries[start..].iter().collect();
107        // Stable sort, so equal priorities keep insertion (dependency) order.
108        resolved.sort_by_key(|a| std::cmp::Reverse(a.priority));
109        resolved
110            .into_iter()
111            .map(|e| Contribution {
112                owner: e.owner,
113                priority: e.priority,
114                mode: e.mode,
115                item: e
116                    .item
117                    .downcast_ref::<T>()
118                    .expect("keyed by TypeId::of::<T>"),
119            })
120            .collect()
121    }
122
123    /// The items of type `T`, in resolved order (envelope dropped).
124    pub fn items<T: 'static>(&self) -> Vec<&T> {
125        self.contributions::<T>()
126            .into_iter()
127            .map(|c| c.item)
128            .collect()
129    }
130
131    /// Removes and returns the items of type `T` as owned values, resolved the
132    /// same way as [`items`](Registry::items). For a one-time collection at boot;
133    /// the type's contributions are consumed, so no `Clone` bound is needed.
134    pub fn take<T: 'static>(&mut self) -> Vec<T> {
135        let Some(entries) = self.by_type.remove(&TypeId::of::<T>()) else {
136            return Vec::new();
137        };
138        let start = entries
139            .iter()
140            .rposition(|e| e.mode == ContributeMode::Replace)
141            .unwrap_or(0);
142        let mut kept: Vec<Entry> = entries.into_iter().skip(start).collect();
143        // Stable sort, so equal priorities keep insertion (dependency) order.
144        kept.sort_by_key(|a| std::cmp::Reverse(a.priority));
145        kept.into_iter()
146            .map(|e| *e.item.downcast::<T>().expect("keyed by TypeId::of::<T>"))
147            .collect()
148    }
149
150    /// Drops every contribution stamped with `owner`, across all types. Rolls back
151    /// a module's partial contributions when its `register` fails, so a quarantined
152    /// module leaves nothing behind.
153    pub fn purge_owner(&mut self, owner: ModuleId) {
154        for entries in self.by_type.values_mut() {
155            entries.retain(|e| e.owner != owner);
156        }
157        self.by_type.retain(|_, entries| !entries.is_empty());
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    const A: ModuleId = ModuleId::new("acme.a");
166    const B: ModuleId = ModuleId::new("acme.b");
167
168    #[derive(Debug, PartialEq)]
169    struct Widget(&'static str);
170
171    #[test]
172    fn collects_in_order_and_stamps_owner() {
173        let mut r = Registry::new();
174        r.set_owner(A);
175        r.add(Widget("one"));
176        r.set_owner(B);
177        r.add(Widget("two"));
178
179        let cs = r.contributions::<Widget>();
180        assert_eq!(cs.len(), 2);
181        assert_eq!(cs[0].item, &Widget("one"));
182        assert_eq!(cs[0].owner, A);
183        assert_eq!(cs[1].owner, B);
184        assert_eq!(r.items::<Widget>(), vec![&Widget("one"), &Widget("two")]);
185    }
186
187    #[test]
188    fn higher_priority_sorts_first_ties_keep_order() {
189        let mut r = Registry::new();
190        r.set_owner(A);
191        r.add(Widget("low"));
192        r.add_with(
193            Widget("high"),
194            ContributeOpts {
195                priority: 10,
196                mode: ContributeMode::Append,
197            },
198        );
199        r.add(Widget("low2"));
200        assert_eq!(
201            r.items::<Widget>(),
202            vec![&Widget("high"), &Widget("low"), &Widget("low2")]
203        );
204    }
205
206    #[test]
207    fn replace_supersedes_earlier() {
208        let mut r = Registry::new();
209        r.set_owner(A);
210        r.add(Widget("default"));
211        r.set_owner(B);
212        r.add_with(
213            Widget("override"),
214            ContributeOpts {
215                priority: 0,
216                mode: ContributeMode::Replace,
217            },
218        );
219        assert_eq!(r.items::<Widget>(), vec![&Widget("override")]);
220    }
221
222    #[test]
223    fn purge_owner_drops_only_that_owners_contributions() {
224        let mut r = Registry::new();
225        r.set_owner(A);
226        r.add(Widget("a"));
227        r.add(7u32);
228        r.set_owner(B);
229        r.add(Widget("b"));
230
231        r.purge_owner(A);
232        // B's widget survives; A's widget and A's u32 (its whole bucket) are gone.
233        assert_eq!(r.items::<Widget>(), vec![&Widget("b")]);
234        assert!(r.items::<u32>().is_empty());
235    }
236
237    #[test]
238    fn distinct_types_are_independent() {
239        let mut r = Registry::new();
240        r.set_owner(A);
241        r.add(Widget("w"));
242        r.add(7u32);
243        assert_eq!(r.items::<Widget>(), vec![&Widget("w")]);
244        assert_eq!(r.items::<u32>(), vec![&7u32]);
245    }
246}