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 = plugin_instance.state.is_ready();
128
129            if is_loaded {
130                loaded += 1;
131            } else {
132                failed += 1;
133            }
134
135            let is_healthy = plugin_instance.is_healthy();
136
137            health.push(PluginHealthInfo {
138                id: plugin_id.as_str().to_string(),
139                healthy: is_healthy,
140                message: if is_healthy {
141                    "Plugin is healthy".to_string()
142                } else {
143                    "Plugin has issues".to_string()
144                },
145                last_check: plugin_instance.health.last_check.to_rfc3339(),
146            });
147        }
148    }
149
150    let total_plugins = registry_stats.total_plugins as usize;
151    let success_rate = if total_plugins > 0 {
152        (loaded as f64 / total_plugins as f64) * 100.0
153    } else {
154        100.0
155    };
156
157    let status = PluginStatusData {
158        stats: PluginStats {
159            total_plugins,
160            discovered: total_plugins,
161            loaded,
162            failed,
163            skipped: 0, // Not tracked in current registry
164            success_rate,
165        },
166        health,
167        last_updated: Some(registry_stats.last_updated.to_rfc3339()),
168    };
169
170    Json(ApiResponse::success(status))
171}
172
173/// Get plugin details
174pub async fn get_plugin_details(
175    State(state): State<AdminState>,
176    Path(plugin_id): Path<String>,
177) -> Json<ApiResponse<serde_json::Value>> {
178    let registry = state.plugin_registry.read().await;
179    // Find plugin ID from registry - must exist in registry to query it
180    // The registry uses PluginId from its version of mockforge_plugin_core
181    let plugin_id_core = match registry.list_plugins().iter().find(|id| id.as_str() == plugin_id) {
182        Some(id) => id.clone(),
183        None => {
184            // Plugin not found in registry
185            return Json(ApiResponse::error(format!("Plugin not found: {}", plugin_id)));
186        }
187    };
188
189    if let Some(plugin_instance) = registry.get_plugin(&plugin_id_core) {
190        let details = json!({
191            "id": plugin_instance.manifest.info.id.as_str(),
192            "name": plugin_instance.manifest.info.name,
193            "version": plugin_instance.manifest.info.version.to_string(),
194            "description": plugin_instance.manifest.info.description,
195            "author": {
196                "name": plugin_instance.manifest.info.author.name,
197                "email": plugin_instance.manifest.info.author.email
198            },
199            "capabilities": plugin_instance.manifest.capabilities,
200            "dependencies": plugin_instance.manifest.dependencies.iter()
201                .map(|(id, version)| json!({
202                    "id": id.as_str(),
203                    "version": version.to_string()
204                }))
205                .collect::<Vec<_>>(),
206            "state": plugin_instance.state.to_string(),
207            "healthy": plugin_instance.is_healthy(),
208            "health": {
209                "state": plugin_instance.health.state.to_string(),
210                "healthy": plugin_instance.health.healthy,
211                "message": plugin_instance.health.message,
212                "last_check": plugin_instance.health.last_check.to_rfc3339(),
213                "metrics": {
214                    "total_executions": plugin_instance.health.metrics.total_executions,
215                    "successful_executions": plugin_instance.health.metrics.successful_executions,
216                    "failed_executions": plugin_instance.health.metrics.failed_executions,
217                    "avg_execution_time_ms": plugin_instance.health.metrics.avg_execution_time_ms,
218                    "max_execution_time_ms": plugin_instance.health.metrics.max_execution_time_ms,
219                    "memory_usage_bytes": plugin_instance.health.metrics.memory_usage_bytes,
220                    "peak_memory_usage_bytes": plugin_instance.health.metrics.peak_memory_usage_bytes
221                }
222            }
223        });
224
225        Json(ApiResponse::success(details))
226    } else {
227        Json(ApiResponse::error(format!("Plugin not found: {}", plugin_id)))
228    }
229}
230
231/// Delete a plugin
232pub async fn delete_plugin(
233    State(state): State<AdminState>,
234    Path(plugin_id): Path<String>,
235) -> Json<ApiResponse<serde_json::Value>> {
236    let mut registry = state.plugin_registry.write().await;
237    // Find plugin ID from registry - must exist to remove it
238    let plugin_id_core = match registry.list_plugins().iter().find(|id| id.as_str() == plugin_id) {
239        Some(id) => id.clone(),
240        None => {
241            return Json(ApiResponse::error(format!("Plugin not found: {}", plugin_id)));
242        }
243    };
244
245    match registry.remove_plugin(&plugin_id_core) {
246        Ok(removed_plugin) => Json(ApiResponse::success(json!({
247            "message": format!("Plugin '{}' removed successfully", plugin_id),
248            "plugin": {
249                "id": removed_plugin.id.as_str(),
250                "name": removed_plugin.manifest.info.name
251            }
252        }))),
253        Err(_) => Json(ApiResponse::error(format!("Failed to remove plugin: {}", plugin_id))),
254    }
255}
256
257/// Reload a plugin
258pub async fn reload_plugin(
259    State(state): State<AdminState>,
260    Json(payload): Json<ReloadPluginRequest>,
261) -> Json<ApiResponse<serde_json::Value>> {
262    let registry = state.plugin_registry.read().await;
263    // Find plugin ID from registry - must exist to reload it
264    let plugin_id_core = match registry
265        .list_plugins()
266        .iter()
267        .find(|id| id.as_str() == payload.plugin_id)
268    {
269        Some(id) => id.clone(),
270        None => {
271            return Json(ApiResponse::error(format!("Plugin not found: {}", payload.plugin_id)));
272        }
273    };
274
275    if registry.has_plugin(&plugin_id_core) {
276        // In a real implementation, this would unload and reload the plugin from disk
277        // For now, just return success since the plugin exists
278        Json(ApiResponse::success(json!({
279            "message": format!("Plugin '{}' reload initiated", payload.plugin_id),
280            "status": "reloading"
281        })))
282    } else {
283        Json(ApiResponse::error(format!("Plugin not found: {}", payload.plugin_id)))
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_plugin_info_creation() {
293        let info = PluginInfo {
294            id: "test".to_string(),
295            name: "Test Plugin".to_string(),
296            version: "1.0.0".to_string(),
297            types: vec!["resolver".to_string()],
298            status: "ready".to_string(),
299            healthy: true,
300            description: "Test description".to_string(),
301            author: "Test Author".to_string(),
302        };
303
304        assert_eq!(info.id, "test");
305        assert_eq!(info.name, "Test Plugin");
306        assert_eq!(info.version, "1.0.0");
307        assert!(info.healthy);
308        assert_eq!(info.author, "Test Author");
309    }
310
311    #[test]
312    fn test_plugin_stats_creation() {
313        let stats = PluginStats {
314            total_plugins: 10,
315            discovered: 10,
316            loaded: 8,
317            failed: 2,
318            skipped: 0,
319            success_rate: 80.0,
320        };
321
322        assert_eq!(stats.total_plugins, 10);
323        assert_eq!(stats.discovered, 10);
324        assert_eq!(stats.loaded, 8);
325        assert_eq!(stats.failed, 2);
326        assert_eq!(stats.success_rate, 80.0);
327    }
328
329    #[test]
330    fn test_plugin_health_info_creation() {
331        let health = PluginHealthInfo {
332            id: "plugin-1".to_string(),
333            healthy: true,
334            message: "All good".to_string(),
335            last_check: "2024-01-01T00:00:00Z".to_string(),
336        };
337
338        assert_eq!(health.id, "plugin-1");
339        assert!(health.healthy);
340        assert_eq!(health.message, "All good");
341    }
342
343    #[test]
344    fn test_plugin_status_data_creation() {
345        let stats = PluginStats {
346            total_plugins: 5,
347            discovered: 5,
348            loaded: 5,
349            failed: 0,
350            skipped: 0,
351            success_rate: 100.0,
352        };
353
354        let status_data = PluginStatusData {
355            stats,
356            health: vec![],
357            last_updated: Some("2024-01-01T00:00:00Z".to_string()),
358        };
359
360        assert_eq!(status_data.stats.total_plugins, 5);
361        assert!(status_data.health.is_empty());
362    }
363
364    #[test]
365    fn test_reload_plugin_request() {
366        let req = ReloadPluginRequest {
367            plugin_id: "test-plugin".to_string(),
368        };
369
370        assert_eq!(req.plugin_id, "test-plugin");
371    }
372
373    #[test]
374    fn test_plugin_list_query_default() {
375        let query = PluginListQuery {
376            plugin_type: None,
377            status: None,
378        };
379
380        assert!(query.plugin_type.is_none());
381        assert!(query.status.is_none());
382    }
383
384    #[test]
385    fn test_plugin_list_query_with_filters() {
386        let query = PluginListQuery {
387            plugin_type: Some("resolver".to_string()),
388            status: Some("ready".to_string()),
389        };
390
391        assert_eq!(query.plugin_type, Some("resolver".to_string()));
392        assert_eq!(query.status, Some("ready".to_string()));
393    }
394}