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>, #[serde(default)] pub searchable: Option, #[serde(default)] pub filterable: Option, #[serde(default)] pub sortable: Option, #[serde(default)] pub visible_when: 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, } /// 插件页面类型(tagged enum,TOML 中通过 type 字段区分) #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(tag = "type")] pub enum PluginPageType { #[serde(rename = "crud")] Crud { entity: String, label: String, #[serde(default)] icon: Option, #[serde(default)] enable_search: Option, #[serde(default)] enable_views: Option>, }, #[serde(rename = "tree")] Tree { entity: String, label: String, #[serde(default)] icon: Option, id_field: String, parent_field: String, label_field: String, }, #[serde(rename = "detail")] Detail { entity: String, label: String, sections: Vec, }, #[serde(rename = "tabs")] Tabs { label: String, #[serde(default)] icon: Option, tabs: Vec, }, } /// 插件页面区段(用于 detail 页面类型) #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(tag = "type")] pub enum PluginSection { #[serde(rename = "fields")] Fields { label: String, fields: Vec, }, #[serde(rename = "crud")] Crud { label: String, entity: String, #[serde(default)] filter_field: Option, #[serde(default)] enable_views: 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 ))); } } } // 验证页面类型配置 if let Some(ui) = &manifest.ui { validate_pages(&ui.pages)?; } Ok(manifest) } /// 递归验证页面配置 fn validate_pages(pages: &[PluginPageType]) -> PluginResult<()> { for page in pages { match page { PluginPageType::Crud { entity, .. } => { if entity.is_empty() { return Err(PluginError::InvalidManifest( "crud page 的 entity 不能为空".into(), )); } } PluginPageType::Tree { entity: _, label: _, icon: _, id_field, parent_field, label_field, } => { if id_field.is_empty() || parent_field.is_empty() || label_field.is_empty() { return Err(PluginError::InvalidManifest( "tree page 的 id_field/parent_field/label_field 不能为空".into(), )); } } PluginPageType::Detail { entity, sections, .. } => { if entity.is_empty() { return Err(PluginError::InvalidManifest( "detail page 的 entity 不能为空".into(), )); } if sections.is_empty() { return Err(PluginError::InvalidManifest( "detail page 的 sections 不能为空".into(), )); } } PluginPageType::Tabs { tabs, .. } => { if tabs.is_empty() { return Err(PluginError::InvalidManifest( "tabs page 的 tabs 不能为空".into(), )); } validate_pages(tabs)?; } } } Ok(()) } #[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]] type = "crud" entity = "product" label = "商品管理" icon = "ShoppingOutlined" [[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); // 验证新格式解析正确 match &ui.pages[0] { PluginPageType::Crud { entity, label, .. } => { assert_eq!(entity, "product"); assert_eq!(label, "商品管理"); } _ => panic!("Expected Crud page type"), } } #[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()); } #[test] fn parse_manifest_with_new_fields_and_page_types() { let toml = r#" [metadata] id = "test-plugin" name = "Test" version = "0.1.0" description = "Test" author = "Test" min_platform_version = "0.1.0" [[schema.entities]] name = "customer" display_name = "客户" [[schema.entities.fields]] name = "code" field_type = "string" required = true display_name = "编码" unique = true searchable = true filterable = true visible_when = "customer_type == 'enterprise'" [[ui.pages]] type = "tabs" label = "客户管理" icon = "team" [[ui.pages.tabs]] label = "客户列表" type = "crud" entity = "customer" enable_search = true enable_views = ["table", "timeline"] [[ui.pages]] type = "detail" entity = "customer" label = "客户详情" [[ui.pages.sections]] type = "fields" label = "基本信息" fields = ["code", "name"] "#; let manifest = parse_manifest(toml).expect("should parse"); let field = &manifest.schema.as_ref().unwrap().entities[0].fields[0]; assert_eq!(field.searchable, Some(true)); assert_eq!(field.filterable, Some(true)); assert_eq!( field.visible_when.as_deref(), Some("customer_type == 'enterprise'") ); // 验证页面类型解析 let ui = manifest.ui.as_ref().unwrap(); assert_eq!(ui.pages.len(), 2); // tabs 页面 match &ui.pages[0] { PluginPageType::Tabs { label, tabs, .. } => { assert_eq!(label, "客户管理"); assert_eq!(tabs.len(), 1); } _ => panic!("Expected Tabs page type"), } // detail 页面 match &ui.pages[1] { PluginPageType::Detail { entity, sections, .. } => { assert_eq!(entity, "customer"); assert_eq!(sections.len(), 1); } _ => panic!("Expected Detail page type"), } } #[test] fn reject_empty_entity_in_crud_page() { let toml = r#" [metadata] id = "test" name = "Test" version = "0.1.0" [ui] [[ui.pages]] type = "crud" entity = "" label = "测试" "#; let result = parse_manifest(toml); assert!(result.is_err()); } #[test] fn reject_empty_tabs_in_tabs_page() { let toml = r#" [metadata] id = "test" name = "Test" version = "0.1.0" [ui] [[ui.pages]] type = "tabs" label = "空标签页" "#; let result = parse_manifest(toml); assert!(result.is_err()); } }