1use 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#[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#[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#[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#[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#[derive(Debug, Deserialize)]
55pub struct PluginListQuery {
56 #[serde(rename = "type")]
57 pub plugin_type: Option<String>,
58 pub status: Option<String>,
59}
60
61#[derive(Debug, Deserialize)]
63pub struct ReloadPluginRequest {
64 pub plugin_id: String,
65}
66
67pub 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 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 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
112pub 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 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, success_rate,
166 },
167 health,
168 last_updated: Some(registry_stats.last_updated.to_rfc3339()),
169 };
170
171 Json(ApiResponse::success(status))
172}
173
174pub 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
224pub 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
244pub 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 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}