Skip to main content

jasper_plugin_sdk/
lib.rs

1//! # jasper-plugin-sdk
2//!
3//! Jasper 后端插件(wasm,spec 0.2)的 Rust SDK:封装 ABI(spec §6),
4//! 作者只写业务函数/类型,用 [`register!`] 一行接入。
5//!
6//! ```ignore
7//! use jasper_plugin_sdk as sdk;
8//! use sdk::core::model::Note;
9//!
10//! fn before_save(mut note: Note) -> Result<Note, String> {
11//!     note.body = note.body.lines().map(|l| l.trim_end()).collect::<Vec<_>>().join("\n");
12//!     Ok(note)
13//! }
14//!
15//! sdk::register! { before_save: before_save }
16//! // 或:sdk::register! { storage: MyStorage }(impl sdk::storage::Storage)
17//! // 或两者都挂:sdk::register! { before_save: before_save, storage: MyStorage }
18//! ```
19//!
20//! 插件 crate 须为 `crate-type = ["cdylib"]`,目标 `wasm32-unknown-unknown`。
21
22pub mod host;
23pub mod rt;
24pub mod storage;
25
26pub use jasper_core as core;
27pub use rt::PluginError;
28// 宏与作者代码共用同一版本 serde_json
29pub use serde_json;
30
31// wasmi 沙箱无熵源:给 getrandom 注册报错实现(仅为编译通过)。
32// 插件不该自造 Joplin id——id 由宿主生成;误调 core::serialize::new_id 会 panic 该次调用(宿主可容错)。
33#[cfg(target_arch = "wasm32")]
34mod rand_shim {
35    fn no_entropy(_buf: &mut [u8]) -> Result<(), getrandom::Error> {
36        Err(getrandom::Error::UNSUPPORTED)
37    }
38    getrandom::register_custom_getrandom!(no_entropy);
39}
40
41/// 生成 `plugin_alloc` / `plugin_free` / `plugin_dispatch` 三个 ABI 导出(spec §6.1/§6.2)。
42/// 可挂载(任意顺序、可组合):
43/// - `before_save`:`fn(Note) -> Result<Note, String>`
44/// - `storage`:impl [`storage::Storage`] 的类型
45/// - `command`:`fn(&str /* 命令 id */, Value /* args */) -> Result<Value, PluginError>`
46#[macro_export]
47macro_rules! register {
48    ( $($rest:tt)* ) => {
49        $crate::__register_accum! { hook = (), storage = (), command = (); $($rest)* }
50    };
51}
52
53// 累积器:按键收集三个可选槽位,与书写顺序无关。
54#[doc(hidden)]
55#[macro_export]
56macro_rules! __register_accum {
57    ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?); before_save: $f:path $(, $($rest:tt)*)? ) => {
58        $crate::__register_accum! { hook = ($f), storage = ($($s)?), command = ($($c)?); $($($rest)*)? }
59    };
60    ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?); storage: $t:ty $(, $($rest:tt)*)? ) => {
61        $crate::__register_accum! { hook = ($($h)?), storage = ($t), command = ($($c)?); $($($rest)*)? }
62    };
63    ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?); command: $f:path $(, $($rest:tt)*)? ) => {
64        $crate::__register_accum! { hook = ($($h)?), storage = ($($s)?), command = ($f); $($($rest)*)? }
65    };
66    ( hook = ($($h:path)?), storage = ($($s:ty)?), command = ($($c:path)?); ) => {
67        $crate::__register_dispatch! { hook = ($($h)?), storage = ($($s)?), command = ($($c)?) }
68    };
69}
70
71#[doc(hidden)]
72#[macro_export]
73macro_rules! __register_dispatch {
74    ( hook = ($($hook:path)?), storage = ($($st:ty)?), command = ($($cmd:path)?) ) => {
75        // 业务路由:storage.* 方法族优先,其余按 method 匹配。native 下仅供测试。
76        #[allow(dead_code)]
77        fn __jasper_dispatch(
78            method: &str,
79            params: $crate::serde_json::Value,
80        ) -> ::std::result::Result<$crate::serde_json::Value, $crate::PluginError> {
81            $(
82                if let Some(r) = $crate::storage::dispatch_storage::<$st>(method, &params) {
83                    return r;
84                }
85            )?
86            match method {
87                "metadata" => Ok($crate::serde_json::json!({ "ok": true })),
88                $(
89                    "hook.before_save" => {
90                        let note: $crate::core::model::Note = $crate::serde_json::from_value(
91                            params.get("note").cloned().unwrap_or($crate::serde_json::Value::Null),
92                        )
93                        .map_err(|e| $crate::PluginError::invalid(format!("note 解析失败: {e}")))?;
94                        let hook: fn(
95                            $crate::core::model::Note,
96                        ) -> ::std::result::Result<$crate::core::model::Note, String> = $hook;
97                        let out = hook(note).map_err($crate::PluginError::internal)?;
98                        Ok($crate::serde_json::json!({ "note": out }))
99                    }
100                )?
101                $(
102                    "command" => {
103                        let id = params
104                            .get("id")
105                            .and_then(|v| v.as_str())
106                            .unwrap_or("")
107                            .to_string();
108                        let args = params.get("args").cloned().unwrap_or($crate::serde_json::Value::Null);
109                        let run: fn(
110                            &str,
111                            $crate::serde_json::Value,
112                        ) -> ::std::result::Result<$crate::serde_json::Value, $crate::PluginError> = $cmd;
113                        run(&id, args)
114                    }
115                )?
116                other => Err($crate::PluginError::unsupported(format!("未知方法: {other}"))),
117            }
118        }
119
120        #[cfg(target_arch = "wasm32")]
121        mod __jasper_plugin_abi {
122            #[no_mangle]
123            pub extern "C" fn plugin_alloc(size: u32) -> u32 {
124                $crate::rt::alloc(size as usize) as u32
125            }
126            #[no_mangle]
127            pub extern "C" fn plugin_free(ptr: u32, size: u32) {
128                $crate::rt::free(ptr as usize, size as usize)
129            }
130            #[no_mangle]
131            pub extern "C" fn plugin_dispatch(ptr: u32, len: u32) -> u64 {
132                $crate::rt::dispatch(ptr as usize, len as usize, super::__jasper_dispatch)
133            }
134        }
135    };
136}
137
138#[cfg(test)]
139mod tests {
140    use crate::core::model::{MarkupLanguage, Note};
141    use serde_json::json;
142
143    fn trim_hook(mut note: Note) -> Result<Note, String> {
144        note.body = note.body.lines().map(|l| l.trim_end()).collect::<Vec<_>>().join("\n");
145        Ok(note)
146    }
147
148    crate::register! { before_save: trim_hook }
149
150    fn sample_note() -> Note {
151        Note {
152            id: "a".repeat(32),
153            parent_id: String::new(),
154            title: "t".into(),
155            body: "x  \ny\t".into(),
156            created_time: 0,
157            updated_time: 0,
158            markup_language: MarkupLanguage::Markdown,
159            is_todo: false,
160            todo_completed: false,
161            is_conflict: false,
162            source_url: String::new(),
163            order: 0,
164        }
165    }
166
167    #[test]
168    fn dispatch_metadata_and_hook() {
169        let r = __jasper_dispatch("metadata", json!(null)).unwrap();
170        assert_eq!(r, json!({ "ok": true }));
171
172        let r = __jasper_dispatch("hook.before_save", json!({ "note": sample_note() })).unwrap();
173        assert_eq!(r["note"]["body"], "x\ny");
174
175        let e = __jasper_dispatch("nope", json!(null)).unwrap_err();
176        assert_eq!(e.code, "unsupported");
177    }
178}