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(
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 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 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}