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    /// Like [`Registry::take`], but keeps each item's owning module, for a
135    /// consumer that resolves per-module behaviour (an admin path namespace).
136    pub fn take_owned<T: 'static>(&mut self) -> Vec<(ModuleId, T)> {
137        let owners: Vec<ModuleId> = self.contributions::<T>().iter().map(|c| c.owner).collect();
138        owners.into_iter().zip(self.take::<T>()).collect()
139    }
140
141    pub fn take<T: 'static>(&mut self) -> Vec<T> {
142        let Some(entries) = self.by_type.remove(&TypeId::of::<T>()) else {
143            return Vec::new();
144        };
145        let start = entries
146            .iter()
147            .rposition(|e| e.mode == ContributeMode::Replace)
148            .unwrap_or(0);
149        let mut kept: Vec<Entry> = entries.into_iter().skip(start).collect();
150        // Stable sort, so equal priorities keep insertion (dependency) order.
151        kept.sort_by_key(|a| std::cmp::Reverse(a.priority));
152        kept.into_iter()
153            .map(|e| *e.item.downcast::<T>().expect("keyed by TypeId::of::<T>"))
154            .collect()
155    }
156
157    /// Drops every contribution stamped with `owner`, across all types. Rolls back
158    /// a module's partial contributions when its `register` fails, so a quarantined
159    /// module leaves nothing behind.
160    pub fn purge_owner(&mut self, owner: ModuleId) {
161        for entries in self.by_type.values_mut() {
162            entries.retain(|e| e.owner != owner);
163        }
164        self.by_type.retain(|_, entries| !entries.is_empty());
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    const A: ModuleId = ModuleId::new("acme.a");
173    const B: ModuleId = ModuleId::new("acme.b");
174
175    #[derive(Debug, PartialEq)]
176    struct Widget(&'static str);
177
178    #[test]
179    fn collects_in_order_and_stamps_owner() {
180        let mut r = Registry::new();
181        r.set_owner(A);
182        r.add(Widget("one"));
183        r.set_owner(B);
184        r.add(Widget("two"));
185
186        let cs = r.contributions::<Widget>();
187        assert_eq!(cs.len(), 2);
188        assert_eq!(cs[0].item, &Widget("one"));
189        assert_eq!(cs[0].owner, A);
190        assert_eq!(cs[1].owner, B);
191        assert_eq!(r.items::<Widget>(), vec![&Widget("one"), &Widget("two")]);
192    }
193
194    #[test]
195    fn higher_priority_sorts_first_ties_keep_order() {
196        let mut r = Registry::new();
197        r.set_owner(A);
198        r.add(Widget("low"));
199        r.add_with(
200            Widget("high"),
201            ContributeOpts {
202                priority: 10,
203                mode: ContributeMode::Append,
204            },
205        );
206        r.add(Widget("low2"));
207        assert_eq!(
208            r.items::<Widget>(),
209            vec![&Widget("high"), &Widget("low"), &Widget("low2")]
210        );
211    }
212
213    #[test]
214    fn replace_supersedes_earlier() {
215        let mut r = Registry::new();
216        r.set_owner(A);
217        r.add(Widget("default"));
218        r.set_owner(B);
219        r.add_with(
220            Widget("override"),
221            ContributeOpts {
222                priority: 0,
223                mode: ContributeMode::Replace,
224            },
225        );
226        assert_eq!(r.items::<Widget>(), vec![&Widget("override")]);
227    }
228
229    #[test]
230    fn purge_owner_drops_only_that_owners_contributions() {
231        let mut r = Registry::new();
232        r.set_owner(A);
233        r.add(Widget("a"));
234        r.add(7u32);
235        r.set_owner(B);
236        r.add(Widget("b"));
237
238        r.purge_owner(A);
239        // B's widget survives; A's widget and A's u32 (its whole bucket) are gone.
240        assert_eq!(r.items::<Widget>(), vec![&Widget("b")]);
241        assert!(r.items::<u32>().is_empty());
242    }
243
244    #[test]
245    fn distinct_types_are_independent() {
246        let mut r = Registry::new();
247        r.set_owner(A);
248        r.add(Widget("w"));
249        r.add(7u32);
250        assert_eq!(r.items::<Widget>(), vec![&Widget("w")]);
251        assert_eq!(r.items::<u32>(), vec![&7u32]);
252    }
253}