1use crate::models::{HealthStatus, ProjectStatus, Workspace, WorkspaceMetrics};
4use crate::OrchestrationError;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct StatusReporter {
11 workspace: Workspace,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct StatusReport {
18 pub health_status: HealthStatus,
20
21 pub compliance_score: f64,
23
24 pub total_projects: usize,
26
27 pub total_dependencies: usize,
29
30 pub healthy_projects: usize,
32
33 pub warning_projects: usize,
35
36 pub critical_projects: usize,
38
39 pub unknown_projects: usize,
41
42 pub project_statuses: HashMap<String, ProjectStatus>,
44
45 pub metrics: WorkspaceMetrics,
47
48 pub timestamp: String,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct AggregatedMetrics {
55 pub average_health: f64,
57
58 pub healthy_percentage: f64,
60
61 pub warning_percentage: f64,
63
64 pub critical_percentage: f64,
66
67 pub avg_dependencies_per_project: f64,
69
70 pub max_dependencies: usize,
72
73 pub min_dependencies: usize,
75
76 pub total_rules: usize,
78
79 pub enabled_rules: usize,
81
82 pub disabled_rules: usize,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ProjectHealthIndicator {
89 pub name: String,
91
92 pub status: ProjectStatus,
94
95 pub dependency_count: usize,
97
98 pub dependent_count: usize,
100
101 pub rule_compliance: f64,
103}
104
105impl StatusReporter {
106 pub fn new(workspace: Workspace) -> Self {
108 Self { workspace }
109 }
110
111 pub fn generate_report(&self) -> Result<StatusReport, OrchestrationError> {
113 let timestamp = chrono::Utc::now().to_rfc3339();
114
115 let mut healthy_count = 0;
117 let mut warning_count = 0;
118 let mut critical_count = 0;
119 let mut unknown_count = 0;
120 let mut project_statuses = HashMap::new();
121
122 for project in &self.workspace.projects {
123 project_statuses.insert(project.name.clone(), project.status);
124
125 match project.status {
126 ProjectStatus::Healthy => healthy_count += 1,
127 ProjectStatus::Warning => warning_count += 1,
128 ProjectStatus::Critical => critical_count += 1,
129 ProjectStatus::Unknown => unknown_count += 1,
130 }
131 }
132
133 let report = StatusReport {
134 health_status: self.workspace.metrics.health_status,
135 compliance_score: self.workspace.metrics.compliance_score,
136 total_projects: self.workspace.projects.len(),
137 total_dependencies: self.workspace.dependencies.len(),
138 healthy_projects: healthy_count,
139 warning_projects: warning_count,
140 critical_projects: critical_count,
141 unknown_projects: unknown_count,
142 project_statuses,
143 metrics: self.workspace.metrics.clone(),
144 timestamp,
145 };
146
147 Ok(report)
148 }
149
150 pub fn collect_metrics(&self) -> Result<AggregatedMetrics, OrchestrationError> {
152 let total_projects = self.workspace.projects.len() as f64;
153
154 if total_projects == 0.0 {
155 return Ok(AggregatedMetrics {
156 average_health: 1.0,
157 healthy_percentage: 100.0,
158 warning_percentage: 0.0,
159 critical_percentage: 0.0,
160 avg_dependencies_per_project: 0.0,
161 max_dependencies: 0,
162 min_dependencies: 0,
163 total_rules: self.workspace.config.rules.len(),
164 enabled_rules: self.workspace.config.rules.iter().filter(|r| r.enabled).count(),
165 disabled_rules: self.workspace.config.rules.iter().filter(|r| !r.enabled).count(),
166 });
167 }
168
169 let mut healthy_count = 0.0;
171 let mut warning_count = 0.0;
172 let mut critical_count = 0.0;
173
174 for project in &self.workspace.projects {
175 match project.status {
176 ProjectStatus::Healthy => healthy_count += 1.0,
177 ProjectStatus::Warning => warning_count += 1.0,
178 ProjectStatus::Critical => critical_count += 1.0,
179 ProjectStatus::Unknown => {}
180 }
181 }
182
183 let mut project_dep_counts: HashMap<String, usize> = HashMap::new();
185 for dep in &self.workspace.dependencies {
186 *project_dep_counts.entry(dep.from.clone()).or_insert(0) += 1;
187 }
188
189 let (max_deps, min_deps) = if project_dep_counts.is_empty() {
190 (0, 0)
191 } else {
192 let max = *project_dep_counts.values().max().unwrap_or(&0);
193 let min = *project_dep_counts.values().min().unwrap_or(&0);
194 (max, min)
195 };
196
197 let avg_deps = if self.workspace.projects.is_empty() {
198 0.0
199 } else {
200 self.workspace.dependencies.len() as f64 / self.workspace.projects.len() as f64
201 };
202
203 Ok(AggregatedMetrics {
204 average_health: self.workspace.metrics.compliance_score,
205 healthy_percentage: (healthy_count / total_projects) * 100.0,
206 warning_percentage: (warning_count / total_projects) * 100.0,
207 critical_percentage: (critical_count / total_projects) * 100.0,
208 avg_dependencies_per_project: avg_deps,
209 max_dependencies: max_deps,
210 min_dependencies: min_deps,
211 total_rules: self.workspace.config.rules.len(),
212 enabled_rules: self.workspace.config.rules.iter().filter(|r| r.enabled).count(),
213 disabled_rules: self.workspace.config.rules.iter().filter(|r| !r.enabled).count(),
214 })
215 }
216
217 pub fn get_project_health_indicators(&self) -> Result<Vec<ProjectHealthIndicator>, OrchestrationError> {
219 let mut indicators = Vec::new();
220
221 for project in &self.workspace.projects {
222 let dependency_count = self
224 .workspace
225 .dependencies
226 .iter()
227 .filter(|d| d.from == project.name)
228 .count();
229
230 let dependent_count = self
232 .workspace
233 .dependencies
234 .iter()
235 .filter(|d| d.to == project.name)
236 .count();
237
238 let rule_compliance = self.workspace.metrics.compliance_score;
240
241 indicators.push(ProjectHealthIndicator {
242 name: project.name.clone(),
243 status: project.status,
244 dependency_count,
245 dependent_count,
246 rule_compliance,
247 });
248 }
249
250 Ok(indicators)
251 }
252
253 pub fn track_project_health(&self, project_name: &str) -> Result<ProjectHealthIndicator, OrchestrationError> {
255 let project = self
256 .workspace
257 .projects
258 .iter()
259 .find(|p| p.name == project_name)
260 .ok_or_else(|| OrchestrationError::ProjectNotFound(project_name.to_string()))?;
261
262 let dependency_count = self
263 .workspace
264 .dependencies
265 .iter()
266 .filter(|d| d.from == project.name)
267 .count();
268
269 let dependent_count = self
270 .workspace
271 .dependencies
272 .iter()
273 .filter(|d| d.to == project.name)
274 .count();
275
276 Ok(ProjectHealthIndicator {
277 name: project.name.clone(),
278 status: project.status,
279 dependency_count,
280 dependent_count,
281 rule_compliance: self.workspace.metrics.compliance_score,
282 })
283 }
284
285 pub fn generate_compliance_summary(&self) -> Result<ComplianceSummary, OrchestrationError> {
287 let total_rules = self.workspace.config.rules.len();
288 let enabled_rules = self.workspace.config.rules.iter().filter(|r| r.enabled).count();
289
290 let mut violations = Vec::new();
291
292 for project in &self.workspace.projects {
294 if project.status == ProjectStatus::Critical {
295 violations.push(format!("Project '{}' has critical status", project.name));
296 }
297 }
298
299 let compliance_score = self.workspace.metrics.compliance_score;
300 let is_compliant = compliance_score >= 0.8 && violations.is_empty();
301
302 Ok(ComplianceSummary {
303 total_rules,
304 enabled_rules,
305 compliance_score,
306 is_compliant,
307 violations,
308 })
309 }
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct ComplianceSummary {
315 pub total_rules: usize,
317
318 pub enabled_rules: usize,
320
321 pub compliance_score: f64,
323
324 pub is_compliant: bool,
326
327 pub violations: Vec<String>,
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::models::{DependencyType, Project, ProjectDependency, WorkspaceConfig, WorkspaceRule, RuleType};
335
336 fn create_test_workspace() -> Workspace {
337 let mut workspace = Workspace::default();
338
339 workspace.projects = vec![
340 Project {
341 path: "/path/to/project1".into(),
342 name: "project1".to_string(),
343 project_type: "rust".to_string(),
344 version: "0.1.0".to_string(),
345 status: ProjectStatus::Healthy,
346 },
347 Project {
348 path: "/path/to/project2".into(),
349 name: "project2".to_string(),
350 project_type: "rust".to_string(),
351 version: "0.1.0".to_string(),
352 status: ProjectStatus::Warning,
353 },
354 Project {
355 path: "/path/to/project3".into(),
356 name: "project3".to_string(),
357 project_type: "rust".to_string(),
358 version: "0.1.0".to_string(),
359 status: ProjectStatus::Healthy,
360 },
361 ];
362
363 workspace.dependencies = vec![
364 ProjectDependency {
365 from: "project1".to_string(),
366 to: "project2".to_string(),
367 dependency_type: DependencyType::Direct,
368 version_constraint: "^0.1.0".to_string(),
369 },
370 ProjectDependency {
371 from: "project2".to_string(),
372 to: "project3".to_string(),
373 dependency_type: DependencyType::Direct,
374 version_constraint: "^0.1.0".to_string(),
375 },
376 ];
377
378 workspace.config = WorkspaceConfig {
379 rules: vec![
380 WorkspaceRule {
381 name: "no-circular-deps".to_string(),
382 rule_type: RuleType::DependencyConstraint,
383 enabled: true,
384 },
385 WorkspaceRule {
386 name: "naming-convention".to_string(),
387 rule_type: RuleType::NamingConvention,
388 enabled: true,
389 },
390 ],
391 settings: serde_json::json!({}),
392 };
393
394 workspace.metrics = WorkspaceMetrics {
395 total_projects: 3,
396 total_dependencies: 2,
397 compliance_score: 0.95,
398 health_status: HealthStatus::Healthy,
399 };
400
401 workspace
402 }
403
404 #[test]
405 fn test_status_reporter_creation() {
406 let workspace = create_test_workspace();
407 let reporter = StatusReporter::new(workspace);
408
409 assert_eq!(reporter.workspace.projects.len(), 3);
410 }
411
412 #[test]
413 fn test_generate_report() {
414 let workspace = create_test_workspace();
415 let reporter = StatusReporter::new(workspace);
416
417 let report = reporter.generate_report().expect("report generation failed");
418
419 assert_eq!(report.total_projects, 3);
420 assert_eq!(report.total_dependencies, 2);
421 assert_eq!(report.healthy_projects, 2);
422 assert_eq!(report.warning_projects, 1);
423 assert_eq!(report.critical_projects, 0);
424 assert_eq!(report.compliance_score, 0.95);
425 }
426
427 #[test]
428 fn test_collect_metrics() {
429 let workspace = create_test_workspace();
430 let reporter = StatusReporter::new(workspace);
431
432 let metrics = reporter.collect_metrics().expect("metrics collection failed");
433
434 assert_eq!(metrics.total_rules, 2);
435 assert_eq!(metrics.enabled_rules, 2);
436 assert_eq!(metrics.disabled_rules, 0);
437 assert!(metrics.healthy_percentage > 0.0);
438 assert!(metrics.warning_percentage > 0.0);
439 }
440
441 #[test]
442 fn test_get_project_health_indicators() {
443 let workspace = create_test_workspace();
444 let reporter = StatusReporter::new(workspace);
445
446 let indicators = reporter
447 .get_project_health_indicators()
448 .expect("health indicators failed");
449
450 assert_eq!(indicators.len(), 3);
451 assert_eq!(indicators[0].name, "project1");
452 assert_eq!(indicators[0].status, ProjectStatus::Healthy);
453 }
454
455 #[test]
456 fn test_track_project_health() {
457 let workspace = create_test_workspace();
458 let reporter = StatusReporter::new(workspace);
459
460 let health = reporter
461 .track_project_health("project1")
462 .expect("health tracking failed");
463
464 assert_eq!(health.name, "project1");
465 assert_eq!(health.status, ProjectStatus::Healthy);
466 }
467
468 #[test]
469 fn test_track_project_health_not_found() {
470 let workspace = create_test_workspace();
471 let reporter = StatusReporter::new(workspace);
472
473 let result = reporter.track_project_health("nonexistent");
474
475 assert!(result.is_err());
476 }
477
478 #[test]
479 fn test_generate_compliance_summary() {
480 let workspace = create_test_workspace();
481 let reporter = StatusReporter::new(workspace);
482
483 let summary = reporter
484 .generate_compliance_summary()
485 .expect("compliance summary failed");
486
487 assert_eq!(summary.total_rules, 2);
488 assert_eq!(summary.enabled_rules, 2);
489 assert!(summary.is_compliant);
490 }
491
492 #[test]
493 fn test_empty_workspace() {
494 let workspace = Workspace::default();
495 let reporter = StatusReporter::new(workspace);
496
497 let report = reporter.generate_report().expect("report generation failed");
498
499 assert_eq!(report.total_projects, 0);
500 assert_eq!(report.total_dependencies, 0);
501 }
502
503 #[test]
504 fn test_metrics_with_empty_workspace() {
505 let workspace = Workspace::default();
506 let reporter = StatusReporter::new(workspace);
507
508 let metrics = reporter.collect_metrics().expect("metrics collection failed");
509
510 assert_eq!(metrics.avg_dependencies_per_project, 0.0);
511 assert_eq!(metrics.max_dependencies, 0);
512 }
513
514 #[test]
515 fn test_report_timestamp() {
516 let workspace = create_test_workspace();
517 let reporter = StatusReporter::new(workspace);
518
519 let report = reporter.generate_report().expect("report generation failed");
520
521 assert!(!report.timestamp.is_empty());
523 assert!(report.timestamp.contains('T'));
524 }
525
526 #[test]
527 fn test_project_status_breakdown() {
528 let workspace = create_test_workspace();
529 let reporter = StatusReporter::new(workspace);
530
531 let report = reporter.generate_report().expect("report generation failed");
532
533 assert_eq!(report.project_statuses.len(), 3);
534 assert_eq!(
535 report.project_statuses.get("project1"),
536 Some(&ProjectStatus::Healthy)
537 );
538 assert_eq!(
539 report.project_statuses.get("project2"),
540 Some(&ProjectStatus::Warning)
541 );
542 }
543
544 #[test]
545 fn test_dependency_counting() {
546 let workspace = create_test_workspace();
547 let reporter = StatusReporter::new(workspace);
548
549 let indicators = reporter
550 .get_project_health_indicators()
551 .expect("health indicators failed");
552
553 assert_eq!(indicators[0].dependency_count, 1);
555 assert_eq!(indicators[1].dependency_count, 1);
557 assert_eq!(indicators[2].dependency_count, 0);
559 }
560
561 #[test]
562 fn test_dependent_counting() {
563 let workspace = create_test_workspace();
564 let reporter = StatusReporter::new(workspace);
565
566 let indicators = reporter
567 .get_project_health_indicators()
568 .expect("health indicators failed");
569
570 assert_eq!(indicators[0].dependent_count, 0);
572 assert_eq!(indicators[1].dependent_count, 1);
574 assert_eq!(indicators[2].dependent_count, 1);
576 }
577
578 #[test]
579 fn test_compliance_summary_with_critical_project() {
580 let mut workspace = create_test_workspace();
581 workspace.projects[0].status = ProjectStatus::Critical;
582
583 let reporter = StatusReporter::new(workspace);
584 let summary = reporter
585 .generate_compliance_summary()
586 .expect("compliance summary failed");
587
588 assert!(!summary.violations.is_empty());
589 }
590
591 #[test]
592 fn test_aggregated_metrics_percentages() {
593 let workspace = create_test_workspace();
594 let reporter = StatusReporter::new(workspace);
595
596 let metrics = reporter.collect_metrics().expect("metrics collection failed");
597
598 let total = metrics.healthy_percentage + metrics.warning_percentage + metrics.critical_percentage;
599 assert!((total - 100.0).abs() < 0.1);
601 }
602}