Skip to main content

backbone_core/
module.rs

1//! Backbone Module System
2//!
3//! This module provides the trait for modules to register themselves with
4//! the framework, including migration discovery for automatic database setup.
5//!
6//! Inspired by Laravel's Service Provider pattern.
7
8use async_trait::async_trait;
9use std::path::PathBuf;
10
11/// Metadata about a module's migrations
12#[derive(Debug, Clone)]
13pub struct MigrationInfo {
14    /// Module name (e.g., "bersihir")
15    pub module: String,
16    /// Path to migrations directory
17    pub path: PathBuf,
18    /// Number of migrations found (optional, for status display)
19    pub count: Option<usize>,
20}
21
22/// Metadata about a module's seeds
23#[derive(Debug, Clone)]
24pub struct SeedInfo {
25    /// Module name
26    pub module: String,
27    /// Path to seeds directory
28    pub path: PathBuf,
29}
30
31/// Trait for Backbone modules with migration support
32///
33/// Implement this trait to register a module with the Backbone framework.
34/// The framework will automatically discover and run migrations for all
35/// registered modules in dependency order.
36///
37/// # Example
38///
39/// ```ignore
40/// use backbone_core::module::BackboneModule;
41/// use std::path::PathBuf;
42///
43/// pub struct BersihirModule;
44///
45/// impl BackboneModule for BersihirModule {
46///     fn name(&self) -> &'static str { "bersihir" }
47///     fn version(&self) -> &'static str { env!("CARGO_PKG_VERSION") }
48///
49///     fn dependencies(&self) -> Vec<&'static str> {
50///         vec!["sapiens"] // Depends on user module
51///     }
52///
53///     fn migrations_path(&self) -> Option<PathBuf> {
54///         Some(PathBuf::from("libs/modules/bersihir/migrations"))
55///     }
56/// }
57/// ```
58#[async_trait]
59pub trait BackboneModule: Send + Sync {
60    /// Returns the unique identifier for this module
61    ///
62    /// This should be a short, lowercase name like "bersihir", "sapiens", etc.
63    fn name(&self) -> &'static str;
64
65    /// Returns the version of this module
66    ///
67    /// Typically uses `env!("CARGO_PKG_VERSION")` to get from Cargo.toml
68    fn version(&self) -> &'static str;
69
70    /// Returns the list of module names this module depends on
71    ///
72    /// Dependencies are used to determine migration order. A module's
73    /// migrations will only run after all its dependencies have been migrated.
74    ///
75    /// # Returns
76    /// A list of module names (e.g., `vec!["sapiens", "bucket"]`)
77    fn dependencies(&self) -> Vec<&'static str> {
78        vec![]
79    }
80
81    /// Returns the path to this module's migrations directory
82    ///
83    /// Path should be relative to the repository root.
84    /// Return `None` if the module has no migrations.
85    ///
86    /// # Example paths:
87    /// - `libs/modules/bersihir/migrations`
88    /// - `libs/modules/sapiens/migrations`
89    fn migrations_path(&self) -> Option<PathBuf>;
90
91    /// Returns the path to this module's seeds directory (optional)
92    ///
93    /// Seeds are used to populate the database with initial data.
94    fn seeds_path(&self) -> Option<PathBuf> {
95        None
96    }
97
98    /// Called after migrations are run to initialize the module
99    ///
100    /// Use this for any setup that needs to happen after the database
101    /// is ready but before the application starts serving requests.
102    async fn on_boot(&self) -> anyhow::Result<()> {
103        Ok(())
104    }
105
106    /// Called when the application is shutting down
107    ///
108    /// Use this for cleanup tasks.
109    async fn on_shutdown(&self) -> anyhow::Result<()> {
110        Ok(())
111    }
112
113    /// Perform a health check for this module
114    ///
115    /// Returns `true` if the module is healthy and ready to serve requests.
116    async fn health_check(&self) -> bool {
117        true
118    }
119
120    /// Get migration info for this module
121    fn migration_info(&self) -> Option<MigrationInfo> {
122        self.migrations_path().map(|path| MigrationInfo {
123            module: self.name().to_string(),
124            path,
125            count: None,
126        })
127    }
128
129    /// Get seed info for this module
130    fn seed_info(&self) -> Option<SeedInfo> {
131        self.seeds_path().map(|path| SeedInfo {
132            module: self.name().to_string(),
133            path,
134        })
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    struct TestModule;
143
144    impl BackboneModule for TestModule {
145        fn name(&self) -> &'static str {
146            "test"
147        }
148
149        fn version(&self) -> &'static str {
150            "0.1.0"
151        }
152
153        fn dependencies(&self) -> Vec<&'static str> {
154            vec!["core"]
155        }
156
157        fn migrations_path(&self) -> Option<PathBuf> {
158            Some(PathBuf::from("libs/modules/test/migrations"))
159        }
160
161        fn seeds_path(&self) -> Option<PathBuf> {
162            Some(PathBuf::from("libs/modules/test/migrations/seeds"))
163        }
164    }
165
166    #[test]
167    fn test_module_metadata() {
168        let module = TestModule;
169        assert_eq!(module.name(), "test");
170        assert_eq!(module.version(), "0.1.0");
171        assert_eq!(module.dependencies(), vec!["core"]);
172    }
173
174    #[test]
175    fn test_migration_info() {
176        let module = TestModule;
177        let info = module.migration_info();
178        assert!(info.is_some());
179        let info = info.unwrap();
180        assert_eq!(info.module, "test");
181        assert_eq!(info.path, PathBuf::from("libs/modules/test/migrations"));
182    }
183
184    #[test]
185    fn test_seed_info() {
186        let module = TestModule;
187        let info = module.seed_info();
188        assert!(info.is_some());
189        let info = info.unwrap();
190        assert_eq!(info.module, "test");
191        assert_eq!(info.path, PathBuf::from("libs/modules/test/migrations/seeds"));
192    }
193}