Skip to main content

laterite_core/
module.rs

1//! Module registration.
2//!
3//! Both framework crates and application feature crates (plugins) expose a
4//! [`Module`]: a unit with a stable [`ModuleId`], its own migrations, the modules
5//! and database capabilities it requires, and the contributions it registers. The
6//! binary registers them in a [`ModuleRegistry`], which orders everything so a
7//! module's dependencies come first, and answers whether a module is present.
8
9use std::collections::HashMap;
10
11use crate::error::{CoreError, CoreResult};
12use crate::migration::MigrationSet;
13use crate::registry::Registry;
14
15/// A module's stable identity: a lowercase `vendor.package` code (e.g.
16/// `ModuleId::new("acme.blog")`), declared once and never changed. A newtype so an
17/// id is never confused with an arbitrary string.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct ModuleId(&'static str);
20
21impl ModuleId {
22    pub const fn new(code: &'static str) -> Self {
23        Self(code)
24    }
25    pub const fn as_str(&self) -> &'static str {
26        self.0
27    }
28}
29
30impl std::fmt::Display for ModuleId {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        f.write_str(self.0)
33    }
34}
35
36/// A named database capability a module requires or uses optionally (currently a
37/// PostgreSQL extension, e.g. `Capability::new("postgis")`). A newtype for the
38/// same reason as [`ModuleId`].
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
40pub struct Capability(&'static str);
41
42impl Capability {
43    pub const fn new(name: &'static str) -> Self {
44        Self(name)
45    }
46    pub const fn as_str(&self) -> &'static str {
47        self.0
48    }
49}
50
51/// A registerable unit: a framework crate or an application feature/plugin.
52pub trait Module: Send + Sync + 'static {
53    /// Stable identity, a lowercase `vendor.package` code. Namespaces this
54    /// module's applied-migration history and is the target of other modules'
55    /// [`requires`](Module::requires). Its string must equal the `module_id` of
56    /// the set returned by [`migrations`](Module::migrations).
57    fn id(&self) -> ModuleId;
58
59    /// This module's migrations, in apply order. Defaults to none, for a
60    /// code-only module.
61    fn migrations(&self) -> MigrationSet {
62        MigrationSet::new(self.id().as_str(), Vec::new())
63    }
64
65    /// Ids of modules whose migrations and registration must run before this
66    /// module's, so cross-module foreign keys and contributions resolve. Defaults
67    /// to none.
68    fn requires(&self) -> &'static [ModuleId] {
69        &[]
70    }
71
72    /// Database capabilities this module cannot work without. The application
73    /// refuses to boot if one is unavailable, with a clear error naming the
74    /// module and capability, instead of failing deep inside a migration.
75    fn requires_db_capabilities(&self) -> &'static [Capability] {
76        &[]
77    }
78
79    /// Database capabilities this module uses when present and does without when
80    /// absent. Query the boot-time [`CapabilitySet`](crate::capabilities::CapabilitySet)
81    /// to gate the enhanced path (for example trigram fuzzy search).
82    fn optional_db_capabilities(&self) -> &'static [Capability] {
83        &[]
84    }
85
86    /// Contributes this module's items (resources, permissions, nav, settings,
87    /// field types, and any plugin-defined extension items) to the [`Registry`].
88    /// Runs in [`requires`](Module::requires) order, after migrations. Default:
89    /// contributes nothing.
90    fn register(&self, registry: &mut Registry) {
91        let _ = registry;
92    }
93
94    /// Where this module's admin screens mount, relative to the admin root.
95    ///
96    /// `None` (the default) namespaces them under the module's identity, so
97    /// `rainmill.location` contributing `/nodes` serves `/rainmill/location/nodes`
98    /// and two modules cannot collide by accident. Declaring a base takes that
99    /// path instead, which is a claim: a clash with another module aborts boot.
100    /// A deployment can override either through `backend.paths`.
101    fn admin_base(&self) -> Option<&'static str> {
102        None
103    }
104
105    /// This module's UI-string catalogs, as `(locale, po_text)` pairs, each an
106    /// `include_str!`'d gettext PO baked into the binary. English needs none (its
107    /// source string is the key); other locales ship a PO. The boot loader merges
108    /// them into the shared catalog store. Default: none.
109    fn catalogs(&self) -> &'static [(&'static str, &'static str)] {
110        &[]
111    }
112}
113
114/// Ordered collection of registered modules.
115#[derive(Default)]
116pub struct ModuleRegistry {
117    modules: Vec<Box<dyn Module>>,
118}
119
120impl ModuleRegistry {
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    /// Registers a module. Registration order breaks ties in the ordering, so
126    /// foundational modules register first.
127    pub fn register(&mut self, module: impl Module) -> &mut Self {
128        self.modules.push(Box::new(module));
129        self
130    }
131
132    /// Registers an already-boxed module, for callers assembling a
133    /// `Vec<Box<dyn Module>>` (the built-in set, an app's plugins).
134    pub fn register_boxed(&mut self, module: Box<dyn Module>) -> &mut Self {
135        self.modules.push(module);
136        self
137    }
138
139    /// Whether a module with this id is registered.
140    pub fn has(&self, id: ModuleId) -> bool {
141        self.modules.iter().any(|m| m.id() == id)
142    }
143
144    /// The registered module with this id, if any.
145    pub fn get(&self, id: ModuleId) -> Option<&dyn Module> {
146        self.modules
147            .iter()
148            .map(|m| m.as_ref())
149            .find(|m| m.id() == id)
150    }
151
152    pub fn iter(&self) -> impl Iterator<Item = &dyn Module> + '_ {
153        self.modules.iter().map(|m| m.as_ref())
154    }
155
156    pub fn len(&self) -> usize {
157        self.modules.len()
158    }
159
160    pub fn is_empty(&self) -> bool {
161        self.modules.is_empty()
162    }
163
164    /// The registered modules in dependency order (a module after every module it
165    /// [`requires`](Module::requires)); registration order breaks ties. Errors on
166    /// a duplicate id, a required module that is not registered, or a cycle. This
167    /// is the canonical order for both migrations and registration.
168    pub fn ordered(&self) -> CoreResult<Vec<&dyn Module>> {
169        let mut by_id: HashMap<ModuleId, &dyn Module> = HashMap::with_capacity(self.modules.len());
170        for m in &self.modules {
171            if by_id.insert(m.id(), m.as_ref()).is_some() {
172                return Err(CoreError::DuplicateModule {
173                    module: m.id().to_string(),
174                });
175            }
176        }
177        for m in &self.modules {
178            for dep in m.requires() {
179                if !by_id.contains_key(dep) {
180                    return Err(CoreError::UnknownModuleDependency {
181                        module: m.id().to_string(),
182                        dependency: dep.to_string(),
183                    });
184                }
185            }
186        }
187
188        let mut state: HashMap<ModuleId, Visit> = HashMap::with_capacity(self.modules.len());
189        let mut order: Vec<ModuleId> = Vec::with_capacity(self.modules.len());
190        for m in &self.modules {
191            visit(m.id(), &by_id, &mut state, &mut order)?;
192        }
193        Ok(order.into_iter().map(|id| by_id[&id]).collect())
194    }
195
196    /// Every module's [`MigrationSet`] in dependency order. Feed the result to
197    /// [`migration::run`](crate::migration::run).
198    pub fn ordered_migration_sets(&self) -> CoreResult<Vec<MigrationSet>> {
199        Ok(self
200            .ordered()?
201            .into_iter()
202            .map(|m| m.migrations())
203            .collect())
204    }
205}
206
207#[derive(Clone, Copy)]
208enum Visit {
209    Active,
210    Done,
211}
212
213/// Depth-first post-order visit; `Active` marks the current stack, so meeting it
214/// again is a cycle.
215fn visit(
216    id: ModuleId,
217    by_id: &HashMap<ModuleId, &dyn Module>,
218    state: &mut HashMap<ModuleId, Visit>,
219    order: &mut Vec<ModuleId>,
220) -> CoreResult<()> {
221    match state.get(&id) {
222        Some(Visit::Done) => return Ok(()),
223        Some(Visit::Active) => {
224            return Err(CoreError::ModuleDependencyCycle {
225                module: id.to_string(),
226            })
227        }
228        None => {}
229    }
230    state.insert(id, Visit::Active);
231    for dep in by_id[&id].requires() {
232        visit(*dep, by_id, state, order)?;
233    }
234    state.insert(id, Visit::Done);
235    order.push(id);
236    Ok(())
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    /// A test module: fixed id and required-module ids, no migrations.
244    struct Mod(ModuleId, &'static [ModuleId]);
245
246    impl Module for Mod {
247        fn id(&self) -> ModuleId {
248            self.0
249        }
250        fn requires(&self) -> &'static [ModuleId] {
251            self.1
252        }
253    }
254
255    const BLOG: ModuleId = ModuleId::new("acme.blog");
256    const USER: ModuleId = ModuleId::new("acme.user");
257    const MEDIA: ModuleId = ModuleId::new("acme.media");
258
259    fn order_of(reg: &ModuleRegistry) -> Vec<&'static str> {
260        reg.ordered()
261            .unwrap()
262            .into_iter()
263            .map(|m| m.id().as_str())
264            .collect()
265    }
266
267    #[test]
268    fn dependencies_come_before_dependents() {
269        let mut reg = ModuleRegistry::new();
270        // Registered dependents-first on purpose: ordering must still put deps first.
271        reg.register(Mod(BLOG, &[USER, MEDIA]))
272            .register(Mod(MEDIA, &[]))
273            .register(Mod(USER, &[]));
274        let order = order_of(&reg);
275        let pos = |id: &str| order.iter().position(|x| *x == id).unwrap();
276        assert!(pos("acme.user") < pos("acme.blog"));
277        assert!(pos("acme.media") < pos("acme.blog"));
278    }
279
280    #[test]
281    fn registration_order_breaks_ties() {
282        let mut reg = ModuleRegistry::new();
283        reg.register(Mod(ModuleId::new("a"), &[]))
284            .register(Mod(ModuleId::new("b"), &[]))
285            .register(Mod(ModuleId::new("c"), &[]));
286        assert_eq!(order_of(&reg), ["a", "b", "c"]);
287    }
288
289    #[test]
290    fn has_and_get_find_by_id() {
291        let mut reg = ModuleRegistry::new();
292        reg.register(Mod(MEDIA, &[]));
293        assert!(reg.has(MEDIA));
294        assert!(!reg.has(USER));
295        assert_eq!(reg.get(MEDIA).unwrap().id(), MEDIA);
296        assert!(reg.get(ModuleId::new("missing")).is_none());
297    }
298
299    #[test]
300    fn unknown_dependency_errors() {
301        let mut reg = ModuleRegistry::new();
302        reg.register(Mod(BLOG, &[USER]));
303        assert!(matches!(
304            reg.ordered(),
305            Err(CoreError::UnknownModuleDependency { .. })
306        ));
307    }
308
309    #[test]
310    fn cycle_errors() {
311        // Required-module arrays are static, so declare them as consts (a user
312        // const fn's result is not promoted to `'static`).
313        const A_REQS: &[ModuleId] = &[ModuleId::new("acme.b")];
314        const B_REQS: &[ModuleId] = &[ModuleId::new("acme.a")];
315        let mut reg = ModuleRegistry::new();
316        reg.register(Mod(ModuleId::new("acme.a"), A_REQS))
317            .register(Mod(ModuleId::new("acme.b"), B_REQS));
318        assert!(matches!(
319            reg.ordered(),
320            Err(CoreError::ModuleDependencyCycle { .. })
321        ));
322    }
323
324    #[test]
325    fn duplicate_id_errors() {
326        let mut reg = ModuleRegistry::new();
327        reg.register(Mod(ModuleId::new("a"), &[]))
328            .register(Mod(ModuleId::new("a"), &[]));
329        assert!(matches!(
330            reg.ordered(),
331            Err(CoreError::DuplicateModule { .. })
332        ));
333    }
334}