Skip to main content

fast_rich/
json.rs

1//! JSON rendering with syntax highlighting.
2//!
3//! Provides beautiful JSON output with colors for keys, strings, numbers,
4//! booleans, and null values.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use fast_rich::prelude::*;
10//! use fast_rich::json::Json;
11//!
12//! let console = Console::new();
13//! let json = Json::from_str(r#"{"name": "Alice", "age": 30}"#).unwrap();
14//! console.print_renderable(&json);
15//! ```
16
17use crate::console::RenderContext;
18use crate::highlighter::{Highlighter, JsonHighlighter};
19use crate::renderable::{Renderable, Segment};
20#[cfg(test)]
21use crate::text::Overflow;
22use crate::text::Text;
23use serde::Serialize;
24use serde_json::{self, Value};
25use std::fmt;
26use std::fs;
27use std::path::Path;
28
29/// Error type for JSON operations.
30#[derive(Debug)]
31pub enum JsonError {
32    /// Failed to parse JSON
33    Parse(serde_json::Error),
34    /// Failed to serialize data
35    Serialize(serde_json::Error),
36    /// Failed to read file
37    Io(std::io::Error),
38}
39
40impl fmt::Display for JsonError {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            JsonError::Parse(e) => write!(f, "JSON parse error: {}", e),
44            JsonError::Serialize(e) => write!(f, "JSON serialize error: {}", e),
45            JsonError::Io(e) => write!(f, "IO error: {}", e),
46        }
47    }
48}
49
50impl std::error::Error for JsonError {}
51
52impl From<std::io::Error> for JsonError {
53    fn from(e: std::io::Error) -> Self {
54        JsonError::Io(e)
55    }
56}
57
58/// Indentation style for JSON output.
59///
60/// Matches Python rich's `indent` parameter which can be `None`, `int`, or `str`.
61#[derive(Debug, Clone)]
62pub enum JsonIndent {
63    /// Compact output with no indentation or newlines
64    Compact,
65    /// Number of spaces for indentation
66    Spaces(usize),
67    /// Custom string for indentation (e.g., tab "\t")
68    Custom(String),
69}
70
71impl Default for JsonIndent {
72    fn default() -> Self {
73        JsonIndent::Spaces(2)
74    }
75}
76
77impl From<usize> for JsonIndent {
78    fn from(n: usize) -> Self {
79        if n == 0 {
80            JsonIndent::Compact
81        } else {
82            JsonIndent::Spaces(n)
83        }
84    }
85}
86
87impl From<&str> for JsonIndent {
88    fn from(s: &str) -> Self {
89        JsonIndent::Custom(s.to_string())
90    }
91}
92
93/// Options for JSON rendering, matching Python rich's JSON class.
94#[derive(Debug, Clone)]
95pub struct JsonOptions {
96    /// Indentation style (default: 2 spaces)
97    pub indent: JsonIndent,
98    /// Enable syntax highlighting (default: true)
99    pub highlight: bool,
100    /// Sort object keys alphabetically (default: false)
101    pub sort_keys: bool,
102    /// Escape all non-ASCII characters to \uXXXX (default: false)
103    pub ensure_ascii: bool,
104    /// Disable word wrapping (default: true, matching Python rich)
105    pub no_wrap: bool,
106}
107
108impl Default for JsonOptions {
109    fn default() -> Self {
110        JsonOptions {
111            indent: JsonIndent::Spaces(2),
112            highlight: true,
113            sort_keys: false,
114            ensure_ascii: false,
115            no_wrap: true, // Match Python rich's behavior
116        }
117    }
118}
119
120/// A renderable that pretty-prints JSON with syntax highlighting.
121///
122/// # Example
123///
124/// ```no_run
125/// use fast_rich::json::Json;
126///
127/// // From a JSON string
128/// let json = Json::from_str(r#"{"key": "value"}"#).unwrap();
129///
130/// // From serializable data
131/// use serde::Serialize;
132/// #[derive(Serialize)]
133/// struct User { name: String, age: u32 }
134/// let user = User { name: "Alice".into(), age: 30 };
135/// let json = Json::from_data(&user).unwrap();
136///
137/// // With options
138/// let json = Json::from_str(r#"{"z": 1, "a": 2}"#)
139///     .unwrap()
140///     .sort_keys()
141///     .indent(4)
142///     .ensure_ascii();
143///
144/// // Compact output
145/// let json = Json::from_str(r#"{"a": 1}"#).unwrap().compact();
146///
147/// // Tab indentation
148/// let json = Json::from_str(r#"{"a": 1}"#).unwrap().indent_with("\t");
149/// ```
150#[derive(Debug, Clone)]
151pub struct Json {
152    /// The formatted and highlighted text
153    text: Text,
154    /// Original parsed value for re-rendering with different options
155    value: Value,
156    /// Current options
157    options: JsonOptions,
158}
159
160impl Json {
161    /// Create a JSON renderable from a JSON string.
162    ///
163    /// # Arguments
164    ///
165    /// * `json` - A valid JSON string
166    ///
167    /// # Returns
168    ///
169    /// Returns a `Json` instance or a `JsonError` if parsing fails.
170    #[allow(clippy::should_implement_trait)]
171    pub fn from_str(json: &str) -> Result<Self, JsonError> {
172        Self::from_str_with_options(json, JsonOptions::default())
173    }
174
175    /// Create a JSON renderable from a file path.
176    ///
177    /// # Arguments
178    ///
179    /// * `path` - Path to a JSON file
180    ///
181    /// # Returns
182    ///
183    /// Returns a `Json` instance or a `JsonError` if reading/parsing fails.
184    ///
185    /// # Example
186    /// ```no_run
187    /// use fast_rich::json::Json;
188    /// let json = Json::from_file("config.json").unwrap();
189    /// ```
190    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, JsonError> {
191        let content = fs::read_to_string(path)?;
192        Self::from_str(&content)
193    }
194
195    /// Create a JSON renderable from serializable data.
196    ///
197    /// # Arguments
198    ///
199    /// * `data` - Any type implementing `Serialize`
200    ///
201    /// # Returns
202    ///
203    /// Returns a `Json` instance or a `JsonError` if serialization fails.
204    pub fn from_data<T: Serialize>(data: &T) -> Result<Self, JsonError> {
205        let value = serde_json::to_value(data).map_err(JsonError::Serialize)?;
206        Self::from_value(value, JsonOptions::default())
207    }
208
209    /// Create a JSON renderable with custom options.
210    pub fn from_str_with_options(json: &str, options: JsonOptions) -> Result<Self, JsonError> {
211        let value: Value = serde_json::from_str(json).map_err(JsonError::Parse)?;
212        Self::from_value(value, options)
213    }
214
215    /// Create a JSON renderable from a serde_json Value.
216    fn from_value(value: Value, options: JsonOptions) -> Result<Self, JsonError> {
217        let text = Self::render_value(&value, &options);
218        Ok(Json {
219            text,
220            value,
221            options,
222        })
223    }
224
225    /// Render the JSON value to styled Text.
226    fn render_value(value: &Value, options: &JsonOptions) -> Text {
227        // Sort keys if requested
228        let value_to_render = if options.sort_keys {
229            sort_json_keys(value)
230        } else {
231            value.clone()
232        };
233
234        // Format JSON
235        let formatted = format_json(&value_to_render, &options.indent, options.ensure_ascii);
236
237        // Apply highlighting
238        let mut text = if options.highlight {
239            let highlighter = JsonHighlighter::new();
240            let spans = highlighter.highlight(&formatted);
241            Text::from_spans(spans)
242        } else {
243            Text::plain(formatted)
244        };
245
246        // Apply no_wrap (match Python rich behavior)
247        if options.no_wrap {
248            text = text.no_wrap();
249        }
250
251        text
252    }
253
254    /// Re-render with updated options
255    fn rerender(&self) -> Self {
256        let text = Self::render_value(&self.value, &self.options);
257        Json {
258            text,
259            value: self.value.clone(),
260            options: self.options.clone(),
261        }
262    }
263
264    /// Set custom indentation (number of spaces).
265    ///
266    /// Use `0` for compact output, or any positive number for that many spaces.
267    ///
268    /// # Example
269    /// ```no_run
270    /// use fast_rich::json::Json;
271    /// let json = Json::from_str(r#"{"a": 1}"#).unwrap().indent(4);
272    /// ```
273    pub fn indent(mut self, spaces: usize) -> Self {
274        self.options.indent = JsonIndent::from(spaces);
275        self.rerender()
276    }
277
278    /// Set custom indentation using a string (e.g., tab).
279    ///
280    /// # Example
281    /// ```no_run
282    /// use fast_rich::json::Json;
283    /// let json = Json::from_str(r#"{"a": 1}"#).unwrap().indent_with("\t");
284    /// ```
285    pub fn indent_with(mut self, indent_str: &str) -> Self {
286        self.options.indent = JsonIndent::Custom(indent_str.to_string());
287        self.rerender()
288    }
289
290    /// Output compact JSON (no indentation, single line).
291    ///
292    /// # Example
293    /// ```no_run
294    /// use fast_rich::json::Json;
295    /// let json = Json::from_str(r#"{"a": 1, "b": 2}"#).unwrap().compact();
296    /// // Output: {"a":1,"b":2}
297    /// ```
298    pub fn compact(mut self) -> Self {
299        self.options.indent = JsonIndent::Compact;
300        self.rerender()
301    }
302
303    /// Sort object keys alphabetically.
304    ///
305    /// # Example
306    /// ```no_run
307    /// use fast_rich::json::Json;
308    /// let json = Json::from_str(r#"{"z": 1, "a": 2}"#).unwrap().sort_keys();
309    /// // Output will have keys in order: "a", "z"
310    /// ```
311    pub fn sort_keys(mut self) -> Self {
312        self.options.sort_keys = true;
313        self.rerender()
314    }
315
316    /// Escape all non-ASCII characters to \uXXXX format.
317    ///
318    /// Useful when outputting to systems that don't handle Unicode well.
319    ///
320    /// # Example
321    /// ```no_run
322    /// use fast_rich::json::Json;
323    /// let json = Json::from_str(r#"{"emoji": "😀"}"#).unwrap().ensure_ascii();
324    /// // "😀" becomes "\ud83d\ude00"
325    /// ```
326    pub fn ensure_ascii(mut self) -> Self {
327        self.options.ensure_ascii = true;
328        self.rerender()
329    }
330
331    /// Disable syntax highlighting.
332    ///
333    /// # Example
334    /// ```no_run
335    /// use fast_rich::json::Json;
336    /// let json = Json::from_str(r#"{"a": 1}"#).unwrap().no_highlight();
337    /// ```
338    pub fn no_highlight(mut self) -> Self {
339        self.options.highlight = false;
340        self.rerender()
341    }
342
343    /// Enable word wrapping (by default, wrapping is disabled).
344    ///
345    /// # Example
346    /// ```no_run
347    /// use fast_rich::json::Json;
348    /// let json = Json::from_str(r#"{"a": 1}"#).unwrap().wrap();
349    /// ```
350    pub fn wrap(mut self) -> Self {
351        self.options.no_wrap = false;
352        self.rerender()
353    }
354
355    /// Get the plain text content (useful for testing).
356    pub fn plain_text(&self) -> String {
357        self.text.plain_text()
358    }
359}
360
361impl Renderable for Json {
362    fn render(&self, context: &RenderContext) -> Vec<Segment> {
363        self.text.render(context)
364    }
365}
366
367// Error messages for JSON serialization
368const ERR_SERIALIZE_COMPACT: &str = "Failed to serialize JSON value to string";
369const ERR_SERIALIZE_FORMATTED: &str = "Failed to serialize JSON value with custom formatter";
370const ERR_INVALID_UTF8: &str = "JSON serialization produced invalid UTF-8";
371
372/// Format a JSON value with custom indentation and ASCII escaping.
373fn format_json(value: &Value, indent: &JsonIndent, ensure_ascii: bool) -> String {
374    match indent {
375        JsonIndent::Compact => {
376            // Compact output - no whitespace
377            let result = serde_json::to_string(value).expect(ERR_SERIALIZE_COMPACT);
378            if ensure_ascii {
379                escape_non_ascii(&result)
380            } else {
381                result
382            }
383        }
384        JsonIndent::Spaces(n) => {
385            let indent_bytes = " ".repeat(*n).into_bytes();
386            let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
387            let mut buf = Vec::new();
388            let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
389            value.serialize(&mut ser).expect(ERR_SERIALIZE_FORMATTED);
390            let result = String::from_utf8(buf).expect(ERR_INVALID_UTF8);
391
392            if ensure_ascii {
393                escape_non_ascii(&result)
394            } else {
395                result
396            }
397        }
398        JsonIndent::Custom(s) => {
399            let indent_bytes = s.as_bytes().to_vec();
400            let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
401            let mut buf = Vec::new();
402            let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
403            value.serialize(&mut ser).expect(ERR_SERIALIZE_FORMATTED);
404            let result = String::from_utf8(buf).expect(ERR_INVALID_UTF8);
405
406            if ensure_ascii {
407                escape_non_ascii(&result)
408            } else {
409                result
410            }
411        }
412    }
413}
414
415/// Escape non-ASCII characters to \uXXXX format.
416fn escape_non_ascii(s: &str) -> String {
417    let mut result = String::with_capacity(s.len());
418    for c in s.chars() {
419        if c.is_ascii() {
420            result.push(c);
421        } else {
422            // Encode as \uXXXX (or surrogate pairs for chars > 0xFFFF)
423            let code = c as u32;
424            if code <= 0xFFFF {
425                result.push_str(&format!("\\u{:04x}", code));
426            } else {
427                // Need surrogate pair
428                let code = code - 0x10000;
429                let high = 0xD800 + (code >> 10);
430                let low = 0xDC00 + (code & 0x3FF);
431                result.push_str(&format!("\\u{:04x}\\u{:04x}", high, low));
432            }
433        }
434    }
435    result
436}
437
438/// Recursively sort JSON object keys alphabetically.
439fn sort_json_keys(value: &Value) -> Value {
440    match value {
441        Value::Object(map) => {
442            let mut sorted: Vec<_> = map.iter().collect();
443            sorted.sort_by(|a, b| a.0.cmp(b.0));
444            let sorted_map: serde_json::Map<String, Value> = sorted
445                .into_iter()
446                .map(|(k, v)| (k.clone(), sort_json_keys(v)))
447                .collect();
448            Value::Object(sorted_map)
449        }
450        Value::Array(arr) => Value::Array(arr.iter().map(sort_json_keys).collect()),
451        _ => value.clone(),
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use std::io::Write;
459    use tempfile::NamedTempFile;
460
461    #[test]
462    fn test_json_from_str() {
463        let json = Json::from_str(r#"{"name": "Alice"}"#);
464        assert!(json.is_ok());
465    }
466
467    #[test]
468    fn test_json_invalid() {
469        let json = Json::from_str("not valid json");
470        assert!(json.is_err());
471    }
472
473    #[test]
474    fn test_json_from_data() {
475        #[derive(Serialize)]
476        struct User {
477            name: String,
478            age: u32,
479        }
480
481        let user = User {
482            name: "Bob".to_string(),
483            age: 25,
484        };
485        let json = Json::from_data(&user);
486        assert!(json.is_ok());
487        let text = json.unwrap().plain_text();
488        assert!(text.contains("Bob"));
489        assert!(text.contains("25"));
490    }
491
492    #[test]
493    fn test_json_sort_keys() {
494        let json = Json::from_str(r#"{"z": 1, "a": 2, "m": 3}"#)
495            .unwrap()
496            .sort_keys();
497        let text = json.plain_text();
498        let a_pos = text.find("\"a\"").unwrap();
499        let m_pos = text.find("\"m\"").unwrap();
500        let z_pos = text.find("\"z\"").unwrap();
501        assert!(a_pos < m_pos);
502        assert!(m_pos < z_pos);
503    }
504
505    #[test]
506    fn test_json_nested() {
507        let json = Json::from_str(r#"{"user": {"name": "Alice", "scores": [1, 2, 3]}}"#);
508        assert!(json.is_ok());
509    }
510
511    #[test]
512    fn test_json_types() {
513        let json = Json::from_str(
514            r#"{"str": "hello", "num": 42, "float": 3.14, "bool": true, "null": null}"#,
515        );
516        assert!(json.is_ok());
517    }
518
519    #[test]
520    fn test_json_indent_spaces() {
521        let json = Json::from_str(r#"{"a": 1}"#).unwrap().indent(4);
522        let text = json.plain_text();
523        // With 4-space indent, should have "    " before "a"
524        assert!(text.contains("    \"a\""));
525    }
526
527    #[test]
528    fn test_json_indent_tab() {
529        let json = Json::from_str(r#"{"a": 1}"#).unwrap().indent_with("\t");
530        let text = json.plain_text();
531        // With tab indent, should have "\t" before "a"
532        assert!(text.contains("\t\"a\""));
533    }
534
535    #[test]
536    fn test_json_compact() {
537        let json = Json::from_str(r#"{"a": 1, "b": 2}"#).unwrap().compact();
538        let text = json.plain_text();
539        // Compact output should have no newlines or extra spaces
540        assert!(!text.contains('\n'));
541        assert_eq!(text, r#"{"a":1,"b":2}"#);
542    }
543
544    #[test]
545    fn test_json_ensure_ascii() {
546        let json = Json::from_str(r#"{"emoji": "😀"}"#).unwrap().ensure_ascii();
547        let text = json.plain_text();
548        // Should have escaped emoji
549        assert!(text.contains("\\u"));
550        assert!(!text.contains("😀"));
551    }
552
553    #[test]
554    fn test_json_no_highlight() {
555        let json = Json::from_str(r#"{"a": 1}"#).unwrap().no_highlight();
556        // Should still work, just without colors
557        assert!(json.plain_text().contains("\"a\""));
558    }
559
560    #[test]
561    fn test_json_no_wrap_default() {
562        let json = Json::from_str(r#"{"a": 1}"#).unwrap();
563        // By default, no_wrap should be true (Overflow::Visible)
564        assert_eq!(json.text.overflow, Overflow::Visible);
565    }
566
567    #[test]
568    fn test_json_wrap() {
569        let json = Json::from_str(r#"{"a": 1}"#).unwrap().wrap();
570        // After calling wrap(), overflow should be Wrap
571        assert_eq!(json.text.overflow, Overflow::Wrap);
572    }
573
574    #[test]
575    fn test_json_from_file() {
576        // Create a temp file with JSON content
577        let mut temp_file = NamedTempFile::new().unwrap();
578        writeln!(temp_file, r#"{{"name": "test", "value": 42}}"#).unwrap();
579
580        let json = Json::from_file(temp_file.path());
581        assert!(json.is_ok());
582        let text = json.unwrap().plain_text();
583        assert!(text.contains("test"));
584        assert!(text.contains("42"));
585    }
586
587    #[test]
588    fn test_json_from_file_not_found() {
589        let json = Json::from_file("/nonexistent/path/to/file.json");
590        assert!(json.is_err());
591    }
592}