Skip to main content

jasper_plugin_sdk/
storage.rs

1//! 存储 provider(spec §3.9 / §6.5 `storage.*`,0.2 新增)。
2//!
3//! 插件实现 [`Storage`] trait,经 `register! { storage: MyType }` 接入;
4//! 宿主在每次调用里传入该数据源的 config——插件应视自己为无状态(每次调用新实例)。
5//! 本版一个 `register!` 只挂一个 Storage 类型:即使 manifest 声明多个
6//! `[[contributes.storage]]`,也由同一类型按 config 自行区分(params.storage 不参与路由)。
7
8use crate::rt::PluginError;
9use base64::Engine as _;
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12
13/// 条目文件元信息(对齐宿主 `storage::ItemStat`)。
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ItemStat {
16    /// 文件名,形如 `<32hex>.md`
17    pub name: String,
18    /// mtime(Unix 毫秒)。尽力返回真实值;0 合法但宿主失去增量缓存。
19    pub updated_time: i64,
20}
21
22/// 存储后端 trait,8 方法镜像宿主 `StorageBackend`(语义约定见 spec §6.5):
23/// `delete_*` 幂等(不存在视为成功);`init_new` 建根 + `.resource/` + 默认 info.json。
24pub trait Storage: Sized {
25    /// 从数据源配置(与 manifest `config_schema` 对齐的对象)构造。
26    fn from_config(config: &Value) -> Result<Self, PluginError>;
27
28    fn list_items(&self) -> Result<Vec<ItemStat>, PluginError>;
29    fn get_item(&self, name: &str) -> Result<String, PluginError>;
30    fn put_item(&self, name: &str, content: &str) -> Result<(), PluginError>;
31    fn delete_item(&self, name: &str) -> Result<(), PluginError>;
32    fn get_resource(&self, resource_id: &str) -> Result<Vec<u8>, PluginError>;
33    fn put_resource(&self, resource_id: &str, data: &[u8]) -> Result<(), PluginError>;
34    fn delete_resource(&self, resource_id: &str) -> Result<(), PluginError>;
35    fn init_new(&self) -> Result<(), PluginError>;
36}
37
38fn str_param<'a>(params: &'a Value, key: &str) -> Result<&'a str, PluginError> {
39    params
40        .get(key)
41        .and_then(Value::as_str)
42        .ok_or_else(|| PluginError::invalid(format!("缺参数 {key}")))
43}
44
45/// `storage.*` 方法路由(`register!` 生成的 dispatch 调用)。
46/// 非 storage. 前缀返回 None,让调用方继续尝试其它方法族。
47pub fn dispatch_storage<S: Storage>(method: &str, params: &Value) -> Option<Result<Value, PluginError>> {
48    let op = method.strip_prefix("storage.")?;
49    Some(run::<S>(op, params))
50}
51
52fn run<S: Storage>(op: &str, params: &Value) -> Result<Value, PluginError> {
53    let b64 = base64::engine::general_purpose::STANDARD;
54    let config = params.get("config").cloned().unwrap_or(Value::Null);
55    let s = S::from_config(&config)?;
56    match op {
57        "list_items" => {
58            let items = s.list_items()?;
59            Ok(json!({ "items": items }))
60        }
61        "get_item" => {
62            let content = s.get_item(str_param(params, "name")?)?;
63            Ok(json!({ "content": content }))
64        }
65        "put_item" => {
66            s.put_item(str_param(params, "name")?, str_param(params, "content")?)?;
67            Ok(json!({}))
68        }
69        "delete_item" => {
70            s.delete_item(str_param(params, "name")?)?;
71            Ok(json!({}))
72        }
73        "get_resource" => {
74            let data = s.get_resource(str_param(params, "resource_id")?)?;
75            Ok(json!({ "data_b64": b64.encode(data) }))
76        }
77        "put_resource" => {
78            let data = b64
79                .decode(str_param(params, "data_b64")?)
80                .map_err(|e| PluginError::invalid(format!("data_b64 解码失败: {e}")))?;
81            s.put_resource(str_param(params, "resource_id")?, &data)?;
82            Ok(json!({}))
83        }
84        "delete_resource" => {
85            s.delete_resource(str_param(params, "resource_id")?)?;
86            Ok(json!({}))
87        }
88        "init_new" => {
89            s.init_new()?;
90            Ok(json!({}))
91        }
92        other => Err(PluginError::unsupported(format!("未知 storage 方法: {other}"))),
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use std::cell::RefCell;
100
101    // 纯内存假存储:验证路由与 base64 编解码(native 可跑,不涉 host_call)
102    struct Mem;
103    thread_local! {
104        static LAST: RefCell<Option<(String, Vec<u8>)>> = const { RefCell::new(None) };
105    }
106
107    impl Storage for Mem {
108        fn from_config(config: &Value) -> Result<Self, PluginError> {
109            if config.get("fail").is_some() {
110                return Err(PluginError::invalid("bad config"));
111            }
112            Ok(Mem)
113        }
114        fn list_items(&self) -> Result<Vec<ItemStat>, PluginError> {
115            Ok(vec![ItemStat { name: "a".repeat(32) + ".md", updated_time: 42 }])
116        }
117        fn get_item(&self, name: &str) -> Result<String, PluginError> {
118            Ok(format!("content of {name}"))
119        }
120        fn put_item(&self, _: &str, _: &str) -> Result<(), PluginError> {
121            Ok(())
122        }
123        fn delete_item(&self, _: &str) -> Result<(), PluginError> {
124            Ok(())
125        }
126        fn get_resource(&self, _: &str) -> Result<Vec<u8>, PluginError> {
127            Ok(vec![0xde, 0xad, 0xbe, 0xef])
128        }
129        fn put_resource(&self, id: &str, data: &[u8]) -> Result<(), PluginError> {
130            LAST.with(|l| *l.borrow_mut() = Some((id.to_string(), data.to_vec())));
131            Ok(())
132        }
133        fn delete_resource(&self, _: &str) -> Result<(), PluginError> {
134            Ok(())
135        }
136        fn init_new(&self) -> Result<(), PluginError> {
137            Ok(())
138        }
139    }
140
141    #[test]
142    fn routes_and_encodes() {
143        let r = dispatch_storage::<Mem>("storage.list_items", &json!({"config": {}}))
144            .unwrap()
145            .unwrap();
146        assert_eq!(r["items"][0]["updated_time"], 42);
147
148        let r = dispatch_storage::<Mem>("storage.get_resource", &json!({"config": {}, "resource_id": "x"}))
149            .unwrap()
150            .unwrap();
151        assert_eq!(r["data_b64"], "3q2+7w=="); // base64(de ad be ef)
152
153        dispatch_storage::<Mem>(
154            "storage.put_resource",
155            &json!({"config": {}, "resource_id": "y", "data_b64": "3q2+7w=="}),
156        )
157        .unwrap()
158        .unwrap();
159        LAST.with(|l| {
160            let (id, data) = l.borrow().clone().unwrap();
161            assert_eq!(id, "y");
162            assert_eq!(data, vec![0xde, 0xad, 0xbe, 0xef]);
163        });
164    }
165
166    #[test]
167    fn non_storage_method_passes_through() {
168        assert!(dispatch_storage::<Mem>("command", &json!({})).is_none());
169    }
170
171    #[test]
172    fn bad_config_and_bad_b64_are_invalid() {
173        let e = dispatch_storage::<Mem>("storage.list_items", &json!({"config": {"fail": 1}}))
174            .unwrap()
175            .unwrap_err();
176        assert_eq!(e.code, "invalid");
177        let e = dispatch_storage::<Mem>(
178            "storage.put_resource",
179            &json!({"config": {}, "resource_id": "y", "data_b64": "!!!"}),
180        )
181        .unwrap()
182        .unwrap_err();
183        assert_eq!(e.code, "invalid");
184    }
185}