use colored_json::prelude::*;
use mongodb::bson::{Bson, Document};
use super::bson_utils::{BsonConverter, JsonConverter};
use crate::error::Result;
use crate::executor::ResultData;
pub struct JsonFormatter {
pretty: bool,
indent: usize,
use_colors: bool,
converter: JsonConverter,
}
impl JsonFormatter {
pub fn new(pretty: bool, use_colors: bool, indent: usize) -> Self {
Self {
pretty,
indent,
use_colors,
converter: JsonConverter::simplified(),
}
}
pub fn format(&self, data: &ResultData) -> Result<String> {
match data {
ResultData::Documents(docs) => self.format_documents(docs),
ResultData::DocumentsWithPagination { documents, .. } => {
self.format_documents(documents)
}
ResultData::Document(doc) => self.format_document(doc),
ResultData::Message(msg) => Ok(format!("\"{}\"", msg)),
ResultData::List(items) => {
let list_str = items.join("\n");
Ok(list_str)
}
ResultData::InsertOne { inserted_id } => {
Ok(format!("{{ \"insertedId\": \"{}\" }}", inserted_id))
}
ResultData::InsertMany { inserted_ids } => {
let ids_json = inserted_ids
.iter()
.map(|id| format!("\"{}\"", id))
.collect::<Vec<_>>()
.join(", ");
Ok(format!("{{ \"insertedIds\": [{}] }}", ids_json))
}
ResultData::Update { matched, modified } => Ok(format!(
"{{ \"matchedCount\": {}, \"modifiedCount\": {} }}",
matched, modified
)),
ResultData::Delete { deleted } => Ok(format!("{{ \"deletedCount\": {} }}", deleted)),
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())
}
}
}
fn format_documents(&self, docs: &[Document]) -> Result<String> {
let json_docs: Vec<serde_json::Value> = docs
.iter()
.map(|doc| self.bson_to_simplified_json(doc))
.collect();
let json_str = if self.pretty {
self.to_pretty_string(&json_docs)
.unwrap_or_else(|_| format!("{:?}", docs))
} else {
serde_json::to_string(&json_docs).unwrap_or_else(|_| format!("{:?}", docs))
};
if self.use_colors && self.pretty {
Ok(json_str.to_colored_json_auto().unwrap_or(json_str))
} else {
Ok(json_str)
}
}
pub fn format_document(&self, doc: &Document) -> Result<String> {
let json_value = self.bson_to_simplified_json(doc);
let json_str = if self.pretty {
self.to_pretty_string(&json_value)
.unwrap_or_else(|_| format!("{:?}", doc))
} else {
serde_json::to_string(&json_value).unwrap_or_else(|_| format!("{:?}", doc))
};
if self.use_colors && self.pretty {
Ok(json_str.to_colored_json_auto().unwrap_or(json_str))
} else {
Ok(json_str)
}
}
fn to_pretty_string<T: serde::Serialize>(
&self,
value: &T,
) -> std::result::Result<String, serde_json::Error> {
let mut buf = Vec::new();
let indent = " ".repeat(self.indent);
let formatter = serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes());
let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
value.serialize(&mut ser)?;
Ok(String::from_utf8(buf).unwrap())
}
fn bson_to_simplified_json(&self, doc: &Document) -> serde_json::Value {
self.converter.convert(&Bson::Document(doc.clone()))
}
}
impl Default for JsonFormatter {
fn default() -> Self {
Self::new(true, false, 2)
}
}
#[cfg(test)]
mod tests {
use super::*;
use mongodb::bson::doc;
#[test]
fn test_json_formatter() {
let formatter = JsonFormatter::new(false, false, 2);
let doc = doc! { "name": "test", "value": 42 };
let result = formatter.format_document(&doc).unwrap();
assert!(result.contains("name"));
assert!(result.contains("test"));
}
#[test]
fn test_json_formatter_simplified_objectid() {
use mongodb::bson::oid::ObjectId;
let formatter = JsonFormatter::new(true, false, 2);
let oid = ObjectId::parse_str("65705d84dfc3f3b5094e1f72").unwrap();
let doc = doc! { "_id": oid };
let result = formatter.format_document(&doc).unwrap();
assert!(result.contains("\"_id\""));
assert!(result.contains("\"65705d84dfc3f3b5094e1f72\""));
assert!(!result.contains("$oid"));
}
#[test]
fn test_json_formatter_simplified_datetime() {
use mongodb::bson::DateTime;
let formatter = JsonFormatter::new(true, false, 2);
let dt = DateTime::from_millis(1701862788373);
let doc = doc! { "created_time": dt };
let result = formatter.format_document(&doc).unwrap();
assert!(result.contains("\"created_time\""));
assert!(result.contains("2023-12-06"));
assert!(!result.contains("$date"));
assert!(!result.contains("$numberLong"));
}
#[test]
fn test_json_formatter_simplified_long() {
let formatter = JsonFormatter::new(true, false, 2);
let doc = doc! { "user_id": 1i64 };
let result = formatter.format_document(&doc).unwrap();
assert!(result.contains("\"user_id\""));
assert!(result.contains("1"));
assert!(!result.contains("Long"));
}
#[test]
fn test_json_formatter_complete_document() {
use mongodb::bson::{DateTime, oid::ObjectId};
let formatter = JsonFormatter::new(true, false, 2);
let oid = ObjectId::parse_str("65705d84dfc3f3b5094e1f72").unwrap();
let dt = DateTime::from_millis(1701862788373);
let doc = doc! {
"_id": oid,
"user_id": 1i64,
"nickname": "dalei",
"oauth2": null,
"created_time": dt
};
let result = formatter.format_document(&doc).unwrap();
assert!(result.contains("\"_id\": \"65705d84dfc3f3b5094e1f72\""));
assert!(result.contains("\"user_id\": 1"));
assert!(result.contains("\"nickname\": \"dalei\""));
assert!(result.contains("\"oauth2\": null"));
assert!(result.contains("\"created_time\": \"2023-12-06"));
assert!(!result.contains("$oid"));
assert!(!result.contains("$date"));
}
#[test]
fn test_json_formatter_compact() {
let formatter = JsonFormatter::new(false, false, 2);
let doc = doc! { "name": "test", "value": 42 };
let result = formatter.format_document(&doc).unwrap();
assert!(
!result.contains('\n'),
"Compact JSON should not contain newlines"
);
assert!(
!result.contains(" "),
"Compact JSON should not contain double spaces"
);
assert!(result.contains("name"));
assert!(result.contains("test"));
assert!(result.contains("value"));
}
#[test]
fn test_json_formatter_compact_vs_pretty() {
let compact = JsonFormatter::new(false, false, 2);
let pretty = JsonFormatter::new(true, false, 2);
let doc = doc! { "a": 1, "b": 2, "c": 3 };
let compact_result = compact.format_document(&doc).unwrap();
let pretty_result = pretty.format_document(&doc).unwrap();
assert!(compact_result.len() < pretty_result.len());
assert!(pretty_result.contains('\n'));
assert!(!compact_result.contains('\n'));
}
}