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 + Send + Sync>,
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: Send + Sync + 'static>(&mut self, item: T) {
77        self.add_with(item, ContributeOpts::default());
78    }
79
80    /// Contributes `item` with an explicit priority and mode.
81    ///
82    /// `Send + Sync` because a contribution the framework does not consume stays
83    /// readable after boot, from request handlers on any thread.
84    pub fn add_with<T: Send + Sync + 'static>(&mut self, item: T, opts: ContributeOpts) {
85        let owner = self
86            .current
87            .expect("Registry::add called outside a module register() pass");
88        self.by_type
89            .entry(TypeId::of::<T>())
90            .or_default()
91            .push(Entry {
92                owner,
93                priority: opts.priority,
94                mode: opts.mode,
95                item: Box::new(item),
96            });
97    }
98
99    /// The contributions of type `T`, resolved: a `Replace` supersedes everything
100    /// before it, then higher priority sorts first, ties keeping dependency order.
101    pub fn contributions<T: 'static>(&self) -> Vec<Contribution<'_, T>> {
102        let Some(entries) = self.by_type.get(&TypeId::of::<T>()) else {
103            return Vec::new();
104        };
105        let start = entries
106            .iter()
107            .rposition(|e| e.mode == ContributeMode::Replace)
108            .unwrap_or(0);
109        let mut resolved: Vec<&Entry> = entries[start..].iter().collect();
110        // Stable sort, so equal priorities keep insertion (dependency) order.
111        resolved.sort_by_key(|a| std::cmp::Reverse(a.priority));
112        resolved
113            .into_iter()
114            .map(|e| Contribution {
115                owner: e.owner,
116                priority: e.priority,
117                mode: e.mode,
118                item: e
119                    .item
120                    .downcast_ref::<T>()
121                    .expect("keyed by TypeId::of::<T>"),
122            })
123            .collect()
124    }
125
126    /// The items of type `T`, in resolved order (envelope dropped).
127    pub fn items<T: 'static>(&self) -> Vec<&T> {
128        self.contributions::<T>()
129            .into_iter()
130            .map(|c| c.item)
131            .collect()
132    }
133
134    /// Removes and returns the items of type `T` as owned values, resolved the
135    /// same way as [`items`](Registry::items). For a one-time collection at boot;
136    /// the type's contributions are consumed, so no `Clone` bound is needed.
137    /// Like [`Registry::take`], but keeps each item's owning module, for a
138    /// consumer that resolves per-module behaviour (an admin path namespace).
139    pub fn take_owned<T: 'static>(&mut self) -> Vec<(ModuleId, T)> {
140        let owners: Vec<ModuleId> = self.contributions::<T>().iter().map(|c| c.owner).collect();
141        owners.into_iter().zip(self.take::<T>()).collect()
142    }
143
144    pub fn take<T: 'static>(&mut self) -> Vec<T> {
145        let Some(entries) = self.by_type.remove(&TypeId::of::<T>()) else {
146            return Vec::new();
147        };
148        let start = entries
149            .iter()
150            .rposition(|e| e.mode == ContributeMode::Replace)
151            .unwrap_or(0);
152        let mut kept: Vec<Entry> = entries.into_iter().skip(start).collect();
153        // Stable sort, so equal priorities keep insertion (dependency) order.
154        kept.sort_by_key(|a| std::cmp::Reverse(a.priority));
155        kept.into_iter()
156            .map(|e| *e.item.downcast::<T>().expect("keyed by TypeId::of::<T>"))
157            .collect()
158    }
159
160    /// Drops every contribution stamped with `owner`, across all types. Rolls back
161    /// a module's partial contributions when its `register` fails, so a quarantined
162    /// module leaves nothing behind.
163    pub fn purge_owner(&mut self, owner: ModuleId) {
164        for entries in self.by_type.values_mut() {
165            entries.retain(|e| e.owner != owner);
166        }
167        self.by_type.retain(|_, entries| !entries.is_empty());
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    const A: ModuleId = ModuleId::new("acme.a");
176    const B: ModuleId = ModuleId::new("acme.b");
177
178    #[derive(Debug, PartialEq)]
179    struct Widget(&'static str);
180
181    #[test]
182    fn collects_in_order_and_stamps_owner() {
183        let mut r = Registry::new();
184        r.set_owner(A);
185        r.add(Widget("one"));
186        r.set_owner(B);
187        r.add(Widget("two"));
188
189        let cs = r.contributions::<Widget>();
190        assert_eq!(cs.len(), 2);
191        assert_eq!(cs[0].item, &Widget("one"));
192        assert_eq!(cs[0].owner, A);
193        assert_eq!(cs[1].owner, B);
194        assert_eq!(r.items::<Widget>(), vec![&Widget("one"), &Widget("two")]);
195    }
196
197    #[test]
198    fn higher_priority_sorts_first_ties_keep_order() {
199        let mut r = Registry::new();
200        r.set_owner(A);
201        r.add(Widget("low"));
202        r.add_with(
203            Widget("high"),
204            ContributeOpts {
205                priority: 10,
206                mode: ContributeMode::Append,
207            },
208        );
209        r.add(Widget("low2"));
210        assert_eq!(
211            r.items::<Widget>(),
212            vec![&Widget("high"), &Widget("low"), &Widget("low2")]
213        );
214    }
215
216    #[test]
217    fn replace_supersedes_earlier() {
218        let mut r = Registry::new();
219        r.set_owner(A);
220        r.add(Widget("default"));
221        r.set_owner(B);
222        r.add_with(
223            Widget("override"),
224            ContributeOpts {
225                priority: 0,
226                mode: ContributeMode::Replace,
227            },
228        );
229        assert_eq!(r.items::<Widget>(), vec![&Widget("override")]);
230    }
231
232    #[test]
233    fn purge_owner_drops_only_that_owners_contributions() {
234        let mut r = Registry::new();
235        r.set_owner(A);
236        r.add(Widget("a"));
237        r.add(7u32);
238        r.set_owner(B);
239        r.add(Widget("b"));
240
241        r.purge_owner(A);
242        // B's widget survives; A's widget and A's u32 (its whole bucket) are gone.
243        assert_eq!(r.items::<Widget>(), vec![&Widget("b")]);
244        assert!(r.items::<u32>().is_empty());
245    }
246
247    #[test]
248    fn distinct_types_are_independent() {
249        let mut r = Registry::new();
250        r.set_owner(A);
251        r.add(Widget("w"));
252        r.add(7u32);
253        assert_eq!(r.items::<Widget>(), vec![&Widget("w")]);
254        assert_eq!(r.items::<u32>(), vec![&7u32]);
255    }
256}