use crate::config::BatlessConfig;
use crate::error::{BatlessError, BatlessResult};
use crate::file_info::FileInfo;
use serde_json::json;
pub struct OutputFormatter;
impl OutputFormatter {
pub fn format_output(
file_info: &FileInfo,
file_path: &str,
config: &BatlessConfig,
output_mode: OutputMode,
) -> BatlessResult<String> {
use crate::formatters::Formatter;
use crate::formatters::{
ast_formatter::AstFormatter, index_formatter::IndexFormatter,
json_formatter::JsonFormatter, plain_formatter::PlainFormatter,
summary_formatter::SummaryFormatter,
};
match output_mode {
OutputMode::Plain => PlainFormatter.format(file_info, file_path, config),
OutputMode::Json => JsonFormatter.format(file_info, file_path, config),
OutputMode::Summary => SummaryFormatter.format(file_info, file_path, config),
OutputMode::Index => IndexFormatter.format(file_info, file_path, config),
OutputMode::Ast => AstFormatter.format(file_info, file_path, config),
}
}
pub fn format_line(
line: &str,
line_number: usize,
_file_path: &str,
_config: &BatlessConfig,
output_mode: OutputMode,
) -> BatlessResult<String> {
match output_mode {
OutputMode::Plain => Ok(line.to_string()),
OutputMode::Json => {
let json_line = json!({
"line_number": line_number,
"content": line
});
serde_json::to_string(&json_line).map_err(BatlessError::from)
}
OutputMode::Summary => Ok(line.to_string()), OutputMode::Index => Ok(line.to_string()), OutputMode::Ast => Ok(line.to_string()), }
}
pub fn format_compact_json(file_info: &FileInfo, file_path: &str) -> BatlessResult<String> {
let json_data = json!({
"path": file_path,
"lines": file_info.total_lines,
"bytes": file_info.total_bytes,
"language": file_info.language,
"truncated": file_info.truncated,
"content": file_info.lines
});
serde_json::to_string(&json_data).map_err(BatlessError::from)
}
pub fn format_error(error: &BatlessError, file_path: &str, output_mode: OutputMode) -> String {
match output_mode {
OutputMode::Json => {
let error_json = json!({
"error": true,
"file_path": file_path,
"error_type": Self::error_type_name(error),
"message": error.to_string()
});
serde_json::to_string_pretty(&error_json)
.unwrap_or_else(|_| format!("{{\"error\": true, \"message\": \"{error}\"}}"))
}
_ => format!("Error processing {file_path}: {error}"),
}
}
fn error_type_name(error: &BatlessError) -> &'static str {
match error {
BatlessError::FileNotFound { .. } => "file_not_found",
BatlessError::FileReadError { .. } => "file_read_error",
BatlessError::PermissionDenied { .. } => "permission_denied",
BatlessError::LanguageNotFound { .. } => "language_not_found",
BatlessError::LanguageDetectionError { .. } => "language_detection_error",
BatlessError::EncodingError { .. } => "encoding_error",
BatlessError::ProcessingError { .. } => "processing_error",
BatlessError::ConfigurationError { .. } => "configuration_error",
BatlessError::JsonSerializationError(_) => "json_serialization_error",
BatlessError::OutputError(_) => "output_error",
BatlessError::IoError(_) => "io_error",
}
}
pub fn format_metadata_only(file_info: &FileInfo, file_path: &str) -> BatlessResult<String> {
let metadata = json!({
"file_path": file_path,
"total_lines": file_info.total_lines,
"total_lines_exact": file_info.total_lines_exact,
"total_bytes": file_info.total_bytes,
"language": file_info.language,
"encoding": file_info.encoding,
"truncated": file_info.truncated,
"truncation_reason": file_info.truncation_reason(),
"has_syntax_errors": !file_info.syntax_errors.is_empty(),
"error_count": file_info.syntax_errors.len(),
"token_count": file_info.token_count(),
"tokens_truncated": file_info.tokens_truncated(),
"summary_line_count": file_info.summary_line_count(),
"processing_ratio": file_info.processing_ratio()
});
serde_json::to_string_pretty(&metadata).map_err(BatlessError::from)
}
pub fn format_stats_report(
file_info: &FileInfo,
file_path: &str,
processing_time_ms: u128,
) -> String {
let stats = file_info.get_stats_summary();
format!(
r"File Processing Statistics
==========================
File: {}
Language: {}
Encoding: {}
Total Lines: {}
Total Lines Exact: {}
Processed Lines: {}
Total Bytes: {}
Processing Time: {}ms
Truncated: {}
Syntax Errors: {}
Tokens: {}
Tokens Truncated: {}
Summary Lines: {}
Processing Ratio: {:.2}%",
file_path,
stats.language.as_deref().unwrap_or("Unknown"),
stats.encoding,
stats.total_lines,
if stats.total_lines_exact { "Yes" } else { "No" },
stats.processed_lines,
stats.total_bytes,
processing_time_ms,
if stats.truncated { "Yes" } else { "No" },
stats.error_count,
stats.token_count,
if stats.tokens_truncated { "Yes" } else { "No" },
stats.summary_line_count,
file_info.processing_ratio() * 100.0
)
}
pub fn format_file_table(file_results: &[(String, Result<FileInfo, BatlessError>)]) -> String {
let mut table = Vec::new();
table.push(format!(
"{:<30} {:<10} {:<8} {:<8} {:<12} {:<10}",
"File", "Language", "Lines", "Bytes", "Status", "Truncated"
));
table.push("-".repeat(80));
for (file_path, result) in file_results {
let row = match result {
Ok(info) => format!(
"{:<30} {:<10} {:<8} {:<8} {:<12} {:<10}",
Self::truncate_path(file_path, 30),
info.language.as_deref().unwrap_or("Unknown"),
info.total_lines,
info.total_bytes,
if info.is_success() {
"Success"
} else {
"Errors"
},
if info.truncated { "Yes" } else { "No" }
),
Err(_error) => format!(
"{:<30} {:<10} {:<8} {:<8} {:<12} {:<10}",
Self::truncate_path(file_path, 30),
"-",
"-",
"-",
"Error",
"-"
),
};
table.push(row);
}
table.join("\n")
}
fn truncate_path(path: &str, max_length: usize) -> String {
if path.len() <= max_length {
path.to_string()
} else {
format!("...{}", &path[path.len() - (max_length - 3)..])
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
Plain,
Json,
Summary,
Index,
Ast,
}
impl OutputMode {
pub fn parse_mode(s: &str) -> Result<Self, String> {
match s.to_lowercase().as_str() {
"plain" => Ok(OutputMode::Plain),
"json" => Ok(OutputMode::Json),
"summary" => Ok(OutputMode::Summary),
"index" => Ok(OutputMode::Index),
"ast" => Ok(OutputMode::Ast),
_ => Err(format!("Unknown output mode: {s}")),
}
}
pub fn all() -> Vec<Self> {
vec![
OutputMode::Plain,
OutputMode::Json,
OutputMode::Summary,
OutputMode::Index,
OutputMode::Ast,
]
}
pub fn as_str(&self) -> &'static str {
match self {
OutputMode::Plain => "plain",
OutputMode::Json => "json",
OutputMode::Summary => "summary",
OutputMode::Index => "index",
OutputMode::Ast => "ast",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::BatlessConfig;
use serde_json::Value;
fn create_test_file_info() -> FileInfo {
FileInfo::with_metadata(10, 256, Some("Rust".to_string()), "UTF-8".to_string())
.with_lines(vec![
"fn main() {".to_string(),
" println!(\"Hello\");".to_string(),
"}".to_string(),
])
.with_tokens(Some(vec!["fn".to_string(), "main".to_string()]))
}
#[test]
fn test_format_plain() -> BatlessResult<()> {
let file_info = create_test_file_info();
let config = crate::config::BatlessConfig::new();
let result =
OutputFormatter::format_output(&file_info, "test.rs", &config, OutputMode::Plain)?;
assert_eq!(result, "fn main() {\n println!(\"Hello\");\n}");
Ok(())
}
#[test]
fn test_format_json() -> BatlessResult<()> {
let file_info = create_test_file_info();
let config = BatlessConfig::default();
let result =
OutputFormatter::format_output(&file_info, "test.rs", &config, OutputMode::Json)?;
let parsed: Value = serde_json::from_str(&result)?;
assert!(parsed["file"].as_str().unwrap() == "test.rs");
assert_eq!(parsed["processed_lines"].as_u64().unwrap(), 3);
assert_eq!(parsed["total_lines"].as_u64().unwrap(), 10);
assert!(parsed["lines"].is_array());
Ok(())
}
#[test]
fn test_format_summary() -> BatlessResult<()> {
let file_info = create_test_file_info();
let config = BatlessConfig::default();
let result =
OutputFormatter::format_output(&file_info, "test.rs", &config, OutputMode::Summary)?;
assert!(result.contains("=== File Summary ==="));
assert!(result.contains("Language: Rust"));
assert!(result.contains("Total Lines: 10"));
Ok(())
}
#[test]
fn test_format_compact_json() -> BatlessResult<()> {
let file_info = create_test_file_info();
let result = OutputFormatter::format_compact_json(&file_info, "test.rs")?;
let parsed: Value = serde_json::from_str(&result)?;
assert!(parsed["path"].as_str().unwrap() == "test.rs");
assert!(parsed["lines"].as_u64().unwrap() == 10);
Ok(())
}
#[test]
fn test_format_error() {
let error = BatlessError::FileNotFound {
path: "test.txt".to_string(),
suggestions: vec![],
};
let json_result = OutputFormatter::format_error(&error, "test.txt", OutputMode::Json);
assert!(json_result.contains("\"error\": true"));
assert!(json_result.contains("\"error_type\": \"file_not_found\""));
let plain_result = OutputFormatter::format_error(&error, "test.txt", OutputMode::Plain);
assert!(plain_result.contains("Error processing test.txt"));
}
#[test]
fn test_error_type_name_theme_removed() {
let error = BatlessError::LanguageNotFound {
language: "xyz".to_string(),
suggestions: vec![],
};
let json_result = OutputFormatter::format_error(&error, "test.txt", OutputMode::Json);
assert!(json_result.contains("\"error_type\": \"language_not_found\""));
}
#[test]
fn test_output_mode_parsing() {
assert_eq!(OutputMode::parse_mode("plain").unwrap(), OutputMode::Plain);
assert_eq!(OutputMode::parse_mode("json").unwrap(), OutputMode::Json);
assert_eq!(OutputMode::parse_mode("ast").unwrap(), OutputMode::Ast);
assert!(OutputMode::parse_mode("highlight").is_err());
assert!(OutputMode::parse_mode("invalid").is_err());
}
#[test]
fn test_output_mode_string_conversion() {
assert_eq!(OutputMode::Plain.as_str(), "plain");
assert_eq!(OutputMode::Json.as_str(), "json");
assert_eq!(OutputMode::Summary.as_str(), "summary");
assert_eq!(OutputMode::Index.as_str(), "index");
assert_eq!(OutputMode::Ast.as_str(), "ast");
}
#[test]
fn test_format_metadata_only() -> BatlessResult<()> {
let file_info = create_test_file_info();
let result = OutputFormatter::format_metadata_only(&file_info, "test.rs")?;
let parsed: Value = serde_json::from_str(&result)?;
assert!(parsed["file_path"].as_str().unwrap() == "test.rs");
assert!(parsed["total_lines"].as_u64().unwrap() == 10);
assert!(parsed["content"].is_null());
Ok(())
}
#[test]
fn test_format_stats_report() {
let file_info = create_test_file_info();
let result = OutputFormatter::format_stats_report(&file_info, "test.rs", 42);
assert!(result.contains("File Processing Statistics"));
assert!(result.contains("File: test.rs"));
assert!(result.contains("Processing Time: 42ms"));
assert!(result.contains("Language: Rust"));
}
#[test]
fn test_format_file_table() {
let file_info = create_test_file_info();
let results = vec![
("test.rs".to_string(), Ok(file_info)),
(
"error.txt".to_string(),
Err(BatlessError::FileNotFound {
path: "error.txt".to_string(),
suggestions: vec![],
}),
),
];
let table = OutputFormatter::format_file_table(&results);
assert!(table.contains("File"));
assert!(table.contains("Language"));
assert!(table.contains("test.rs"));
assert!(table.contains("error.txt"));
assert!(table.contains("Success"));
assert!(table.contains("Error"));
}
#[test]
fn test_truncate_path() {
assert_eq!(OutputFormatter::truncate_path("short.txt", 20), "short.txt");
assert_eq!(
OutputFormatter::truncate_path("very/long/path/to/file.txt", 15),
".../to/file.txt"
);
}
#[test]
fn test_error_type_names() {
assert_eq!(
OutputFormatter::error_type_name(&BatlessError::FileNotFound {
path: "test".to_string(),
suggestions: vec![],
}),
"file_not_found"
);
assert_eq!(
OutputFormatter::error_type_name(&BatlessError::LanguageNotFound {
language: "test".to_string(),
suggestions: vec![],
}),
"language_not_found"
);
}
}