use prettytable::{Cell, Row, Table};
use crate::executor::QueryResult;
#[derive(Debug, Clone, Copy)]
pub enum OutputFormat {
Table,
Json,
Csv,
Markdown,
Html,
}
pub struct ResultFormatter {
format: OutputFormat,
}
impl ResultFormatter {
pub fn new() -> Self {
ResultFormatter { format: OutputFormat::Table }
}
pub fn set_format(&mut self, format: OutputFormat) {
self.format = format;
}
pub fn print_result(&self, result: &QueryResult) {
match self.format {
OutputFormat::Table => self.print_table(result),
OutputFormat::Json => self.print_json(result),
OutputFormat::Csv => self.print_csv(result),
OutputFormat::Markdown => self.print_markdown(result),
OutputFormat::Html => self.print_html(result),
}
if let Some(time_ms) = result.execution_time_ms {
println!("{} rows in set ({:.3}s)", result.row_count, time_ms / 1000.0);
} else {
println!("{} rows", result.row_count);
}
}
fn print_table(&self, result: &QueryResult) {
if result.columns.is_empty() {
return;
}
let mut table = Table::new();
let header_cells: Vec<Cell> = result.columns.iter().map(|col| Cell::new(col)).collect();
table.add_row(Row::new(header_cells));
for row in &result.rows {
let cells: Vec<Cell> = row.iter().map(|val| Cell::new(val)).collect();
table.add_row(Row::new(cells));
}
table.printstd();
}
fn print_json(&self, result: &QueryResult) {
let mut json_rows = Vec::new();
for row in &result.rows {
let mut json_obj = serde_json::Map::new();
for (i, col) in result.columns.iter().enumerate() {
if i < row.len() {
json_obj.insert(col.clone(), serde_json::Value::String(row[i].clone()));
}
}
json_rows.push(serde_json::Value::Object(json_obj));
}
let output = serde_json::to_string_pretty(&json_rows).unwrap_or_else(|_| "[]".to_string());
println!("{}", output);
}
fn print_csv(&self, result: &QueryResult) {
println!("{}", result.columns.join(","));
for row in &result.rows {
println!("{}", row.join(","));
}
}
fn print_markdown(&self, result: &QueryResult) {
if result.columns.is_empty() {
return;
}
print!("|");
for col in &result.columns {
print!(" {} |", col);
}
println!();
print!("|");
for _ in &result.columns {
print!("---|");
}
println!();
for row in &result.rows {
print!("|");
for val in row {
print!(" {} |", val);
}
println!();
}
}
fn print_html(&self, result: &QueryResult) {
if result.columns.is_empty() {
println!("<table></table>");
return;
}
println!("<table>");
println!(" <thead>");
println!(" <tr>");
for col in &result.columns {
println!(" <th>{}</th>", Self::escape_html(col));
}
println!(" </tr>");
println!(" </thead>");
println!(" <tbody>");
for row in &result.rows {
println!(" <tr>");
for val in row {
println!(" <td>{}</td>", Self::escape_html(val));
}
println!(" </tr>");
}
println!(" </tbody>");
println!("</table>");
}
fn escape_html(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_result() -> QueryResult {
QueryResult {
columns: vec!["id".to_string(), "name".to_string()],
rows: vec![
vec!["1".to_string(), "Alice".to_string()],
vec!["2".to_string(), "Bob".to_string()],
],
row_count: 2,
execution_time_ms: None,
}
}
#[test]
fn test_html_escaping() {
assert_eq!(
ResultFormatter::escape_html("<script>alert('xss')</script>"),
"<script>alert('xss')</script>"
);
assert_eq!(ResultFormatter::escape_html("A & B"), "A & B");
}
#[test]
fn test_markdown_format() {
let formatter = ResultFormatter::new();
let result = create_test_result();
formatter.print_markdown(&result);
}
#[test]
fn test_html_format() {
let formatter = ResultFormatter::new();
let result = create_test_result();
formatter.print_html(&result);
}
#[test]
fn test_empty_result() {
let formatter = ResultFormatter::new();
let result =
QueryResult { columns: vec![], rows: vec![], row_count: 0, execution_time_ms: None };
formatter.print_markdown(&result);
formatter.print_html(&result);
}
}