use serde::{Deserialize, Serialize}; use crate::error::{PluginError, PluginResult}; /// 插件清单 — 从 TOML 文件解析 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginManifest { pub metadata: PluginMetadata, pub schema: Option, pub events: Option, pub ui: Option, pub permissions: Option>, } /// 插件元数据 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginMetadata { pub id: String, pub name: String, pub version: String, #[serde(default)] pub description: String, #[serde(default)] pub author: String, #[serde(default)] pub min_platform_version: Option, #[serde(default)] pub dependencies: Vec, } /// 插件 Schema — 定义动态实体 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginSchema { pub entities: Vec, } /// 插件实体定义 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginEntity { pub name: String, pub display_name: String, #[serde(default)] pub fields: Vec, #[serde(default)] pub indexes: Vec, } /// 插件字段定义 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginField { pub name: String, pub field_type: PluginFieldType, #[serde(default)] pub required: bool, #[serde(default)] pub unique: bool, pub default: Option, pub display_name: Option, pub ui_widget: Option, pub options: Option>, } /// 字段类型 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PluginFieldType { String, Integer, Float, Boolean, Date, DateTime, Json, Uuid, Decimal, } /// 索引定义 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginIndex { pub name: String, pub fields: Vec, #[serde(default)] pub unique: bool, } /// 事件订阅配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginEvents { pub subscribe: Vec, } /// UI 页面配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginUi { pub pages: Vec, } /// 插件页面定义 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginPage { pub route: String, pub entity: String, pub display_name: String, #[serde(default)] pub icon: String, #[serde(default)] pub menu_group: Option, } /// 权限定义 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginPermission { pub code: String, pub name: String, #[serde(default)] pub description: String, } /// 从 TOML 字符串解析插件清单 pub fn parse_manifest(toml_str: &str) -> PluginResult { let manifest: PluginManifest = toml::from_str(toml_str).map_err(|e| PluginError::InvalidManifest(e.to_string()))?; // 验证必填字段 if manifest.metadata.id.is_empty() { return Err(PluginError::InvalidManifest("metadata.id 不能为空".to_string())); } if manifest.metadata.name.is_empty() { return Err(PluginError::InvalidManifest( "metadata.name 不能为空".to_string(), )); } // 验证实体名称 if let Some(schema) = &manifest.schema { for entity in &schema.entities { if entity.name.is_empty() { return Err(PluginError::InvalidManifest( "entity.name 不能为空".to_string(), )); } // 验证实体名称只包含合法字符 if !entity .name .chars() .all(|c| c.is_ascii_alphanumeric() || c == '_') { return Err(PluginError::InvalidManifest(format!( "entity.name '{}' 只能包含字母、数字和下划线", entity.name ))); } } } Ok(manifest) } #[cfg(test)] mod tests { use super::*; #[test] fn parse_minimal_manifest() { let toml = r#" [metadata] id = "test-plugin" name = "测试插件" version = "0.1.0" "#; let manifest = parse_manifest(toml).unwrap(); assert_eq!(manifest.metadata.id, "test-plugin"); assert_eq!(manifest.metadata.name, "测试插件"); assert!(manifest.schema.is_none()); } #[test] fn parse_full_manifest() { let toml = r#" [metadata] id = "inventory" name = "进销存" version = "1.0.0" description = "简单进销存管理" author = "ERP Team" [schema] [[schema.entities]] name = "product" display_name = "商品" [[schema.entities.fields]] name = "sku" field_type = "string" required = true unique = true display_name = "SKU 编码" [[schema.entities.fields]] name = "price" field_type = "decimal" required = true display_name = "价格" [events] subscribe = ["workflow.task.completed", "order.*"] [ui] [[ui.pages]] route = "/products" entity = "product" display_name = "商品管理" icon = "ShoppingOutlined" menu_group = "进销存" [[permissions]] code = "product.list" name = "查看商品" description = "查看商品列表" "#; let manifest = parse_manifest(toml).unwrap(); assert_eq!(manifest.metadata.id, "inventory"); let schema = manifest.schema.unwrap(); assert_eq!(schema.entities.len(), 1); assert_eq!(schema.entities[0].name, "product"); assert_eq!(schema.entities[0].fields.len(), 2); let events = manifest.events.unwrap(); assert_eq!(events.subscribe.len(), 2); let ui = manifest.ui.unwrap(); assert_eq!(ui.pages.len(), 1); } #[test] fn reject_empty_id() { let toml = r#" [metadata] id = "" name = "测试" version = "0.1.0" "#; let result = parse_manifest(toml); assert!(result.is_err()); } #[test] fn reject_invalid_entity_name() { let toml = r#" [metadata] id = "test" name = "测试" version = "0.1.0" [schema] [[schema.entities]] name = "my-table" display_name = "表格" "#; let result = parse_manifest(toml); assert!(result.is_err()); } }