pub mod bson_utils;
mod colorizer;
mod json;
mod shell;
mod stats;
mod table;
pub use colorizer::Colorizer;
pub use json::JsonFormatter;
pub use shell::ShellFormatter;
pub use stats::StatsFormatter;
pub use table::TableFormatter;
use crate::config::OutputFormat;
use crate::error::Result;
use crate::executor::{ExecutionResult, ResultData};
pub struct Formatter {
format_type: OutputFormat,
colorizer: Colorizer,
use_colors: bool,
json_indent: usize,
show_timing: bool,
}
impl Formatter {
pub fn from_config(display_config: &crate::config::DisplayConfig) -> Self {
Self {
format_type: display_config.format,
colorizer: Colorizer::new(display_config.color_output),
use_colors: display_config.color_output,
json_indent: display_config.json_indent,
show_timing: display_config.show_timing,
}
}
pub fn format(&self, result: &ExecutionResult) -> Result<String> {
if !result.success {
return self.format_error(result);
}
let output = match self.format_type {
OutputFormat::Shell => self.format_shell(&result.data)?,
OutputFormat::Json => self.format_json(&result.data, false)?,
OutputFormat::JsonPretty => self.format_json(&result.data, true)?,
OutputFormat::Table => self.format_table(&result.data)?,
OutputFormat::Compact => self.format_compact(&result.data)?,
};
let stats = self.format_stats(result);
if stats.is_empty() {
Ok(output)
} else {
Ok(format!("{}\n{}", output, stats))
}
}
pub fn format_shell(&self, data: &ResultData) -> Result<String> {
let shell_formatter = ShellFormatter::new(self.use_colors);
match data {
ResultData::Documents(docs) => {
if docs.is_empty() {
return Ok("[]".to_string());
}
let mut result = String::from("[\n");
for (i, doc) in docs.iter().enumerate() {
let formatted = shell_formatter.format_document(doc);
let indented = formatted
.lines()
.map(|line| format!(" {}", line))
.collect::<Vec<_>>()
.join("\n");
result.push_str(&indented);
if i < docs.len() - 1 {
result.push_str(",\n");
} else {
result.push('\n');
}
}
result.push(']');
Ok(result)
}
ResultData::DocumentsWithPagination {
documents,
has_more,
displayed: _,
} => {
if documents.is_empty() {
return Ok("[]".to_string());
}
let mut result = String::from("[\n");
for (i, doc) in documents.iter().enumerate() {
let formatted = shell_formatter.format_document(doc);
let indented = formatted
.lines()
.map(|line| format!(" {}", line))
.collect::<Vec<_>>()
.join("\n");
result.push_str(&indented);
if i < documents.len() - 1 {
result.push_str(",\n");
} else {
result.push('\n');
}
}
result.push(']');
if *has_more {
result.push_str("\nType \"it\" for more\n");
}
Ok(result)
}
ResultData::Document(doc) => Ok(shell_formatter.format_document(doc)),
ResultData::InsertOne { inserted_id } => Ok(format!(
"{{\n acknowledged: true,\n insertedId: {}\n}}",
inserted_id
)),
ResultData::InsertMany { inserted_ids } => {
let ids_str = inserted_ids
.iter()
.enumerate()
.map(|(i, id)| format!(" '{}': {}", i, id))
.collect::<Vec<_>>()
.join(",\n");
Ok(format!(
"{{\n acknowledged: true,\n insertedIds: {{\n{}\n }}\n}}",
ids_str
))
}
ResultData::Update { matched, modified } => Ok(format!(
"{{\n acknowledged: true,\n matchedCount: {},\n modifiedCount: {}\n}}",
matched, modified
)),
ResultData::Delete { deleted } => Ok(format!(
"{{\n acknowledged: true,\n deletedCount: {}\n}}",
deleted
)),
ResultData::Message(msg) => Ok(msg.clone()),
ResultData::List(items) => Ok(items.join("\n")),
ResultData::Count(count) => Ok(format!("{}", count)),
ResultData::None => Ok("null".to_string()),
ResultData::Stream(_) => {
Err(crate::error::ExecutionError::InvalidOperation(
"Cannot format streaming query - use export instead".to_string()
).into())
}
}
}
pub fn format_json(&self, data: &ResultData, pretty: bool) -> Result<String> {
let formatter = JsonFormatter::new(pretty, self.use_colors, self.json_indent);
formatter.format(data)
}
pub fn format_table(&self, data: &ResultData) -> Result<String> {
let formatter = TableFormatter::new();
formatter.format(data)
}
pub fn format_compact(&self, data: &ResultData) -> Result<String> {
match data {
ResultData::Documents(docs) => Ok(format!("{} document(s) returned", docs.len())),
ResultData::DocumentsWithPagination {
documents,
has_more,
displayed,
} => {
let base = format!("{} document(s) returned", documents.len());
if *has_more {
Ok(format!(
"{} ({} displayed, more available)",
base, displayed
))
} else {
Ok(base)
}
}
ResultData::Document(doc) => Ok(format!("1 document: {}", doc)),
ResultData::InsertOne { .. } => Ok("Inserted 1 document".to_string()),
ResultData::InsertMany { inserted_ids } => {
Ok(format!("Inserted {} document(s)", inserted_ids.len()))
}
ResultData::Update { matched, modified } => {
Ok(format!("Matched: {}, Modified: {}", matched, modified))
}
ResultData::Delete { deleted } => Ok(format!("Deleted {} document(s)", deleted)),
ResultData::Message(msg) => Ok(msg.clone()),
ResultData::List(items) => Ok(format!("{} item(s)", items.len())),
ResultData::Count(count) => Ok(format!("Count: {}", count)),
ResultData::None => Ok("null".to_string()),
ResultData::Stream(_) => {
Err(crate::error::ExecutionError::InvalidOperation(
"Cannot format streaming query - use export instead".to_string()
).into())
}
}
}
fn format_error(&self, result: &ExecutionResult) -> Result<String> {
let unknown_error = String::from("Unknown error");
let error_msg = result.error.as_ref().unwrap_or(&unknown_error);
if self.use_colors {
Ok(self.colorizer.error(error_msg))
} else {
Ok(format!("Error: {}", error_msg))
}
}
fn format_stats(&self, result: &ExecutionResult) -> String {
let formatter = StatsFormatter::new(self.show_timing, true);
formatter.format(result)
}
}
impl Default for Formatter {
fn default() -> Self {
Self::from_config(&crate::config::DisplayConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use mongodb::bson::doc;
#[test]
fn test_formatter_creation() {
let mut config = crate::config::DisplayConfig::default();
config.format = OutputFormat::Json;
config.color_output = false;
let formatter = Formatter::from_config(&config);
assert!(!formatter.use_colors);
}
#[test]
fn test_format_compact() {
let mut config = crate::config::DisplayConfig::default();
config.format = OutputFormat::Compact;
config.color_output = false;
let formatter = Formatter::from_config(&config);
let docs = vec![doc! { "name": "test" }];
let result = formatter
.format_compact(&ResultData::Documents(docs))
.unwrap();
assert!(result.contains("1 document(s)"));
}
#[test]
fn test_format_shell_documents_as_array() {
let mut config = crate::config::DisplayConfig::default();
config.format = OutputFormat::Shell;
config.color_output = false;
let formatter = Formatter::from_config(&config);
let docs = vec![
doc! { "name": "Alice", "age": 25 },
doc! { "name": "Bob", "age": 30 },
];
let result = formatter
.format_shell(&ResultData::Documents(docs))
.unwrap();
assert!(result.starts_with("["));
assert!(result.ends_with("]"));
assert!(result.contains("'Alice'"));
assert!(result.contains("'Bob'"));
assert!(result.contains("},"));
}
#[test]
fn test_format_shell_empty_documents() {
let mut config = crate::config::DisplayConfig::default();
config.format = OutputFormat::Shell;
config.color_output = false;
let formatter = Formatter::from_config(&config);
let docs: Vec<mongodb::bson::Document> = vec![];
let result = formatter
.format_shell(&ResultData::Documents(docs))
.unwrap();
assert_eq!(result, "[]");
}
}