use crate::rt::PluginError;
use base64::Engine as _;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ItemStat {
pub name: String,
pub updated_time: i64,
}
pub trait Storage: Sized {
fn from_config(config: &Value) -> Result<Self, PluginError>;
fn list_items(&self) -> Result<Vec<ItemStat>, PluginError>;
fn get_item(&self, name: &str) -> Result<String, PluginError>;
fn put_item(&self, name: &str, content: &str) -> Result<(), PluginError>;
fn delete_item(&self, name: &str) -> Result<(), PluginError>;
fn get_resource(&self, resource_id: &str) -> Result<Vec<u8>, PluginError>;
fn put_resource(&self, resource_id: &str, data: &[u8]) -> Result<(), PluginError>;
fn delete_resource(&self, resource_id: &str) -> Result<(), PluginError>;
fn init_new(&self) -> Result<(), PluginError>;
}
fn str_param<'a>(params: &'a Value, key: &str) -> Result<&'a str, PluginError> {
params
.get(key)
.and_then(Value::as_str)
.ok_or_else(|| PluginError::invalid(format!("缺参数 {key}")))
}
pub fn dispatch_storage<S: Storage>(method: &str, params: &Value) -> Option<Result<Value, PluginError>> {
let op = method.strip_prefix("storage.")?;
Some(run::<S>(op, params))
}
fn run<S: Storage>(op: &str, params: &Value) -> Result<Value, PluginError> {
let b64 = base64::engine::general_purpose::STANDARD;
let config = params.get("config").cloned().unwrap_or(Value::Null);
let s = S::from_config(&config)?;
match op {
"list_items" => {
let items = s.list_items()?;
Ok(json!({ "items": items }))
}
"get_item" => {
let content = s.get_item(str_param(params, "name")?)?;
Ok(json!({ "content": content }))
}
"put_item" => {
s.put_item(str_param(params, "name")?, str_param(params, "content")?)?;
Ok(json!({}))
}
"delete_item" => {
s.delete_item(str_param(params, "name")?)?;
Ok(json!({}))
}
"get_resource" => {
let data = s.get_resource(str_param(params, "resource_id")?)?;
Ok(json!({ "data_b64": b64.encode(data) }))
}
"put_resource" => {
let data = b64
.decode(str_param(params, "data_b64")?)
.map_err(|e| PluginError::invalid(format!("data_b64 解码失败: {e}")))?;
s.put_resource(str_param(params, "resource_id")?, &data)?;
Ok(json!({}))
}
"delete_resource" => {
s.delete_resource(str_param(params, "resource_id")?)?;
Ok(json!({}))
}
"init_new" => {
s.init_new()?;
Ok(json!({}))
}
other => Err(PluginError::unsupported(format!("未知 storage 方法: {other}"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct Mem;
thread_local! {
static LAST: RefCell<Option<(String, Vec<u8>)>> = const { RefCell::new(None) };
}
impl Storage for Mem {
fn from_config(config: &Value) -> Result<Self, PluginError> {
if config.get("fail").is_some() {
return Err(PluginError::invalid("bad config"));
}
Ok(Mem)
}
fn list_items(&self) -> Result<Vec<ItemStat>, PluginError> {
Ok(vec![ItemStat { name: "a".repeat(32) + ".md", updated_time: 42 }])
}
fn get_item(&self, name: &str) -> Result<String, PluginError> {
Ok(format!("content of {name}"))
}
fn put_item(&self, _: &str, _: &str) -> Result<(), PluginError> {
Ok(())
}
fn delete_item(&self, _: &str) -> Result<(), PluginError> {
Ok(())
}
fn get_resource(&self, _: &str) -> Result<Vec<u8>, PluginError> {
Ok(vec![0xde, 0xad, 0xbe, 0xef])
}
fn put_resource(&self, id: &str, data: &[u8]) -> Result<(), PluginError> {
LAST.with(|l| *l.borrow_mut() = Some((id.to_string(), data.to_vec())));
Ok(())
}
fn delete_resource(&self, _: &str) -> Result<(), PluginError> {
Ok(())
}
fn init_new(&self) -> Result<(), PluginError> {
Ok(())
}
}
#[test]
fn routes_and_encodes() {
let r = dispatch_storage::<Mem>("storage.list_items", &json!({"config": {}}))
.unwrap()
.unwrap();
assert_eq!(r["items"][0]["updated_time"], 42);
let r = dispatch_storage::<Mem>("storage.get_resource", &json!({"config": {}, "resource_id": "x"}))
.unwrap()
.unwrap();
assert_eq!(r["data_b64"], "3q2+7w==");
dispatch_storage::<Mem>(
"storage.put_resource",
&json!({"config": {}, "resource_id": "y", "data_b64": "3q2+7w=="}),
)
.unwrap()
.unwrap();
LAST.with(|l| {
let (id, data) = l.borrow().clone().unwrap();
assert_eq!(id, "y");
assert_eq!(data, vec![0xde, 0xad, 0xbe, 0xef]);
});
}
#[test]
fn non_storage_method_passes_through() {
assert!(dispatch_storage::<Mem>("command", &json!({})).is_none());
}
#[test]
fn bad_config_and_bad_b64_are_invalid() {
let e = dispatch_storage::<Mem>("storage.list_items", &json!({"config": {"fail": 1}}))
.unwrap()
.unwrap_err();
assert_eq!(e.code, "invalid");
let e = dispatch_storage::<Mem>(
"storage.put_resource",
&json!({"config": {}, "resource_id": "y", "data_b64": "!!!"}),
)
.unwrap()
.unwrap_err();
assert_eq!(e.code, "invalid");
}
}