mongosh 0.9.0

A high-performance MongoDB Shell implementation in Rust
Documentation
//! Output formatting and colorization for mongosh
//!
//! This module provides formatting functionality for command execution results.
//!
//! # Supported Formats
//!
//! - **Shell**: MongoDB shell-compatible format with type wrappers (default)
//!   - ObjectId('...'), ISODate('...'), Long('...')
//!   - Pretty-printed nested documents and arrays
//!   - Optional color highlighting
//!
//! - **Json**: Compact single-line JSON
//!   - Minified output without whitespace
//!   - Suitable for logging and piping
//!
//! - **JsonPretty**: Human-readable multi-line JSON
//!   - Indented and formatted
//!   - Suitable for terminal display and debugging
//!
//! - **Table**: ASCII table layout (TODO: full implementation)
//!   - Displays documents as structured tables
//!   - Suitable for comparing multiple documents
//!
//! - **Compact**: Summary format
//!   - Shows only count/summary, not full content
//!   - Example: "5 document(s) returned"
//!
//! # Module Structure
//!
//! - `colorizer`: ANSI color support for terminal output
//! - `shell`: Shell-style formatter (mongosh compatible)
//! - `json`: JSON formatter with BSON type simplification
//! - `table`: Table formatter for document collections
//! - `stats`: Statistics formatter for execution metrics

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};

/// Main formatter for execution results
pub struct Formatter {
    /// Output format type
    format_type: OutputFormat,

    /// Colorizer for syntax highlighting
    colorizer: Colorizer,

    /// Enable colored output
    use_colors: bool,

    /// JSON indentation (number of spaces)
    json_indent: usize,

    /// Show execution timing
    show_timing: bool,
}

impl Formatter {
    /// Create a new formatter from display configuration.
    ///
    /// This is the recommended way to create a formatter as it ensures
    /// all display settings from the configuration are properly applied.
    ///
    /// # Arguments
    /// * `display_config` - Display configuration settings
    ///
    /// # Returns
    /// * `Self` - New formatter instance
    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,
        }
    }

    /// Format execution result according to configured format
    ///
    /// # Arguments
    /// * `result` - Execution result to format
    ///
    /// # Returns
    /// * `Result<String>` - Formatted output or error
    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)?,
        };

        // Append statistics if enabled
        let stats = self.format_stats(result);
        if stats.is_empty() {
            Ok(output)
        } else {
            Ok(format!("{}\n{}", output, stats))
        }
    }

    /// Format result data as Shell format
    ///
    /// # Arguments
    /// * `data` - Result data to format
    ///
    /// # Returns
    /// * `Result<String>` - Shell formatted string or error
    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);
                    // Indent each document
                    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);
                    // Indent each document
                    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(']');

                // Add pagination info
                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(_) => {
                // Streaming queries should not reach formatter - they're consumed by export
                Err(crate::error::ExecutionError::InvalidOperation(
                    "Cannot format streaming query - use export instead".to_string()
                ).into())
            }
        }
    }

    /// Format result data as JSON
    ///
    /// # Arguments
    /// * `data` - Result data to format
    /// * `pretty` - Enable pretty printing
    ///
    /// # Returns
    /// * `Result<String>` - JSON string or error
    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)
    }

    /// Format result data as table
    ///
    /// # Arguments
    /// * `data` - Result data to format
    ///
    /// # Returns
    /// * `Result<String>` - Table string or error
    pub fn format_table(&self, data: &ResultData) -> Result<String> {
        let formatter = TableFormatter::new();
        formatter.format(data)
    }

    /// Format result data in compact form
    ///
    /// # Arguments
    /// * `data` - Result data to format
    ///
    /// # Returns
    /// * `Result<String>` - Compact string or error
    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(_) => {
                // Streaming queries should not reach formatter - they're consumed by export
                Err(crate::error::ExecutionError::InvalidOperation(
                    "Cannot format streaming query - use export instead".to_string()
                ).into())
            }
        }
    }

    /// Format error result
    ///
    /// # Arguments
    /// * `result` - Execution result with error
    ///
    /// # Returns
    /// * `Result<String>` - Formatted error message
    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))
        }
    }

    /// Format execution statistics
    ///
    /// # Arguments
    /// * `result` - Execution result
    ///
    /// # Returns
    /// * `String` - Formatted statistics
    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();

        // Should start with [ and end with ]
        assert!(result.starts_with("["));
        assert!(result.ends_with("]"));

        // Should contain both documents
        assert!(result.contains("'Alice'"));
        assert!(result.contains("'Bob'"));

        // Should be comma separated
        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, "[]");
    }
}