use crate::config::{REPORTER_FORMAT_JSON, REPORTER_FORMAT_TABLE};
use crate::entity::LanguageReport;
use std::io;
use tabled::builder::Builder;
use tabled::settings::{object::Rows, Alignment, Modify, Style};
pub struct LanguageReporter;
impl Default for LanguageReporter {
fn default() -> Self {
Self::new()
}
}
impl LanguageReporter {
pub fn new() -> Self {
Self
}
pub fn validate_format(format: &str) -> io::Result<()> {
match format {
REPORTER_FORMAT_JSON | REPORTER_FORMAT_TABLE => Ok(()),
_ => Err(io::Error::other(format!(
"Unsupported reporter format: '{}'. Supported formats: {}, {}.",
format, REPORTER_FORMAT_TABLE, REPORTER_FORMAT_JSON
))),
}
}
pub fn output(&self, report: &LanguageReport, format: &str) -> io::Result<()> {
Self::validate_format(format)?;
let output_string = match format {
REPORTER_FORMAT_JSON => self.to_json(report)?,
REPORTER_FORMAT_TABLE => self.to_table(report)?,
_ => unreachable!("Format validation should have caught this"),
};
println!("{}", output_string);
Ok(())
}
fn to_json(&self, report: &LanguageReport) -> io::Result<String> {
serde_json::to_string_pretty(report)
.map_err(|e| io::Error::other(format!("JSON serialization error: {}", e)))
}
fn to_table(&self, report: &LanguageReport) -> io::Result<String> {
let mut output = Vec::new();
let detected_files_count: u64 = report
.languages
.iter()
.map(|lang_report| lang_report.file_count)
.sum();
let excluded_files_count = report.total_file_count.saturating_sub(detected_files_count);
let mut summary_builder = Builder::default();
summary_builder.push_record(vec!["Item", "Value"]);
summary_builder.push_record(vec!["Directory", &report.dir]);
summary_builder.push_record(vec!["Total Files", &report.total_file_count.to_string()]);
summary_builder.push_record(vec!["Language Files", &detected_files_count.to_string()]);
summary_builder.push_record(vec!["Excluded Files", &excluded_files_count.to_string()]);
let summary_table = summary_builder
.build()
.with(Style::sharp())
.with(Modify::new(Rows::new(0..=0)).with(Alignment::center()))
.to_string();
let mut lang_builder = Builder::default();
lang_builder.push_record(vec!["Language", "Files", "Percentage"]);
for lang_report in &report.languages {
let percentage =
Self::calculate_percentage(lang_report.file_count, detected_files_count);
lang_builder.push_record(vec![
lang_report.language.name,
&lang_report.file_count.to_string(),
&format!("{:.1}%", percentage),
]);
}
let last_row = report.languages.len() + 1; let lang_table = lang_builder
.build()
.with(Style::sharp())
.with(Modify::new(Rows::new(last_row..=last_row)).with(Alignment::center()))
.to_string();
output.push("=== Scan Summary ===".to_string());
output.push(summary_table);
output.push(String::new());
output.push("=== Language Statistics ===".to_string());
output.push(lang_table);
Ok(output.join("\n"))
}
fn calculate_percentage(file_count: u64, detected_total_files_count: u64) -> f64 {
if detected_total_files_count > 0 {
(file_count as f64 / detected_total_files_count as f64) * 100.0
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entity::{Language, LanguageReportItem};
fn create_test_report() -> LanguageReport {
let language = Language {
name: "Rust",
exts: &["rs"],
};
let language_report = LanguageReportItem {
language,
file_count: 5,
file_paths: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()],
};
LanguageReport {
dir: "/test/path".to_string(),
total_file_count: 5,
languages: vec![language_report],
}
}
#[test]
fn test_validate_format_valid() {
assert!(LanguageReporter::validate_format("table").is_ok());
}
#[test]
fn test_validate_format_invalid() {
let result = LanguageReporter::validate_format("xml");
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert_eq!(
error_msg,
"Unsupported reporter format: 'xml'. Supported formats: table, json."
);
}
#[test]
fn test_to_json_success() {
let reporter = LanguageReporter::new();
let report = create_test_report();
let result = reporter.to_json(&report);
assert!(result.is_ok());
let json_string = result.unwrap();
assert!(json_string.contains("\"dir\": \"/test/path\""));
assert!(json_string.contains("\"total_file_count\": 5"));
assert!(json_string.contains("\"name\": \"Rust\""));
assert!(json_string.contains("\"file_count\": 5"));
let parsed: serde_json::Value =
serde_json::from_str(&json_string).expect("Generated JSON is invalid");
assert_eq!(parsed["dir"], "/test/path");
assert_eq!(parsed["total_file_count"], 5);
}
#[test]
fn test_percentage_excludes_unknown_files() {
let reporter = LanguageReporter::new();
let rust_lang = Language {
name: "Rust",
exts: &["rs"],
};
let js_lang = Language {
name: "JavaScript",
exts: &["js"],
};
let report = LanguageReport {
dir: "/test/path".to_string(),
total_file_count: 100,
languages: vec![
LanguageReportItem {
language: rust_lang,
file_count: 30,
file_paths: vec![],
},
LanguageReportItem {
language: js_lang,
file_count: 20,
file_paths: vec![],
},
],
};
let result = reporter.to_table(&report);
assert!(result.is_ok());
let table_output = result.unwrap();
assert!(table_output.contains("60.0%"));
assert!(table_output.contains("40.0%"));
}
#[test]
fn test_calculate_percentage() {
assert_eq!(LanguageReporter::calculate_percentage(30, 100), 30.0);
assert!((LanguageReporter::calculate_percentage(1, 3) - 33.333333333333336).abs() < 0.0001);
assert_eq!(LanguageReporter::calculate_percentage(5, 0), 0.0);
assert_eq!(LanguageReporter::calculate_percentage(0, 100), 0.0);
assert_eq!(LanguageReporter::calculate_percentage(50, 50), 100.0);
}
}