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 = 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, success_rate,
165 },
166 health,
167 last_updated: Some(registry_stats.last_updated.to_rfc3339()),
168 };
169
170 Json(ApiResponse::success(status))
171}
172
173pub 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 let plugin_id_core = match registry.list_plugins().iter().find(|id| id.as_str() == plugin_id) {
182 Some(id) => id.clone(),
183 None => {
184 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
231pub 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 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
257pub async fn reload_plugin(
263 State(state): State<AdminState>,
264 Json(payload): Json<ReloadPluginRequest>,
265) -> Json<ApiResponse<serde_json::Value>> {
266 let mut registry = state.plugin_registry.write().await;
267 let plugin_id_core = match registry
269 .list_plugins()
270 .iter()
271 .find(|id| id.as_str() == payload.plugin_id)
272 {
273 Some(id) => id.clone(),
274 None => {
275 return Json(ApiResponse::error(format!("Plugin not found: {}", payload.plugin_id)));
276 }
277 };
278
279 match registry.remove_plugin(&plugin_id_core) {
281 Ok(mut instance) => {
282 instance.health = Default::default();
284 let plugin_name = instance.manifest.info.name.clone();
285 match registry.add_plugin(instance) {
286 Ok(()) => Json(ApiResponse::success(json!({
287 "message": format!("Plugin '{}' reloaded successfully", plugin_name),
288 "status": "loaded"
289 }))),
290 Err(e) => Json(ApiResponse::error(format!(
291 "Failed to re-register plugin '{}': {}",
292 payload.plugin_id, e
293 ))),
294 }
295 }
296 Err(e) => Json(ApiResponse::error(format!(
297 "Failed to unload plugin '{}': {}",
298 payload.plugin_id, e
299 ))),
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_plugin_info_creation() {
309 let info = PluginInfo {
310 id: "test".to_string(),
311 name: "Test Plugin".to_string(),
312 version: "1.0.0".to_string(),
313 types: vec!["resolver".to_string()],
314 status: "ready".to_string(),
315 healthy: true,
316 description: "Test description".to_string(),
317 author: "Test Author".to_string(),
318 };
319
320 assert_eq!(info.id, "test");
321 assert_eq!(info.name, "Test Plugin");
322 assert_eq!(info.version, "1.0.0");
323 assert!(info.healthy);
324 assert_eq!(info.author, "Test Author");
325 }
326
327 #[test]
328 fn test_plugin_stats_creation() {
329 let stats = PluginStats {
330 total_plugins: 10,
331 discovered: 10,
332 loaded: 8,
333 failed: 2,
334 skipped: 0,
335 success_rate: 80.0,
336 };
337
338 assert_eq!(stats.total_plugins, 10);
339 assert_eq!(stats.discovered, 10);
340 assert_eq!(stats.loaded, 8);
341 assert_eq!(stats.failed, 2);
342 assert_eq!(stats.success_rate, 80.0);
343 }
344
345 #[test]
346 fn test_plugin_health_info_creation() {
347 let health = PluginHealthInfo {
348 id: "plugin-1".to_string(),
349 healthy: true,
350 message: "All good".to_string(),
351 last_check: "2024-01-01T00:00:00Z".to_string(),
352 };
353
354 assert_eq!(health.id, "plugin-1");
355 assert!(health.healthy);
356 assert_eq!(health.message, "All good");
357 }
358
359 #[test]
360 fn test_plugin_status_data_creation() {
361 let stats = PluginStats {
362 total_plugins: 5,
363 discovered: 5,
364 loaded: 5,
365 failed: 0,
366 skipped: 0,
367 success_rate: 100.0,
368 };
369
370 let status_data = PluginStatusData {
371 stats,
372 health: vec![],
373 last_updated: Some("2024-01-01T00:00:00Z".to_string()),
374 };
375
376 assert_eq!(status_data.stats.total_plugins, 5);
377 assert!(status_data.health.is_empty());
378 }
379
380 #[test]
381 fn test_reload_plugin_request() {
382 let req = ReloadPluginRequest {
383 plugin_id: "test-plugin".to_string(),
384 };
385
386 assert_eq!(req.plugin_id, "test-plugin");
387 }
388
389 #[test]
390 fn test_plugin_list_query_default() {
391 let query = PluginListQuery {
392 plugin_type: None,
393 status: None,
394 };
395
396 assert!(query.plugin_type.is_none());
397 assert!(query.status.is_none());
398 }
399
400 #[test]
401 fn test_plugin_list_query_with_filters() {
402 let query = PluginListQuery {
403 plugin_type: Some("resolver".to_string()),
404 status: Some("ready".to_string()),
405 };
406
407 assert_eq!(query.plugin_type, Some("resolver".to_string()));
408 assert_eq!(query.status, Some("ready".to_string()));
409 }
410}