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    /// This module's UI-string catalogs, as `(locale, po_text)` pairs, each an
95    /// `include_str!`'d gettext PO baked into the binary. English needs none (its
96    /// source string is the key); other locales ship a PO. The boot loader merges
97    /// them into the shared catalog store. Default: none.
98    fn catalogs(&self) -> &'static [(&'static str, &'static str)] {
99        &[]
100    }
101}
102
103/// Ordered collection of registered modules.
104#[derive(Default)]
105pub struct ModuleRegistry {
106    modules: Vec<Box<dyn Module>>,
107}
108
109impl ModuleRegistry {
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// Registers a module. Registration order breaks ties in the ordering, so
115    /// foundational modules register first.
116    pub fn register(&mut self, module: impl Module) -> &mut Self {
117        self.modules.push(Box::new(module));
118        self
119    }
120
121    /// Registers an already-boxed module, for callers assembling a
122    /// `Vec<Box<dyn Module>>` (the built-in set, an app's plugins).
123    pub fn register_boxed(&mut self, module: Box<dyn Module>) -> &mut Self {
124        self.modules.push(module);
125        self
126    }
127
128    /// Whether a module with this id is registered.
129    pub fn has(&self, id: ModuleId) -> bool {
130        self.modules.iter().any(|m| m.id() == id)
131    }
132
133    /// The registered module with this id, if any.
134    pub fn get(&self, id: ModuleId) -> Option<&dyn Module> {
135        self.modules
136            .iter()
137            .map(|m| m.as_ref())
138            .find(|m| m.id() == id)
139    }
140
141    pub fn iter(&self) -> impl Iterator<Item = &dyn Module> + '_ {
142        self.modules.iter().map(|m| m.as_ref())
143    }
144
145    pub fn len(&self) -> usize {
146        self.modules.len()
147    }
148
149    pub fn is_empty(&self) -> bool {
150        self.modules.is_empty()
151    }
152
153    /// The registered modules in dependency order (a module after every module it
154    /// [`requires`](Module::requires)); registration order breaks ties. Errors on
155    /// a duplicate id, a required module that is not registered, or a cycle. This
156    /// is the canonical order for both migrations and registration.
157    pub fn ordered(&self) -> CoreResult<Vec<&dyn Module>> {
158        let mut by_id: HashMap<ModuleId, &dyn Module> = HashMap::with_capacity(self.modules.len());
159        for m in &self.modules {
160            if by_id.insert(m.id(), m.as_ref()).is_some() {
161                return Err(CoreError::DuplicateModule {
162                    module: m.id().to_string(),
163                });
164            }
165        }
166        for m in &self.modules {
167            for dep in m.requires() {
168                if !by_id.contains_key(dep) {
169                    return Err(CoreError::UnknownModuleDependency {
170                        module: m.id().to_string(),
171                        dependency: dep.to_string(),
172                    });
173                }
174            }
175        }
176
177        let mut state: HashMap<ModuleId, Visit> = HashMap::with_capacity(self.modules.len());
178        let mut order: Vec<ModuleId> = Vec::with_capacity(self.modules.len());
179        for m in &self.modules {
180            visit(m.id(), &by_id, &mut state, &mut order)?;
181        }
182        Ok(order.into_iter().map(|id| by_id[&id]).collect())
183    }
184
185    /// Every module's [`MigrationSet`] in dependency order. Feed the result to
186    /// [`migration::run`](crate::migration::run).
187    pub fn ordered_migration_sets(&self) -> CoreResult<Vec<MigrationSet>> {
188        Ok(self
189            .ordered()?
190            .into_iter()
191            .map(|m| m.migrations())
192            .collect())
193    }
194}
195
196#[derive(Clone, Copy)]
197enum Visit {
198    Active,
199    Done,
200}
201
202/// Depth-first post-order visit; `Active` marks the current stack, so meeting it
203/// again is a cycle.
204fn visit(
205    id: ModuleId,
206    by_id: &HashMap<ModuleId, &dyn Module>,
207    state: &mut HashMap<ModuleId, Visit>,
208    order: &mut Vec<ModuleId>,
209) -> CoreResult<()> {
210    match state.get(&id) {
211        Some(Visit::Done) => return Ok(()),
212        Some(Visit::Active) => {
213            return Err(CoreError::ModuleDependencyCycle {
214                module: id.to_string(),
215            })
216        }
217        None => {}
218    }
219    state.insert(id, Visit::Active);
220    for dep in by_id[&id].requires() {
221        visit(*dep, by_id, state, order)?;
222    }
223    state.insert(id, Visit::Done);
224    order.push(id);
225    Ok(())
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    /// A test module: fixed id and required-module ids, no migrations.
233    struct Mod(ModuleId, &'static [ModuleId]);
234
235    impl Module for Mod {
236        fn id(&self) -> ModuleId {
237            self.0
238        }
239        fn requires(&self) -> &'static [ModuleId] {
240            self.1
241        }
242    }
243
244    const BLOG: ModuleId = ModuleId::new("acme.blog");
245    const USER: ModuleId = ModuleId::new("acme.user");
246    const MEDIA: ModuleId = ModuleId::new("acme.media");
247
248    fn order_of(reg: &ModuleRegistry) -> Vec<&'static str> {
249        reg.ordered()
250            .unwrap()
251            .into_iter()
252            .map(|m| m.id().as_str())
253            .collect()
254    }
255
256    #[test]
257    fn dependencies_come_before_dependents() {
258        let mut reg = ModuleRegistry::new();
259        // Registered dependents-first on purpose: ordering must still put deps first.
260        reg.register(Mod(BLOG, &[USER, MEDIA]))
261            .register(Mod(MEDIA, &[]))
262            .register(Mod(USER, &[]));
263        let order = order_of(&reg);
264        let pos = |id: &str| order.iter().position(|x| *x == id).unwrap();
265        assert!(pos("acme.user") < pos("acme.blog"));
266        assert!(pos("acme.media") < pos("acme.blog"));
267    }
268
269    #[test]
270    fn registration_order_breaks_ties() {
271        let mut reg = ModuleRegistry::new();
272        reg.register(Mod(ModuleId::new("a"), &[]))
273            .register(Mod(ModuleId::new("b"), &[]))
274            .register(Mod(ModuleId::new("c"), &[]));
275        assert_eq!(order_of(&reg), ["a", "b", "c"]);
276    }
277
278    #[test]
279    fn has_and_get_find_by_id() {
280        let mut reg = ModuleRegistry::new();
281        reg.register(Mod(MEDIA, &[]));
282        assert!(reg.has(MEDIA));
283        assert!(!reg.has(USER));
284        assert_eq!(reg.get(MEDIA).unwrap().id(), MEDIA);
285        assert!(reg.get(ModuleId::new("missing")).is_none());
286    }
287
288    #[test]
289    fn unknown_dependency_errors() {
290        let mut reg = ModuleRegistry::new();
291        reg.register(Mod(BLOG, &[USER]));
292        assert!(matches!(
293            reg.ordered(),
294            Err(CoreError::UnknownModuleDependency { .. })
295        ));
296    }
297
298    #[test]
299    fn cycle_errors() {
300        // Required-module arrays are static, so declare them as consts (a user
301        // const fn's result is not promoted to `'static`).
302        const A_REQS: &[ModuleId] = &[ModuleId::new("acme.b")];
303        const B_REQS: &[ModuleId] = &[ModuleId::new("acme.a")];
304        let mut reg = ModuleRegistry::new();
305        reg.register(Mod(ModuleId::new("acme.a"), A_REQS))
306            .register(Mod(ModuleId::new("acme.b"), B_REQS));
307        assert!(matches!(
308            reg.ordered(),
309            Err(CoreError::ModuleDependencyCycle { .. })
310        ));
311    }
312
313    #[test]
314    fn duplicate_id_errors() {
315        let mut reg = ModuleRegistry::new();
316        reg.register(Mod(ModuleId::new("a"), &[]))
317            .register(Mod(ModuleId::new("a"), &[]));
318        assert!(matches!(
319            reg.ordered(),
320            Err(CoreError::DuplicateModule { .. })
321        ));
322    }
323}