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