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                InvocationType::ModuleApi { .. } => (
267                    false,
268                    None,
269                    Some("ModuleApi is not supported by run_module!; use run_module_with_setup_and_api".to_string()),
270                ),
271                _ => (
272                    false,
273                    None,
274                    Some("unsupported invocation".to_string()),
275                ),
276            };
277            InvocationResultMessage {
278                correlation_id: invocation.correlation_id,
279                success,
280                payload,
281                error,
282            }
283        };
284
285        let rpc_names = <$module_type>::rpc_method_names();
286        let cli_spec = <$cli_type>::cli_spec();
287
288        run_module_with_setup(
289            $socket_path,
290            $module_id,
291            $module_name,
292            $version,
293            cli_spec,
294            rpc_names.as_slice(),
295            $event_types,
296            dispatch,
297            $on_event,
298            $setup,
299            db,
300            $data_dir,
301        )
302        .await
303    }};
304    (
305        socket_path: $socket_path:expr,
306        module_id: $module_id:expr,
307        module_name: $module_name:expr,
308        version: $version:expr,
309        cli: $cli:expr,
310        cli_type: $cli_type:ty,
311        module_type: $module_type:ty,
312        module: $module:expr,
313        db: $db:expr,
314    ) => {{
315        use blvm_node::module::ipc::protocol::{
316            InvocationMessage, InvocationResultMessage, InvocationResultPayload, InvocationType,
317        };
318        use std::sync::Arc;
319        use $crate::module::runner::{run_module as run_module_fn, InvocationContext};
320
321        let cli = $cli;
322        let module = Arc::new($module);
323        let db = Arc::clone(&$db);
324
325        let dispatch = |invocation: InvocationMessage,
326                        ctx: InvocationContext,
327                        module: &Arc<$module_type>,
328                        cli: &$cli_type| {
329            let (success, payload, error) = match &invocation.invocation_type {
330                InvocationType::Cli { subcommand, args } => {
331                    let args: Vec<String> = args.clone();
332                    match cli.dispatch_cli(&ctx, subcommand, &args) {
333                        Ok(stdout) => (
334                            true,
335                            Some(InvocationResultPayload::Cli {
336                                stdout,
337                                stderr: String::new(),
338                                exit_code: 0,
339                            }),
340                            None,
341                        ),
342                        Err(e) => (false, None, Some(e.to_string())),
343                    }
344                }
345                InvocationType::Rpc { method, params } => {
346                    let db_ref = ctx.db();
347                    match module.dispatch_rpc(method, params, db_ref) {
348                        Ok(v) => (true, Some(InvocationResultPayload::Rpc(v)), None),
349                        Err(e) => (false, None, Some(e.to_string())),
350                    }
351                }
352                InvocationType::ModuleApi { .. } => (
353                    false,
354                    None,
355                    Some("ModuleApi is not supported by run_module!; use run_module_with_setup_and_api".to_string()),
356                ),
357                _ => (
358                    false,
359                    None,
360                    Some("unsupported invocation".to_string()),
361                ),
362            };
363            InvocationResultMessage {
364                correlation_id: invocation.correlation_id,
365                success,
366                payload,
367                error,
368            }
369        };
370
371        let rpc_names = <$module_type>::rpc_method_names();
372        let cli_spec = <$cli_type>::cli_spec();
373
374        run_module_fn(
375            $socket_path,
376            $module_id,
377            $module_name,
378            $version,
379            cli_spec,
380            rpc_names.as_slice(),
381            <$module_type>::event_types(),
382            dispatch,
383            |e, m: &Arc<$module_type>, ctx: &InvocationContext| {
384                let m = std::sync::Arc::clone(m);
385                let ctx = ctx.clone();
386                async move { m.dispatch_event(e, &ctx).await }
387            },
388            module,
389            cli,
390            db,
391        )
392        .await
393    }};
394    (
395        bootstrap: $bootstrap:expr,
396        module_name: $module_name:expr,
397        module: $module:expr,
398        module_type: $module_type:ty,
399        db: $db:expr,
400        on_connect: $on_connect:expr,
401        on_tick: $on_tick:expr,
402    ) => {{
403        let __bootstrap = $bootstrap;
404        let __module = $module;
405        $crate::run_module! {
406            socket_path: __bootstrap.socket_path.clone(),
407            module_id: &__bootstrap.module_id,
408            module_name: $module_name,
409            version: env!("CARGO_PKG_VERSION"),
410            cli: __module.clone(),
411            cli_type: $module_type,
412            module_type: $module_type,
413            module: __module,
414            db: $db,
415            on_connect: Some($on_connect),
416            on_tick: Some($on_tick),
417        }
418    }};
419    (
420        socket_path: $socket_path:expr,
421        module_id: $module_id:expr,
422        module_name: $module_name:expr,
423        version: $version:expr,
424        cli: $cli:expr,
425        cli_type: $cli_type:ty,
426        module_type: $module_type:ty,
427        module: $module:expr,
428        db: $db:expr,
429        on_connect: $on_connect:expr,
430        on_tick: $on_tick:expr,
431    ) => {{
432        use blvm_node::module::ipc::protocol::{
433            InvocationMessage, InvocationResultMessage, InvocationResultPayload, InvocationType,
434        };
435        use std::sync::Arc;
436        use $crate::module::runner::{run_module_with_tick, InvocationContext};
437
438        let cli = $cli;
439        let module = Arc::new($module);
440        let db = Arc::clone(&$db);
441
442        let dispatch = |invocation: InvocationMessage,
443                        ctx: InvocationContext,
444                        module: &Arc<$module_type>,
445                        cli: &$cli_type| {
446            let (success, payload, error) = match &invocation.invocation_type {
447                InvocationType::Cli { subcommand, args } => {
448                    let args: Vec<String> = args.clone();
449                    match cli.dispatch_cli(&ctx, subcommand, &args) {
450                        Ok(stdout) => (
451                            true,
452                            Some(InvocationResultPayload::Cli {
453                                stdout,
454                                stderr: String::new(),
455                                exit_code: 0,
456                            }),
457                            None,
458                        ),
459                        Err(e) => (false, None, Some(e.to_string())),
460                    }
461                }
462                InvocationType::Rpc { method, params } => {
463                    let db_ref = ctx.db();
464                    match module.dispatch_rpc(method, params, db_ref) {
465                        Ok(v) => (true, Some(InvocationResultPayload::Rpc(v)), None),
466                        Err(e) => (false, None, Some(e.to_string())),
467                    }
468                }
469                InvocationType::ModuleApi { .. } => (
470                    false,
471                    None,
472                    Some("ModuleApi is not supported by run_module!; use run_module_with_setup_and_api".to_string()),
473                ),
474                _ => (
475                    false,
476                    None,
477                    Some("unsupported invocation".to_string()),
478                ),
479            };
480            InvocationResultMessage {
481                correlation_id: invocation.correlation_id,
482                success,
483                payload,
484                error,
485            }
486        };
487
488        let rpc_names = <$module_type>::rpc_method_names();
489        let cli_spec = <$cli_type>::cli_spec();
490
491        run_module_with_tick(
492            $socket_path,
493            $module_id,
494            $module_name,
495            $version,
496            cli_spec,
497            rpc_names.as_slice(),
498            <$module_type>::event_types(),
499            dispatch,
500            |e, m: &Arc<$module_type>, ctx: &InvocationContext| {
501                let m = std::sync::Arc::clone(m);
502                let ctx = ctx.clone();
503                async move { m.dispatch_event(e, &ctx).await }
504            },
505            $on_connect,
506            $on_tick,
507            module,
508            cli,
509            db,
510        )
511        .await
512    }};
513}
514
515/// Register RPC methods with the node on connect.
516///
517/// Call this after connecting via `ModuleIntegration::connect`. Pass the node API and
518/// the method names (as string literals) that were registered with `#[rpc_method(name = "...")]`.
519///
520/// # Example
521/// ```ignore
522/// let integration = ModuleIntegration::connect(...).await?;
523/// let node_api = integration.node_api();
524/// register_rpc_methods!(node_api, "hello_greet", "hello_status").await?;
525/// ```
526#[macro_export]
527macro_rules! register_rpc_methods {
528    ($api:expr, $($method:expr),* $(,)?) => {
529        async {
530            let api = $api;
531            $(
532                api.register_rpc_endpoint($method.to_string(), String::new()).await?;
533            )*
534            Ok::<(), blvm_node::module::traits::ModuleError>(())
535        }
536    };
537}
538
539#[cfg(feature = "node")]
540pub mod ipc;
541#[cfg(feature = "node")]
542pub mod manifest;
543#[cfg(feature = "node")]
544pub mod prelude;
545#[cfg(feature = "node")]
546pub mod runner;
547#[cfg(feature = "node")]
548pub mod security;
549#[cfg(feature = "node")]
550pub mod timers;
551#[cfg(feature = "node")]
552pub mod traits;
553
554// Re-export main types for convenience (requires node)
555#[cfg(feature = "node")]
556pub use bootstrap::{ModuleBootstrap, ModuleConfig};
557#[cfg(feature = "node")]
558pub use database::{
559    Migration, MigrationContext, MigrationDown, MigrationUp, open_module_db, run_migrations,
560    run_migrations_down, run_migrations_with_down,
561};
562#[cfg(feature = "node")]
563pub use ipc::client::ModuleIpcClient;
564#[cfg(feature = "node")]
565pub use ipc::protocol::*;
566#[cfg(feature = "node")]
567pub use manifest::ModuleManifest;
568#[cfg(feature = "node")]
569pub use module_db::ModuleDb;
570#[cfg(feature = "node")]
571pub use runner::{
572    InvocationContext, run_async, run_module, run_module_with_setup, run_module_with_setup_and_api,
573    run_module_with_tick,
574};
575#[cfg(feature = "node")]
576pub use security::{Permission, PermissionSet};
577#[cfg(feature = "node")]
578pub use storage::{DatabaseStorageAdapter, ModuleStorage, ModuleStorageDatabaseBridge, ModuleTree};
579#[cfg(feature = "node")]
580pub use timers::{Exec, TimerBuilder, TimerHandle, TimerRegistry, sdk_tick_spec};
581#[cfg(feature = "node")]
582pub use traits::*;