Skip to main content

blvm_sdk/module/
mod.rs

1//! Module Development APIs
2//!
3//! Provides APIs for developing modules that extend blvm-node.
4//!
5//! This module re-exports the necessary types and traits from `blvm-node` to provide
6//! a clean, developer-friendly interface for module development.
7//!
8//! The `wasm` submodule (when `wasm-modules` is enabled) is node-independent:
9//! embedders implement `WasmStorage`/`WasmTree` to bridge to their storage.
10
11#[cfg(feature = "node")]
12pub mod bootstrap;
13#[cfg(feature = "node")]
14pub mod cli_args;
15#[cfg(feature = "node")]
16pub mod database;
17#[cfg(feature = "node")]
18pub mod module_db;
19#[cfg(feature = "node")]
20pub mod storage;
21
22#[cfg(feature = "wasm-modules")]
23pub mod wasm;
24
25/// Minimal module entry point. Expands to full main with bootstrap, migrations, config load, run_module.
26///
27/// **Single-arg form** (preferred when `#[module]` has `config` and `migrations`):
28/// ```ignore
29/// run_module_main!(DemoModule);
30/// ```
31///
32/// **Explicit form** (when ModuleMeta is not implemented):
33/// ```ignore
34/// run_module_main!("demo", DemoModule, DemoConfig, migrations!(1 => up_initial, 2 => up_add_items_tree));
35/// ```
36#[cfg(feature = "node")]
37#[macro_export]
38macro_rules! run_module_main {
39    ($module_type:ty) => {
40        #[tokio::main]
41        async fn main() -> Result<(), Box<dyn std::error::Error>> {
42            let bootstrap = $crate::module::ModuleBootstrap::from_env()?;
43            blvm_node::utils::init_module_logging(
44                <$module_type as $crate::module::ModuleMeta>::MODULE_NAME
45                    .replace('-', "_")
46                    .as_str(),
47                None,
48            );
49            let db = $crate::module::ModuleDb::open(&bootstrap.data_dir)?;
50            db.run_migrations(<$module_type as $crate::module::ModuleMeta>::migrations())?;
51            let config = <<$module_type as $crate::module::ModuleMeta>::Config>::load(
52                bootstrap.data_dir.join("config.toml"),
53            )
54            .unwrap_or_default();
55            let module = <$module_type as $crate::module::ModuleMeta>::__module_new(config);
56            $crate::run_module! {
57                bootstrap: &bootstrap,
58                module_name: <$module_type as $crate::module::ModuleMeta>::MODULE_NAME,
59                module: module,
60                module_type: $module_type,
61                db: db.as_db(),
62            }?;
63            Ok(())
64        }
65    };
66    (
67        $module_name:expr,
68        $module_type:ty,
69        $config_type:ty,
70        $migrations:expr,
71    ) => {
72        #[tokio::main]
73        async fn main() -> Result<(), Box<dyn std::error::Error>> {
74            blvm_node::utils::init_module_logging($module_name.replace('-', "_").as_str(), None);
75            let bootstrap = $crate::module::ModuleBootstrap::from_env()?;
76            let db = $crate::module::ModuleDb::open(&bootstrap.data_dir)?;
77            db.run_migrations($migrations)?;
78            let config =
79                <$config_type>::load(bootstrap.data_dir.join("config.toml")).unwrap_or_default();
80            let module = <$module_type>::__module_new(config);
81            $crate::run_module! {
82                bootstrap: &bootstrap,
83                module_name: $module_name,
84                module: module,
85                module_type: $module_type,
86                db: db.as_db(),
87            }?;
88            Ok(())
89        }
90    };
91}
92
93/// Collect migrations for `run_migrations`. Sugar for `&[(1, up_initial), (2, up_add_cache), ...]`.
94///
95/// # Example
96/// ```ignore
97/// run_migrations(&db, migrations!(1 => up_initial, 2 => up_add_cache))?;
98/// ```
99#[cfg(feature = "node")]
100#[macro_export]
101macro_rules! migrations {
102    ($($v:literal => $up:ident),* $(,)?) => {
103        &[$(($v, $up as $crate::module::MigrationUp)),*]
104    };
105}
106
107/// Run a module with automatic connect, CLI/RPC/event registration, and dispatch.
108///
109/// Replaces manual main-loop boilerplate. CLI spec, RPC methods, and event types are
110/// auto-discovered from #[command], #[rpc_methods], and #[event_handlers]. On unload
111/// (invocation channel closed), the module exits cleanly.
112///
113/// # Example (unified module — preferred)
114/// ```ignore
115/// let bootstrap = ModuleBootstrap::from_env()?;
116/// run_module! {
117///     bootstrap: &bootstrap,
118///     module_name: "demo",
119///     module: DemoModule { config },
120///     module_type: DemoModule,
121///     db,
122/// }
123/// ```
124///
125/// # Example (explicit args — legacy)
126/// ```ignore
127/// run_module! {
128///     socket_path: args.socket_path.clone(),
129///     module_id: &args.module_id,
130///     module_name: "demo-module",
131///     version: env!("CARGO_PKG_VERSION"),
132///     cli: DemoCli,
133///     cli_type: DemoCli,
134///     module_type: DemoModule,
135///     module: DemoModule { config: config.clone() },
136///     db,
137/// }
138/// ```
139#[cfg(feature = "node")]
140#[macro_export]
141macro_rules! run_module {
142    (
143        bootstrap: $bootstrap:expr,
144        module_name: $module_name:expr,
145        module: $module:expr,
146        module_type: $module_type:ty,
147        db: $db:expr,
148    ) => {{
149        let __bootstrap = $bootstrap;
150        let __module = $module;
151        $crate::run_module! {
152            socket_path: __bootstrap.socket_path.clone(),
153            module_id: &__bootstrap.module_id,
154            module_name: $module_name,
155            version: env!("CARGO_PKG_VERSION"),
156            cli: __module.clone(),
157            cli_type: $module_type,
158            module_type: $module_type,
159            module: __module,
160            db: $db,
161        }
162    }};
163    (
164        bootstrap: $bootstrap:expr,
165        module_name: $module_name:expr,
166        module_type: $module_type:ty,
167        cli_type: $cli_type:ty,
168        db: $db:expr,
169        setup: $setup:expr,
170        event_types: $event_types:expr,
171    ) => {{
172        let __bootstrap = $bootstrap;
173        let __db = Arc::clone(&$db);
174        $crate::run_module! {
175            socket_path: __bootstrap.socket_path.clone(),
176            module_id: &__bootstrap.module_id,
177            module_name: $module_name,
178            version: env!("CARGO_PKG_VERSION"),
179            module_type: $module_type,
180            cli_type: $cli_type,
181            db: __db,
182            setup: $setup,
183            event_types: $event_types,
184            on_event: |e, m: &$module_type, ctx| {
185                let m = m.clone();
186                let ctx = ctx.clone();
187                async move { m.dispatch_event(e, &ctx).await }
188            },
189            data_dir: __bootstrap.data_dir.as_path(),
190        }
191    }};
192    (
193        bootstrap: $bootstrap:expr,
194        module_name: $module_name:expr,
195        module_type: $module_type:ty,
196        cli_type: $cli_type:ty,
197        db: $db:expr,
198        setup: $setup:expr,
199        event_types: $event_types:expr,
200        on_event: $on_event:expr,
201    ) => {{
202        let __bootstrap = $bootstrap;
203        let __db = Arc::clone(&$db);
204        $crate::run_module! {
205            socket_path: __bootstrap.socket_path.clone(),
206            module_id: &__bootstrap.module_id,
207            module_name: $module_name,
208            version: env!("CARGO_PKG_VERSION"),
209            module_type: $module_type,
210            cli_type: $cli_type,
211            db: __db,
212            setup: $setup,
213            event_types: $event_types,
214            on_event: $on_event,
215            data_dir: __bootstrap.data_dir.as_path(),
216        }
217    }};
218    (
219        socket_path: $socket_path:expr,
220        module_id: $module_id:expr,
221        module_name: $module_name:expr,
222        version: $version:expr,
223        module_type: $module_type:ty,
224        cli_type: $cli_type:ty,
225        db: $db:expr,
226        setup: $setup:expr,
227        event_types: $event_types:expr,
228        on_event: $on_event:expr,
229        data_dir: $data_dir:expr,
230    ) => {{
231        use blvm_node::module::ipc::protocol::{
232            InvocationMessage, InvocationResultMessage, InvocationResultPayload, InvocationType,
233        };
234        use std::sync::Arc;
235        use $crate::module::runner::{run_module_with_setup, InvocationContext};
236
237        let db = Arc::clone(&$db);
238
239        let dispatch = |invocation: InvocationMessage,
240                        ctx: InvocationContext,
241                        module: &$module_type,
242                        cli: &$cli_type| {
243            let (success, payload, error) = match &invocation.invocation_type {
244                InvocationType::Cli { subcommand, args } => {
245                    let args: Vec<String> = args.clone();
246                    match cli.dispatch_cli(&ctx, subcommand, &args) {
247                        Ok(stdout) => (
248                            true,
249                            Some(InvocationResultPayload::Cli {
250                                stdout,
251                                stderr: String::new(),
252                                exit_code: 0,
253                            }),
254                            None,
255                        ),
256                        Err(e) => (false, None, Some(e.to_string())),
257                    }
258                }
259                InvocationType::Rpc { method, params } => {
260                    let db_ref = ctx.db();
261                    match module.dispatch_rpc(method, params, db_ref) {
262                        Ok(v) => (true, Some(InvocationResultPayload::Rpc(v)), None),
263                        Err(e) => (false, None, Some(e.to_string())),
264                    }
265                }
266            };
267            InvocationResultMessage {
268                correlation_id: invocation.correlation_id,
269                success,
270                payload,
271                error,
272            }
273        };
274
275        let rpc_names = <$module_type>::rpc_method_names();
276        let cli_spec = <$cli_type>::cli_spec();
277
278        run_module_with_setup(
279            $socket_path,
280            $module_id,
281            $module_name,
282            $version,
283            cli_spec,
284            rpc_names.as_slice(),
285            $event_types,
286            dispatch,
287            $on_event,
288            $setup,
289            db,
290            $data_dir,
291        )
292        .await
293    }};
294    (
295        socket_path: $socket_path:expr,
296        module_id: $module_id:expr,
297        module_name: $module_name:expr,
298        version: $version:expr,
299        cli: $cli:expr,
300        cli_type: $cli_type:ty,
301        module_type: $module_type:ty,
302        module: $module:expr,
303        db: $db:expr,
304    ) => {{
305        use blvm_node::module::ipc::protocol::{
306            InvocationMessage, InvocationResultMessage, InvocationResultPayload, InvocationType,
307        };
308        use std::sync::Arc;
309        use $crate::module::runner::{run_module as run_module_fn, InvocationContext};
310
311        let cli = $cli;
312        let module = Arc::new($module);
313        let db = Arc::clone(&$db);
314
315        let dispatch = |invocation: InvocationMessage,
316                        ctx: InvocationContext,
317                        module: &Arc<$module_type>,
318                        cli: &$cli_type| {
319            let (success, payload, error) = match &invocation.invocation_type {
320                InvocationType::Cli { subcommand, args } => {
321                    let args: Vec<String> = args.clone();
322                    match cli.dispatch_cli(&ctx, subcommand, &args) {
323                        Ok(stdout) => (
324                            true,
325                            Some(InvocationResultPayload::Cli {
326                                stdout,
327                                stderr: String::new(),
328                                exit_code: 0,
329                            }),
330                            None,
331                        ),
332                        Err(e) => (false, None, Some(e.to_string())),
333                    }
334                }
335                InvocationType::Rpc { method, params } => {
336                    let db_ref = ctx.db();
337                    match module.dispatch_rpc(method, params, db_ref) {
338                        Ok(v) => (true, Some(InvocationResultPayload::Rpc(v)), None),
339                        Err(e) => (false, None, Some(e.to_string())),
340                    }
341                }
342            };
343            InvocationResultMessage {
344                correlation_id: invocation.correlation_id,
345                success,
346                payload,
347                error,
348            }
349        };
350
351        let rpc_names = <$module_type>::rpc_method_names();
352        let cli_spec = <$cli_type>::cli_spec();
353
354        run_module_fn(
355            $socket_path,
356            $module_id,
357            $module_name,
358            $version,
359            cli_spec,
360            rpc_names.as_slice(),
361            <$module_type>::event_types(),
362            dispatch,
363            |e, m: &Arc<$module_type>, ctx: &InvocationContext| {
364                let m = std::sync::Arc::clone(m);
365                let ctx = ctx.clone();
366                async move { m.dispatch_event(e, &ctx).await }
367            },
368            module,
369            cli,
370            db,
371        )
372        .await
373    }};
374    (
375        bootstrap: $bootstrap:expr,
376        module_name: $module_name:expr,
377        module: $module:expr,
378        module_type: $module_type:ty,
379        db: $db:expr,
380        on_connect: $on_connect:expr,
381        on_tick: $on_tick:expr,
382    ) => {{
383        let __bootstrap = $bootstrap;
384        let __module = $module;
385        $crate::run_module! {
386            socket_path: __bootstrap.socket_path.clone(),
387            module_id: &__bootstrap.module_id,
388            module_name: $module_name,
389            version: env!("CARGO_PKG_VERSION"),
390            cli: __module.clone(),
391            cli_type: $module_type,
392            module_type: $module_type,
393            module: __module,
394            db: $db,
395            on_connect: Some($on_connect),
396            on_tick: Some($on_tick),
397        }
398    }};
399    (
400        socket_path: $socket_path:expr,
401        module_id: $module_id:expr,
402        module_name: $module_name:expr,
403        version: $version:expr,
404        cli: $cli:expr,
405        cli_type: $cli_type:ty,
406        module_type: $module_type:ty,
407        module: $module:expr,
408        db: $db:expr,
409        on_connect: $on_connect:expr,
410        on_tick: $on_tick:expr,
411    ) => {{
412        use blvm_node::module::ipc::protocol::{
413            InvocationMessage, InvocationResultMessage, InvocationResultPayload, InvocationType,
414        };
415        use std::sync::Arc;
416        use $crate::module::runner::{run_module_with_tick, InvocationContext};
417
418        let cli = $cli;
419        let module = Arc::new($module);
420        let db = Arc::clone(&$db);
421
422        let dispatch = |invocation: InvocationMessage,
423                        ctx: InvocationContext,
424                        module: &Arc<$module_type>,
425                        cli: &$cli_type| {
426            let (success, payload, error) = match &invocation.invocation_type {
427                InvocationType::Cli { subcommand, args } => {
428                    let args: Vec<String> = args.clone();
429                    match cli.dispatch_cli(&ctx, subcommand, &args) {
430                        Ok(stdout) => (
431                            true,
432                            Some(InvocationResultPayload::Cli {
433                                stdout,
434                                stderr: String::new(),
435                                exit_code: 0,
436                            }),
437                            None,
438                        ),
439                        Err(e) => (false, None, Some(e.to_string())),
440                    }
441                }
442                InvocationType::Rpc { method, params } => {
443                    let db_ref = ctx.db();
444                    match module.dispatch_rpc(method, params, db_ref) {
445                        Ok(v) => (true, Some(InvocationResultPayload::Rpc(v)), None),
446                        Err(e) => (false, None, Some(e.to_string())),
447                    }
448                }
449            };
450            InvocationResultMessage {
451                correlation_id: invocation.correlation_id,
452                success,
453                payload,
454                error,
455            }
456        };
457
458        let rpc_names = <$module_type>::rpc_method_names();
459        let cli_spec = <$cli_type>::cli_spec();
460
461        run_module_with_tick(
462            $socket_path,
463            $module_id,
464            $module_name,
465            $version,
466            cli_spec,
467            rpc_names.as_slice(),
468            <$module_type>::event_types(),
469            dispatch,
470            |e, m: &Arc<$module_type>, ctx: &InvocationContext| {
471                let m = std::sync::Arc::clone(m);
472                let ctx = ctx.clone();
473                async move { m.dispatch_event(e, &ctx).await }
474            },
475            $on_connect,
476            $on_tick,
477            module,
478            cli,
479            db,
480        )
481        .await
482    }};
483}
484
485/// Register RPC methods with the node on connect.
486///
487/// Call this after connecting via `ModuleIntegration::connect`. Pass the node API and
488/// the method names (as string literals) that were registered with `#[rpc_method(name = "...")]`.
489///
490/// # Example
491/// ```ignore
492/// let integration = ModuleIntegration::connect(...).await?;
493/// let node_api = integration.node_api();
494/// register_rpc_methods!(node_api, "hello_greet", "hello_status").await?;
495/// ```
496#[macro_export]
497macro_rules! register_rpc_methods {
498    ($api:expr, $($method:expr),* $(,)?) => {
499        async {
500            let api = $api;
501            $(
502                api.register_rpc_endpoint($method.to_string(), String::new()).await?;
503            )*
504            Ok::<(), blvm_node::module::traits::ModuleError>(())
505        }
506    };
507}
508
509#[cfg(feature = "node")]
510pub mod ipc;
511#[cfg(feature = "node")]
512pub mod manifest;
513#[cfg(feature = "node")]
514pub mod prelude;
515#[cfg(feature = "node")]
516pub mod runner;
517#[cfg(feature = "node")]
518pub mod security;
519#[cfg(feature = "node")]
520pub mod traits;
521
522// Re-export main types for convenience (requires node)
523#[cfg(feature = "node")]
524pub use bootstrap::{ModuleBootstrap, ModuleConfig};
525#[cfg(feature = "node")]
526pub use database::{
527    open_module_db, run_migrations, run_migrations_down, run_migrations_with_down, Migration,
528    MigrationContext, MigrationDown, MigrationUp,
529};
530#[cfg(feature = "node")]
531pub use ipc::client::ModuleIpcClient;
532#[cfg(feature = "node")]
533pub use ipc::protocol::*;
534#[cfg(feature = "node")]
535pub use manifest::ModuleManifest;
536#[cfg(feature = "node")]
537pub use module_db::ModuleDb;
538#[cfg(feature = "node")]
539pub use runner::{
540    run_async, run_module, run_module_with_setup, run_module_with_tick, InvocationContext,
541};
542#[cfg(feature = "node")]
543pub use security::{Permission, PermissionSet};
544#[cfg(feature = "node")]
545pub use storage::{DatabaseStorageAdapter, ModuleStorage, ModuleStorageDatabaseBridge, ModuleTree};
546#[cfg(feature = "node")]
547pub use traits::*;