mongosh/formatter/
json.rs

1//! JSON formatting for MongoDB documents
2//!
3//! This module provides JSON formatting with BSON type simplification:
4//! - Pretty-printed and compact JSON output
5//! - BSON type conversion to standard JSON types
6//! - Optional color highlighting for JSON output
7//! - Support for ObjectId, DateTime, Int64, Decimal128, Binary, etc.
8
9use colored_json::prelude::*;
10use mongodb::bson::{Bson, Document};
11
12use crate::error::Result;
13use crate::executor::ResultData;
14
15/// JSON formatter with pretty printing support
16pub struct JsonFormatter {
17    /// Enable pretty printing
18    pretty: bool,
19
20    /// Indentation level
21    indent: usize,
22
23    /// Enable colored output
24    use_colors: bool,
25}
26
27impl JsonFormatter {
28    /// Create a new JSON formatter
29    ///
30    /// # Arguments
31    /// * `pretty` - Enable pretty printing
32    /// * `use_colors` - Enable colored output
33    ///
34    /// # Returns
35    /// * `Self` - New formatter
36    pub fn new(pretty: bool, use_colors: bool, indent: usize) -> Self {
37        Self {
38            pretty,
39            indent: indent,
40            use_colors,
41        }
42    }
43
44    /// Format result data as JSON
45    ///
46    /// # Arguments
47    /// * `data` - Result data to format
48    ///
49    /// # Returns
50    /// * `Result<String>` - JSON string or error
51    pub fn format(&self, data: &ResultData) -> Result<String> {
52        match data {
53            ResultData::Documents(docs) => self.format_documents(docs),
54            ResultData::DocumentsWithPagination { documents, .. } => {
55                self.format_documents(documents)
56            }
57            ResultData::Document(doc) => self.format_document(doc),
58            ResultData::Message(msg) => Ok(format!("\"{}\"", msg)),
59            ResultData::List(items) => {
60                let list_str = items.join("\n");
61                Ok(list_str)
62            }
63            ResultData::InsertOne { inserted_id } => {
64                Ok(format!("{{ \"insertedId\": \"{}\" }}", inserted_id))
65            }
66            ResultData::InsertMany { inserted_ids } => {
67                let ids_json = inserted_ids
68                    .iter()
69                    .map(|id| format!("\"{}\"", id))
70                    .collect::<Vec<_>>()
71                    .join(", ");
72                Ok(format!("{{ \"insertedIds\": [{}] }}", ids_json))
73            }
74            ResultData::Update { matched, modified } => Ok(format!(
75                "{{ \"matchedCount\": {}, \"modifiedCount\": {} }}",
76                matched, modified
77            )),
78            ResultData::Delete { deleted } => Ok(format!("{{ \"deletedCount\": {} }}", deleted)),
79            ResultData::Count(count) => Ok(format!("{}", count)),
80            ResultData::None => Ok("null".to_string()),
81        }
82    }
83
84    /// Format documents as JSON array
85    ///
86    /// # Arguments
87    /// * `docs` - Documents to format
88    ///
89    /// # Returns
90    /// * `Result<String>` - JSON array string
91    fn format_documents(&self, docs: &[Document]) -> Result<String> {
92        // Convert BSON documents to simplified JSON
93        let json_docs: Vec<serde_json::Value> = docs
94            .iter()
95            .map(|doc| self.bson_to_simplified_json(doc))
96            .collect();
97
98        let json_str = if self.pretty {
99            self.to_pretty_string(&json_docs)
100                .unwrap_or_else(|_| format!("{:?}", docs))
101        } else {
102            serde_json::to_string(&json_docs).unwrap_or_else(|_| format!("{:?}", docs))
103        };
104
105        // Only apply colors for pretty-printed JSON
106        // Compact JSON should remain as-is for piping/logging
107        if self.use_colors && self.pretty {
108            Ok(json_str.to_colored_json_auto().unwrap_or(json_str))
109        } else {
110            Ok(json_str)
111        }
112    }
113
114    /// Format single document as JSON object
115    ///
116    /// # Arguments
117    /// * `doc` - Document to format
118    ///
119    /// # Returns
120    /// * `Result<String>` - JSON object string
121    pub fn format_document(&self, doc: &Document) -> Result<String> {
122        // Convert BSON document to simplified JSON
123        let json_value = self.bson_to_simplified_json(doc);
124
125        let json_str = if self.pretty {
126            self.to_pretty_string(&json_value)
127                .unwrap_or_else(|_| format!("{:?}", doc))
128        } else {
129            serde_json::to_string(&json_value).unwrap_or_else(|_| format!("{:?}", doc))
130        };
131
132        // Only apply colors for pretty-printed JSON
133        // Compact JSON should remain as-is for piping/logging
134        if self.use_colors && self.pretty {
135            Ok(json_str.to_colored_json_auto().unwrap_or(json_str))
136        } else {
137            Ok(json_str)
138        }
139    }
140
141    /// Convert a value to pretty-printed JSON with custom indentation
142    ///
143    /// # Arguments
144    /// * `value` - The value to serialize
145    ///
146    /// # Returns
147    /// * `Result<String, serde_json::Error>` - Pretty JSON string with custom indent
148    fn to_pretty_string<T: serde::Serialize>(
149        &self,
150        value: &T,
151    ) -> std::result::Result<String, serde_json::Error> {
152        let mut buf = Vec::new();
153        let indent = " ".repeat(self.indent);
154        let formatter = serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes());
155        let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
156        value.serialize(&mut ser)?;
157        Ok(String::from_utf8(buf).unwrap())
158    }
159
160    /// Convert BSON document to simplified JSON
161    ///
162    /// Converts BSON types to human-readable JSON:
163    /// - ObjectId -> String
164    /// - DateTime -> ISO 8601 String
165    /// - Int64 -> Number
166    /// - Decimal128 -> Number
167    /// - Binary -> Base64 String
168    fn bson_to_simplified_json(&self, doc: &Document) -> serde_json::Value {
169        let mut map = serde_json::Map::new();
170
171        for (key, value) in doc {
172            map.insert(key.clone(), self.bson_value_to_json(value));
173        }
174
175        serde_json::Value::Object(map)
176    }
177
178    /// Convert BSON value to simplified JSON value
179    fn bson_value_to_json(&self, value: &Bson) -> serde_json::Value {
180        use serde_json::Value as JsonValue;
181
182        match value {
183            Bson::ObjectId(oid) => JsonValue::String(oid.to_string()),
184            Bson::DateTime(dt) => {
185                let iso = dt
186                    .try_to_rfc3339_string()
187                    .unwrap_or_else(|_| format!("{}", dt.timestamp_millis()));
188                JsonValue::String(iso)
189            }
190            Bson::Int64(n) => JsonValue::Number((*n).into()),
191            Bson::Int32(n) => JsonValue::Number((*n).into()),
192            Bson::Double(f) => serde_json::Number::from_f64(*f)
193                .map(JsonValue::Number)
194                .unwrap_or(JsonValue::Null),
195            Bson::Decimal128(d) => {
196                // Convert Decimal128 to string then to number if possible
197                let s = d.to_string();
198                s.parse::<f64>()
199                    .ok()
200                    .and_then(serde_json::Number::from_f64)
201                    .map(JsonValue::Number)
202                    .unwrap_or_else(|| JsonValue::String(s))
203            }
204            Bson::String(s) => JsonValue::String(s.clone()),
205            Bson::Boolean(b) => JsonValue::Bool(*b),
206            Bson::Null => JsonValue::Null,
207            Bson::Array(arr) => {
208                let json_arr: Vec<JsonValue> =
209                    arr.iter().map(|v| self.bson_value_to_json(v)).collect();
210                JsonValue::Array(json_arr)
211            }
212            Bson::Document(doc) => self.bson_to_simplified_json(doc),
213            Bson::Binary(bin) => {
214                // Convert binary to base64 string
215                use base64::Engine;
216                let base64_str = base64::engine::general_purpose::STANDARD.encode(&bin.bytes);
217                JsonValue::String(base64_str)
218            }
219            Bson::RegularExpression(regex) => {
220                JsonValue::String(format!("/{}/{}", regex.pattern, regex.options))
221            }
222            Bson::Timestamp(ts) => {
223                // Convert timestamp to milliseconds
224                let millis = (ts.time as i64) * 1000 + (ts.increment as i64);
225                JsonValue::Number(millis.into())
226            }
227            _ => JsonValue::String(format!("{:?}", value)),
228        }
229    }
230}
231
232impl Default for JsonFormatter {
233    fn default() -> Self {
234        Self::new(true, false, 2)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use mongodb::bson::doc;
242
243    #[test]
244    fn test_json_formatter() {
245        let formatter = JsonFormatter::new(false, false, 2);
246        let doc = doc! { "name": "test", "value": 42 };
247        let result = formatter.format_document(&doc).unwrap();
248        assert!(result.contains("name"));
249        assert!(result.contains("test"));
250    }
251
252    #[test]
253    fn test_json_formatter_simplified_objectid() {
254        use mongodb::bson::oid::ObjectId;
255        let formatter = JsonFormatter::new(true, false, 2);
256        let oid = ObjectId::parse_str("65705d84dfc3f3b5094e1f72").unwrap();
257        let doc = doc! { "_id": oid };
258        let result = formatter.format_document(&doc).unwrap();
259        // Should be simplified to string, not extended JSON
260        assert!(result.contains("\"_id\""));
261        assert!(result.contains("\"65705d84dfc3f3b5094e1f72\""));
262        assert!(!result.contains("$oid"));
263    }
264
265    #[test]
266    fn test_json_formatter_simplified_datetime() {
267        use mongodb::bson::DateTime;
268        let formatter = JsonFormatter::new(true, false, 2);
269        let dt = DateTime::from_millis(1701862788373);
270        let doc = doc! { "created_time": dt };
271        let result = formatter.format_document(&doc).unwrap();
272        // Should be ISO 8601 string, not extended JSON
273        assert!(result.contains("\"created_time\""));
274        assert!(result.contains("2023-12-06"));
275        assert!(!result.contains("$date"));
276        assert!(!result.contains("$numberLong"));
277    }
278
279    #[test]
280    fn test_json_formatter_simplified_long() {
281        let formatter = JsonFormatter::new(true, false, 2);
282        let doc = doc! { "user_id": 1i64 };
283        let result = formatter.format_document(&doc).unwrap();
284        // Should be a number, not Long('1')
285        assert!(result.contains("\"user_id\""));
286        assert!(result.contains("1"));
287        assert!(!result.contains("Long"));
288    }
289
290    #[test]
291    fn test_json_formatter_complete_document() {
292        use mongodb::bson::{DateTime, oid::ObjectId};
293        let formatter = JsonFormatter::new(true, false, 2);
294        let oid = ObjectId::parse_str("65705d84dfc3f3b5094e1f72").unwrap();
295        let dt = DateTime::from_millis(1701862788373);
296        let doc = doc! {
297            "_id": oid,
298            "user_id": 1i64,
299            "nickname": "dalei",
300            "oauth2": null,
301            "created_time": dt
302        };
303        let result = formatter.format_document(&doc).unwrap();
304
305        // Verify all fields are simplified
306        assert!(result.contains("\"_id\": \"65705d84dfc3f3b5094e1f72\""));
307        assert!(result.contains("\"user_id\": 1"));
308        assert!(result.contains("\"nickname\": \"dalei\""));
309        assert!(result.contains("\"oauth2\": null"));
310        assert!(result.contains("\"created_time\": \"2023-12-06"));
311
312        // Verify no extended JSON formats
313        assert!(!result.contains("$oid"));
314        assert!(!result.contains("$date"));
315    }
316
317    #[test]
318    fn test_json_formatter_compact() {
319        let formatter = JsonFormatter::new(false, false, 2);
320        let doc = doc! { "name": "test", "value": 42 };
321        let result = formatter.format_document(&doc).unwrap();
322
323        // Compact JSON should be single line without extra whitespace
324        assert!(
325            !result.contains('\n'),
326            "Compact JSON should not contain newlines"
327        );
328        assert!(
329            !result.contains("  "),
330            "Compact JSON should not contain double spaces"
331        );
332
333        // But should still contain the data
334        assert!(result.contains("name"));
335        assert!(result.contains("test"));
336        assert!(result.contains("value"));
337    }
338
339    #[test]
340    fn test_json_formatter_compact_vs_pretty() {
341        let compact = JsonFormatter::new(false, false, 2);
342        let pretty = JsonFormatter::new(true, false, 2);
343        let doc = doc! { "a": 1, "b": 2, "c": 3 };
344
345        let compact_result = compact.format_document(&doc).unwrap();
346        let pretty_result = pretty.format_document(&doc).unwrap();
347
348        // Compact should be much shorter
349        assert!(compact_result.len() < pretty_result.len());
350
351        // Pretty should have newlines
352        assert!(pretty_result.contains('\n'));
353        assert!(!compact_result.contains('\n'));
354    }
355}