mockforge_ui/handlers/
plugin.rs

1//! Plugin management handlers
2
3use axum::{
4    extract::{Path, Query, State},
5    response::Json,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9
10use super::{AdminState, ApiResponse};
11
12/// Plugin information for API responses
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct PluginInfo {
15    pub id: String,
16    pub name: String,
17    pub version: String,
18    pub types: Vec<String>,
19    pub status: String,
20    pub healthy: bool,
21    pub description: String,
22    pub author: String,
23}
24
25/// Plugin statistics
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PluginStats {
28    pub total_plugins: usize,
29    pub discovered: usize,
30    pub loaded: usize,
31    pub failed: usize,
32    pub skipped: usize,
33    pub success_rate: f64,
34}
35
36/// Plugin health information
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct PluginHealthInfo {
39    pub id: String,
40    pub healthy: bool,
41    pub message: String,
42    pub last_check: String,
43}
44
45/// Plugin status response
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct PluginStatusData {
48    pub stats: PluginStats,
49    pub health: Vec<PluginHealthInfo>,
50    pub last_updated: Option<String>,
51}
52
53/// Query parameters for plugin listing
54#[derive(Debug, Deserialize)]
55pub struct PluginListQuery {
56    #[serde(rename = "type")]
57    pub plugin_type: Option<String>,
58    pub status: Option<String>,
59}
60
61/// Reload plugin request
62#[derive(Debug, Deserialize)]
63pub struct ReloadPluginRequest {
64    pub plugin_id: String,
65}
66
67/// Get list of plugins
68pub async fn get_plugins(
69    State(state): State<AdminState>,
70    Query(query): Query<PluginListQuery>,
71) -> Json<ApiResponse<serde_json::Value>> {
72    tracing::info!("get_plugins called");
73
74    let registry = state.plugin_registry.read().await;
75    let all_plugins = registry.list_plugins();
76
77    let mut plugins: Vec<PluginInfo> = Vec::new();
78
79    for plugin_id in all_plugins {
80        if let Some(plugin_instance) = registry.get_plugin(&plugin_id) {
81            // Convert core PluginInfo to UI PluginInfo
82            let ui_plugin = PluginInfo {
83                id: plugin_instance.manifest.info.id.as_str().to_string(),
84                name: plugin_instance.manifest.info.name.clone(),
85                version: plugin_instance.manifest.info.version.to_string(),
86                types: plugin_instance.manifest.capabilities.clone(),
87                status: plugin_instance.state.to_string(),
88                healthy: plugin_instance.is_healthy(),
89                description: plugin_instance.manifest.info.description.clone(),
90                author: plugin_instance.manifest.info.author.name.clone(),
91            };
92
93            // Apply filters
94            let matches_type =
95                query.plugin_type.as_ref().map(|t| ui_plugin.types.contains(t)).unwrap_or(true);
96
97            let matches_status =
98                query.status.as_ref().map(|s| ui_plugin.status == *s).unwrap_or(true);
99
100            if matches_type && matches_status {
101                plugins.push(ui_plugin);
102            }
103        }
104    }
105
106    Json(ApiResponse::success(json!({
107        "plugins": plugins,
108        "total": plugins.len()
109    })))
110}
111
112/// Get plugin status
113pub async fn get_plugin_status(
114    State(state): State<AdminState>,
115) -> Json<ApiResponse<PluginStatusData>> {
116    let registry = state.plugin_registry.read().await;
117    let registry_stats = registry.get_stats();
118
119    // Calculate stats from registry
120    let mut loaded = 0;
121    let mut failed = 0;
122
123    let mut health: Vec<PluginHealthInfo> = Vec::new();
124
125    for plugin_id in registry.list_plugins() {
126        if let Some(plugin_instance) = registry.get_plugin(&plugin_id) {
127            let is_loaded =
128                matches!(plugin_instance.state, mockforge_plugin_core::PluginState::Ready);
129
130            if is_loaded {
131                loaded += 1;
132            } else {
133                failed += 1;
134            }
135
136            let is_healthy = plugin_instance.is_healthy();
137
138            health.push(PluginHealthInfo {
139                id: plugin_id.as_str().to_string(),
140                healthy: is_healthy,
141                message: if is_healthy {
142                    "Plugin is healthy".to_string()
143                } else {
144                    "Plugin has issues".to_string()
145                },
146                last_check: plugin_instance.health.last_check.to_rfc3339(),
147            });
148        }
149    }
150
151    let total_plugins = registry_stats.total_plugins as usize;
152    let success_rate = if total_plugins > 0 {
153        (loaded as f64 / total_plugins as f64) * 100.0
154    } else {
155        100.0
156    };
157
158    let status = PluginStatusData {
159        stats: PluginStats {
160            total_plugins,
161            discovered: total_plugins,
162            loaded,
163            failed,
164            skipped: 0, // Not tracked in current registry
165            success_rate,
166        },
167        health,
168        last_updated: Some(registry_stats.last_updated.to_rfc3339()),
169    };
170
171    Json(ApiResponse::success(status))
172}
173
174/// Get plugin details
175pub async fn get_plugin_details(
176    State(state): State<AdminState>,
177    Path(plugin_id): Path<String>,
178) -> Json<ApiResponse<serde_json::Value>> {
179    let registry = state.plugin_registry.read().await;
180    let plugin_id_core = mockforge_plugin_core::PluginId::new(&plugin_id);
181
182    if let Some(plugin_instance) = registry.get_plugin(&plugin_id_core) {
183        let details = json!({
184            "id": plugin_instance.manifest.info.id.as_str(),
185            "name": plugin_instance.manifest.info.name,
186            "version": plugin_instance.manifest.info.version.to_string(),
187            "description": plugin_instance.manifest.info.description,
188            "author": {
189                "name": plugin_instance.manifest.info.author.name,
190                "email": plugin_instance.manifest.info.author.email
191            },
192            "capabilities": plugin_instance.manifest.capabilities,
193            "dependencies": plugin_instance.manifest.dependencies.iter()
194                .map(|(id, version)| json!({
195                    "id": id.as_str(),
196                    "version": version.to_string()
197                }))
198                .collect::<Vec<_>>(),
199            "state": plugin_instance.state.to_string(),
200            "healthy": plugin_instance.is_healthy(),
201            "health": {
202                "state": plugin_instance.health.state.to_string(),
203                "healthy": plugin_instance.health.healthy,
204                "message": plugin_instance.health.message,
205                "last_check": plugin_instance.health.last_check.to_rfc3339(),
206                "metrics": {
207                    "total_executions": plugin_instance.health.metrics.total_executions,
208                    "successful_executions": plugin_instance.health.metrics.successful_executions,
209                    "failed_executions": plugin_instance.health.metrics.failed_executions,
210                    "avg_execution_time_ms": plugin_instance.health.metrics.avg_execution_time_ms,
211                    "max_execution_time_ms": plugin_instance.health.metrics.max_execution_time_ms,
212                    "memory_usage_bytes": plugin_instance.health.metrics.memory_usage_bytes,
213                    "peak_memory_usage_bytes": plugin_instance.health.metrics.peak_memory_usage_bytes
214                }
215            }
216        });
217
218        Json(ApiResponse::success(details))
219    } else {
220        Json(ApiResponse::error(format!("Plugin not found: {}", plugin_id)))
221    }
222}
223
224/// Delete a plugin
225pub async fn delete_plugin(
226    State(state): State<AdminState>,
227    Path(plugin_id): Path<String>,
228) -> Json<ApiResponse<serde_json::Value>> {
229    let mut registry = state.plugin_registry.write().await;
230    let plugin_id_core = mockforge_plugin_core::PluginId::new(&plugin_id);
231
232    match registry.remove_plugin(&plugin_id_core) {
233        Ok(removed_plugin) => Json(ApiResponse::success(json!({
234            "message": format!("Plugin '{}' removed successfully", plugin_id),
235            "plugin": {
236                "id": removed_plugin.id.as_str(),
237                "name": removed_plugin.manifest.info.name
238            }
239        }))),
240        Err(_) => Json(ApiResponse::error(format!("Failed to remove plugin: {}", plugin_id))),
241    }
242}
243
244/// Reload a plugin
245pub async fn reload_plugin(
246    State(state): State<AdminState>,
247    Json(payload): Json<ReloadPluginRequest>,
248) -> Json<ApiResponse<serde_json::Value>> {
249    let registry = state.plugin_registry.read().await;
250    let plugin_id_core = mockforge_plugin_core::PluginId::new(&payload.plugin_id);
251
252    if registry.has_plugin(&plugin_id_core) {
253        // In a real implementation, this would unload and reload the plugin from disk
254        // For now, just return success since the plugin exists
255        Json(ApiResponse::success(json!({
256            "message": format!("Plugin '{}' reload initiated", payload.plugin_id),
257            "status": "reloading"
258        })))
259    } else {
260        Json(ApiResponse::error(format!("Plugin not found: {}", payload.plugin_id)))
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_plugin_info_creation() {
270        let info = PluginInfo {
271            id: "test".to_string(),
272            name: "Test Plugin".to_string(),
273            version: "1.0.0".to_string(),
274            types: vec!["resolver".to_string()],
275            status: "ready".to_string(),
276            healthy: true,
277            description: "Test description".to_string(),
278            author: "Test Author".to_string(),
279        };
280
281        assert_eq!(info.id, "test");
282        assert_eq!(info.name, "Test Plugin");
283        assert_eq!(info.version, "1.0.0");
284        assert!(info.healthy);
285        assert_eq!(info.author, "Test Author");
286    }
287
288    #[test]
289    fn test_plugin_stats_creation() {
290        let stats = PluginStats {
291            total_plugins: 10,
292            discovered: 10,
293            loaded: 8,
294            failed: 2,
295            skipped: 0,
296            success_rate: 80.0,
297        };
298
299        assert_eq!(stats.total_plugins, 10);
300        assert_eq!(stats.discovered, 10);
301        assert_eq!(stats.loaded, 8);
302        assert_eq!(stats.failed, 2);
303        assert_eq!(stats.success_rate, 80.0);
304    }
305
306    #[test]
307    fn test_plugin_health_info_creation() {
308        let health = PluginHealthInfo {
309            id: "plugin-1".to_string(),
310            healthy: true,
311            message: "All good".to_string(),
312            last_check: "2024-01-01T00:00:00Z".to_string(),
313        };
314
315        assert_eq!(health.id, "plugin-1");
316        assert!(health.healthy);
317        assert_eq!(health.message, "All good");
318    }
319
320    #[test]
321    fn test_plugin_status_data_creation() {
322        let stats = PluginStats {
323            total_plugins: 5,
324            discovered: 5,
325            loaded: 5,
326            failed: 0,
327            skipped: 0,
328            success_rate: 100.0,
329        };
330
331        let status_data = PluginStatusData {
332            stats,
333            health: vec![],
334            last_updated: Some("2024-01-01T00:00:00Z".to_string()),
335        };
336
337        assert_eq!(status_data.stats.total_plugins, 5);
338        assert!(status_data.health.is_empty());
339    }
340
341    #[test]
342    fn test_reload_plugin_request() {
343        let req = ReloadPluginRequest {
344            plugin_id: "test-plugin".to_string(),
345        };
346
347        assert_eq!(req.plugin_id, "test-plugin");
348    }
349
350    #[test]
351    fn test_plugin_list_query_default() {
352        let query = PluginListQuery {
353            plugin_type: None,
354            status: None,
355        };
356
357        assert!(query.plugin_type.is_none());
358        assert!(query.status.is_none());
359    }
360
361    #[test]
362    fn test_plugin_list_query_with_filters() {
363        let query = PluginListQuery {
364            plugin_type: Some("resolver".to_string()),
365            status: Some("ready".to_string()),
366        };
367
368        assert_eq!(query.plugin_type, Some("resolver".to_string()));
369        assert_eq!(query.status, Some("ready".to_string()));
370    }
371}