syncable_cli/analyzer/k8s_optimize/formatter/
output.rs

1//! Output formatting for optimization results.
2//!
3//! Supports multiple output formats: table, JSON, and plain text.
4
5use crate::analyzer::k8s_optimize::types::{OptimizationResult, Severity};
6use colored::Colorize;
7use serde::{Deserialize, Serialize};
8
9// ============================================================================
10// Output Format
11// ============================================================================
12
13/// Output format for optimization results.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum OutputFormat {
17    /// ASCII table format (default)
18    #[default]
19    Table,
20    /// JSON format
21    Json,
22    /// YAML format
23    Yaml,
24    /// Plain text summary
25    Summary,
26}
27
28impl OutputFormat {
29    /// Parse from string.
30    pub fn parse(s: &str) -> Option<Self> {
31        match s.to_lowercase().as_str() {
32            "table" => Some(Self::Table),
33            "json" => Some(Self::Json),
34            "yaml" => Some(Self::Yaml),
35            "summary" => Some(Self::Summary),
36            _ => None,
37        }
38    }
39}
40
41// ============================================================================
42// Formatting Functions
43// ============================================================================
44
45/// Format optimization result to string.
46pub fn format_result_to_string(result: &OptimizationResult, format: OutputFormat) -> String {
47    match format {
48        OutputFormat::Table => format_table(result),
49        OutputFormat::Json => format_json(result),
50        OutputFormat::Yaml => format_yaml(result),
51        OutputFormat::Summary => format_summary(result),
52    }
53}
54
55/// Format and print optimization result.
56pub fn format_result(result: &OptimizationResult, format: OutputFormat) {
57    println!("{}", format_result_to_string(result, format));
58}
59
60// ============================================================================
61// Table Format
62// ============================================================================
63
64fn format_table(result: &OptimizationResult) -> String {
65    let mut output = String::new();
66
67    // Header
68    output.push_str(&format!(
69        "\n{}\n",
70        "═══════════════════════════════════════════════════════════════════════════════════════════════════"
71            .bright_blue()
72    ));
73    output.push_str(&format!(
74        "{}\n",
75        "💰 KUBERNETES RESOURCE OPTIMIZATION REPORT"
76            .bright_white()
77            .bold()
78    ));
79    output.push_str(&format!(
80        "{}\n\n",
81        "═══════════════════════════════════════════════════════════════════════════════════════════════════"
82            .bright_blue()
83    ));
84
85    // Summary section
86    output.push_str(&format_summary_section(result));
87
88    // Recommendations section
89    if result.has_recommendations() {
90        output.push_str(&format!(
91            "\n{}\n",
92            "┌─ Recommendations ─────────────────────────────────────────────────────────────────────────────┐"
93                .bright_blue()
94        ));
95
96        for (i, rec) in result.recommendations.iter().enumerate() {
97            let severity_icon = match rec.severity {
98                Severity::Critical => "🔴",
99                Severity::High => "🟠",
100                Severity::Medium => "🟡",
101                Severity::Low => "🟢",
102                Severity::Info => "ℹ️ ",
103            };
104
105            let severity_str = match rec.severity {
106                Severity::Critical => rec.severity.as_str().bright_red(),
107                Severity::High => rec.severity.as_str().red(),
108                Severity::Medium => rec.severity.as_str().yellow(),
109                Severity::Low => rec.severity.as_str().green(),
110                Severity::Info => rec.severity.as_str().blue(),
111            };
112
113            output.push_str(&format!(
114                "│\n│ {} {} {} {}\n",
115                severity_icon,
116                format!("[{}]", rec.rule_code).bright_cyan(),
117                severity_str.bold(),
118                rec.resource_identifier().bright_white()
119            ));
120
121            output.push_str(&format!(
122                "│   {} {} / {}\n",
123                "Resource:".dimmed(),
124                rec.resource_kind.cyan(),
125                rec.container.yellow()
126            ));
127
128            output.push_str(&format!("│   {} {}\n", "Issue:".dimmed(), rec.message));
129
130            // Show current vs recommended
131            if rec.current.has_any() || rec.recommended.has_any() {
132                output.push_str(&format!("│   {}\n", "Current:".dimmed()));
133                if let Some(cpu) = &rec.current.cpu_request {
134                    output.push_str(&format!("│     CPU request: {}\n", cpu.red()));
135                }
136                if let Some(mem) = &rec.current.memory_request {
137                    output.push_str(&format!("│     Memory request: {}\n", mem.red()));
138                }
139
140                output.push_str(&format!("│   {}\n", "Recommended:".dimmed()));
141                if let Some(cpu) = &rec.recommended.cpu_request {
142                    output.push_str(&format!("│     CPU request: {}\n", cpu.green()));
143                }
144                if let Some(mem) = &rec.recommended.memory_request {
145                    output.push_str(&format!("│     Memory request: {}\n", mem.green()));
146                }
147            }
148
149            if i < result.recommendations.len() - 1 {
150                output.push_str(&format!(
151                    "│{}",
152                    "────────────────────────────────────────────────────────────────────────────────────────────\n"
153                        .dimmed()
154                ));
155            }
156        }
157
158        output.push_str(&format!(
159            "{}\n",
160            "└────────────────────────────────────────────────────────────────────────────────────────────────┘"
161                .bright_blue()
162        ));
163    } else {
164        output.push_str(&format!(
165            "\n{}\n",
166            "✅ No optimization issues found! Your resources look well-configured.".green()
167        ));
168    }
169
170    // Footer
171    output.push_str(&format!(
172        "\n{}\n",
173        "═══════════════════════════════════════════════════════════════════════════════════════════════════"
174            .bright_blue()
175    ));
176
177    output
178}
179
180fn format_summary_section(result: &OptimizationResult) -> String {
181    let mut output = String::new();
182
183    output.push_str(&format!(
184        "{}",
185        "┌─ Summary ─────────────────────────────────────────────────────────────────────────────────────────┐\n"
186            .bright_blue()
187    ));
188
189    output.push_str(&format!(
190        "│ {} {:>6}     {} {:>6}     {} {:>6}\n",
191        "Resources:".dimmed(),
192        result.summary.resources_analyzed.to_string().bright_white(),
193        "Containers:".dimmed(),
194        result
195            .summary
196            .containers_analyzed
197            .to_string()
198            .bright_white(),
199        "Mode:".dimmed(),
200        result.metadata.mode.to_string().cyan(),
201    ));
202
203    output.push_str(&format!(
204        "│ {} {:>6}     {} {:>6}     {} {:>6}\n",
205        "Over-provisioned:".dimmed(),
206        if result.summary.over_provisioned > 0 {
207            result.summary.over_provisioned.to_string().red()
208        } else {
209            result.summary.over_provisioned.to_string().green()
210        },
211        "Missing requests:".dimmed(),
212        if result.summary.missing_requests > 0 {
213            result.summary.missing_requests.to_string().yellow()
214        } else {
215            result.summary.missing_requests.to_string().green()
216        },
217        "Optimal:".dimmed(),
218        result.summary.optimal.to_string().green(),
219    ));
220
221    if result.summary.total_waste_percentage > 0.0 {
222        output.push_str(&format!(
223            "│ {} {:.1}%\n",
224            "Estimated waste:".dimmed(),
225            result.summary.total_waste_percentage.to_string().red(),
226        ));
227    }
228
229    if let Some(savings) = result.summary.estimated_monthly_savings_usd {
230        output.push_str(&format!(
231            "│ {} ${:.2}/month\n",
232            "Potential savings:".dimmed(),
233            savings.to_string().green(),
234        ));
235    }
236
237    output.push_str(&format!(
238        "│ {} {}ms     {} {}\n",
239        "Duration:".dimmed(),
240        result.metadata.duration_ms.to_string().dimmed(),
241        "Path:".dimmed(),
242        result.metadata.path.display().to_string().dimmed(),
243    ));
244
245    output.push_str(&format!(
246        "{}",
247        "└───────────────────────────────────────────────────────────────────────────────────────────────────┘\n"
248            .bright_blue()
249    ));
250
251    output
252}
253
254// ============================================================================
255// JSON Format
256// ============================================================================
257
258fn format_json(result: &OptimizationResult) -> String {
259    serde_json::to_string_pretty(result).unwrap_or_else(|_| "{}".to_string())
260}
261
262// ============================================================================
263// YAML Format
264// ============================================================================
265
266fn format_yaml(result: &OptimizationResult) -> String {
267    serde_yaml::to_string(result).unwrap_or_else(|_| "".to_string())
268}
269
270// ============================================================================
271// Summary Format
272// ============================================================================
273
274fn format_summary(result: &OptimizationResult) -> String {
275    let mut output = String::new();
276
277    output.push_str("▶ RESOURCE OPTIMIZATION SUMMARY\n");
278    output.push_str("──────────────────────────────────────────────────\n");
279    output.push_str(&format!(
280        "│ Resources: {} ({})\n",
281        result.summary.resources_analyzed, result.metadata.mode
282    ));
283    output.push_str(&format!(
284        "│ Containers: {}\n",
285        result.summary.containers_analyzed
286    ));
287    output.push_str(&format!(
288        "│ Issues: {} over-provisioned, {} missing requests\n",
289        result.summary.over_provisioned, result.summary.missing_requests
290    ));
291    output.push_str(&format!("│ Optimal: {}\n", result.summary.optimal));
292    output.push_str(&format!(
293        "│ Analysis Time: {}ms\n",
294        result.metadata.duration_ms
295    ));
296    output.push_str("──────────────────────────────────────────────────\n");
297
298    output
299}
300
301// ============================================================================
302// Tests
303// ============================================================================
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use crate::analyzer::k8s_optimize::types::AnalysisMode;
309    use std::path::PathBuf;
310
311    #[test]
312    fn test_output_format_parse() {
313        assert_eq!(OutputFormat::parse("table"), Some(OutputFormat::Table));
314        assert_eq!(OutputFormat::parse("JSON"), Some(OutputFormat::Json));
315        assert_eq!(OutputFormat::parse("yaml"), Some(OutputFormat::Yaml));
316        assert_eq!(OutputFormat::parse("summary"), Some(OutputFormat::Summary));
317        assert_eq!(OutputFormat::parse("invalid"), None);
318    }
319
320    #[test]
321    fn test_format_json() {
322        let result = OptimizationResult::new(PathBuf::from("."), AnalysisMode::Static);
323        let json = format_json(&result);
324        assert!(json.contains("\"summary\""));
325        assert!(json.contains("\"recommendations\""));
326    }
327
328    #[test]
329    fn test_format_summary() {
330        let result = OptimizationResult::new(PathBuf::from("."), AnalysisMode::Static);
331        let summary = format_summary(&result);
332        assert!(summary.contains("RESOURCE OPTIMIZATION SUMMARY"));
333        assert!(summary.contains("Resources:"));
334    }
335
336    #[test]
337    fn test_format_table() {
338        let result = OptimizationResult::new(PathBuf::from("."), AnalysisMode::Static);
339        let table = format_table(&result);
340        assert!(table.contains("KUBERNETES RESOURCE OPTIMIZATION REPORT"));
341        assert!(table.contains("Summary"));
342    }
343}