#[cfg(test)]
use mongodb::bson::Bson;
use mongodb::bson::Document;
use tabled::{
Table,
builder::Builder,
settings::{Alignment, Color, Modify, Style, object::Rows, width::Width},
};
use super::bson_utils::{BsonConverter, CompactConverter};
use crate::error::Result;
use crate::executor::ResultData;
const DEFAULT_MAX_COLUMN_WIDTH: usize = 40;
const DEFAULT_MAX_TABLE_WIDTH: usize = 150;
pub struct TableFormatter {
max_column_width: usize,
#[allow(dead_code)]
max_table_width: usize,
use_colors: bool,
converter: CompactConverter,
}
impl TableFormatter {
pub fn new() -> Self {
Self {
max_column_width: DEFAULT_MAX_COLUMN_WIDTH,
max_table_width: DEFAULT_MAX_TABLE_WIDTH,
use_colors: false,
converter: CompactConverter::new(),
}
}
pub fn format(&self, data: &ResultData) -> Result<String> {
match data {
ResultData::Documents(docs) => {
if docs.is_empty() {
return Ok("(empty result set)".to_string());
}
self.format_documents(docs)
}
ResultData::DocumentsWithPagination { documents, .. } => {
if documents.is_empty() {
return Ok("(empty result set)".to_string());
}
self.format_documents(documents)
}
ResultData::Document(doc) => self.format_documents(&[doc.clone()]),
ResultData::Message(msg) => Ok(msg.clone()),
_ => Ok(format!("{:?}", data)),
}
}
fn format_documents(&self, docs: &[Document]) -> Result<String> {
let fields = self.extract_field_names(docs);
if fields.is_empty() {
return Ok("(no fields found)".to_string());
}
let mut builder = Builder::default();
builder.push_record(fields.clone());
for doc in docs {
let row: Vec<String> = fields
.iter()
.map(|field| self.format_field_value(doc, field))
.collect();
builder.push_record(row);
}
let mut table = builder.build();
self.apply_style(&mut table);
for i in 0..fields.len() {
use tabled::settings::object::Columns;
table.with(Modify::new(Columns::new(i..=i)).with(Width::wrap(self.max_column_width)));
}
table.with(Modify::new(Rows::first()).with(Alignment::center()));
if self.use_colors {
table.modify(Rows::first(), Color::FG_CYAN | Color::BOLD);
}
Ok(table.to_string())
}
fn extract_field_names(&self, docs: &[Document]) -> Vec<String> {
let mut fields = std::collections::BTreeSet::new();
for doc in docs {
for key in doc.keys() {
fields.insert(key.clone());
}
}
let mut field_vec: Vec<String> = fields.into_iter().collect();
if let Some(pos) = field_vec.iter().position(|f| f == "_id") {
field_vec.remove(pos);
field_vec.insert(0, "_id".to_string());
}
field_vec
}
fn format_field_value(&self, doc: &Document, field: &str) -> String {
match doc.get(field) {
Some(value) => self.converter.convert(value),
None => String::from(""),
}
}
fn apply_style(&self, table: &mut Table) {
table.with(Style::modern());
}
}
impl Default for TableFormatter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use mongodb::bson::{doc, oid::ObjectId};
#[test]
fn test_table_formatter_creation() {
let formatter = TableFormatter::new();
assert_eq!(formatter.max_column_width, DEFAULT_MAX_COLUMN_WIDTH);
assert_eq!(formatter.max_table_width, DEFAULT_MAX_TABLE_WIDTH);
}
#[test]
fn test_format_empty_documents() {
let formatter = TableFormatter::new();
let docs: Vec<Document> = vec![];
let result = formatter.format(&ResultData::Documents(docs)).unwrap();
assert_eq!(result, "(empty result set)");
}
#[test]
fn test_format_single_document() {
let formatter = TableFormatter::new();
let doc = doc! {
"name": "Alice",
"age": 25
};
let result = formatter.format(&ResultData::Document(doc)).unwrap();
assert!(result.contains("name"));
assert!(result.contains("age"));
assert!(result.contains("Alice"));
assert!(result.contains("25"));
}
#[test]
fn test_format_multiple_documents() {
let formatter = TableFormatter::new();
let docs = vec![
doc! { "name": "Alice", "age": 25 },
doc! { "name": "Bob", "age": 30 },
];
let result = formatter.format(&ResultData::Documents(docs)).unwrap();
assert!(result.contains("Alice"));
assert!(result.contains("Bob"));
assert!(result.contains("25"));
assert!(result.contains("30"));
}
#[test]
fn test_extract_field_names_with_id() {
let formatter = TableFormatter::new();
let docs = vec![
doc! { "_id": 1, "name": "Alice", "age": 25 },
doc! { "_id": 2, "name": "Bob" },
];
let fields = formatter.extract_field_names(&docs);
assert_eq!(fields[0], "_id");
assert!(fields.contains(&"name".to_string()));
assert!(fields.contains(&"age".to_string()));
}
#[test]
fn test_format_bson_objectid() {
let formatter = TableFormatter::new();
let oid = ObjectId::parse_str("507f1f77bcf86cd799439011").unwrap();
let result = formatter.converter.convert(&Bson::ObjectId(oid));
assert!(result.contains("ObjectId"));
assert!(result.contains("507f1f77bcf86cd799439011"));
}
#[test]
fn test_format_bson_null() {
let formatter = TableFormatter::new();
let result = formatter.converter.convert(&Bson::Null);
assert_eq!(result, "null");
}
#[test]
fn test_format_bson_array_small() {
let formatter = TableFormatter::new();
let arr = Bson::Array(vec![Bson::Int32(1), Bson::Int32(2), Bson::Int32(3)]);
let result = formatter.converter.convert(&arr);
assert!(result.contains("[1, 2, 3]"));
}
#[test]
fn test_format_bson_array_large() {
let formatter = TableFormatter::new();
let arr = Bson::Array(vec![
Bson::Int32(1),
Bson::Int32(2),
Bson::Int32(3),
Bson::Int32(4),
Bson::Int32(5),
]);
let result = formatter.converter.convert(&arr);
assert!(result.contains("[Array(5)]"));
}
#[test]
fn test_format_bson_document_small() {
let formatter = TableFormatter::new();
let doc = Bson::Document(doc! { "x": 1 });
let result = formatter.converter.convert(&doc);
assert!(result.contains("x: 1"));
}
#[test]
fn test_format_bson_document_large() {
let formatter = TableFormatter::new();
let doc = Bson::Document(doc! { "a": 1, "b": 2, "c": 3 });
let result = formatter.converter.convert(&doc);
assert!(result.contains("{Object(3)}"));
}
#[test]
fn test_format_missing_fields() {
let formatter = TableFormatter::new();
let docs = vec![
doc! { "name": "Alice", "age": 25 },
doc! { "name": "Bob" }, ];
let result = formatter.format(&ResultData::Documents(docs)).unwrap();
assert!(result.contains("Alice"));
assert!(result.contains("Bob"));
}
#[test]
fn test_actual_table_output() {
use mongodb::bson::DateTime;
let formatter = TableFormatter::new();
let docs = vec![
doc! {
"_id": ObjectId::parse_str("65705d84dfc3f3b5094e1f72").unwrap(),
"user_id": 1i64,
"nickname": "dalei",
"oauth2": null,
"created_time": DateTime::from_millis(1701862788373),
"age": 20i64,
},
doc! {
"_id": ObjectId::parse_str("65705e2ab6204d1ed051a265").unwrap(),
"user_id": 2i64,
"nickname": "dalei",
"oauth2": null,
"created_time": DateTime::from_millis(1701862954533),
"age": 6i64,
},
];
let result = formatter.format(&ResultData::Documents(docs)).unwrap();
assert!(result.contains("_id"));
assert!(result.contains("user_id"));
assert!(result.contains("nickname"));
assert!(result.contains("oauth2"));
assert!(result.contains("created_time"));
assert!(result.contains("age"));
assert!(result.contains("dalei"));
assert!(result.contains("20"));
}
}