Skip to main content

json_colorizer/
lib.rs

1//! # json-colorizer
2//!
3//! A fast, lightweight JSON formatter, pretty-printer, colorizer, and query library for Rust.
4//!
5//! Use it to pretty-print JSON with syntax highlighting in the terminal,
6//! compact-print JSON, or query nested values with dot-path notation.
7//!
8//! ## Quick Start
9//!
10//! ```rust
11//! use json_colorizer::{format_json, format_json_compact, query, FormatOptions};
12//! use serde_json::json;
13//!
14//! let value = json!({"name": "Alice", "scores": [95, 87, 100]});
15//!
16//! // Pretty-print with colors
17//! let output = format_json(&value, &FormatOptions::default());
18//! println!("{}", output);
19//!
20//! // Compact output
21//! let compact = format_json_compact(&value);
22//! println!("{}", compact);
23//!
24//! // Query a nested value
25//! let score = query(&value, ".scores[0]").unwrap();
26//! assert_eq!(score, vec![&json!(95)]);
27//! ```
28
29use colored::{Color, Colorize};
30use serde::Serialize;
31use serde_json::ser::{Formatter, PrettyFormatter};
32use serde_json::Value;
33use std::io::{self, Write};
34
35// ═══════════════════════════════════════════════════════════════
36//  Public types
37// ═══════════════════════════════════════════════════════════════
38
39/// Options for controlling JSON output formatting.
40#[derive(Debug, Clone)]
41pub struct FormatOptions {
42    /// Number of spaces per indentation level (default: 2).
43    pub indent: usize,
44    /// Whether to colorize the output (default: true).
45    pub color: bool,
46    /// Whether to sort object keys (default: false).
47    pub sort_keys: bool,
48    /// Color theme for the output.
49    pub theme: Theme,
50}
51
52impl Default for FormatOptions {
53    fn default() -> Self {
54        Self {
55            indent: 2,
56            color: true,
57            sort_keys: false,
58            theme: Theme::default(),
59        }
60    }
61}
62
63/// Colors for different JSON components.
64#[derive(Debug, Clone)]
65pub struct Theme {
66    pub key: Color,
67    pub string: Color,
68    pub number: Color,
69    pub boolean: Color,
70    pub null: Color,
71}
72
73impl Default for Theme {
74    fn default() -> Self {
75        Self {
76            key: Color::Cyan,
77            string: Color::Green,
78            number: Color::Yellow,
79            boolean: Color::Magenta,
80            null: Color::BrightBlack, // Dimmed null
81        }
82    }
83}
84
85/// Error returned when a query path is invalid or does not match the JSON.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum QueryError {
88    /// A key was not found in an object.
89    KeyNotFound(String),
90    /// An index was out of bounds in an array.
91    IndexOutOfBounds(usize),
92    /// The query string could not be parsed.
93    InvalidQuery(String),
94}
95
96impl std::fmt::Display for QueryError {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        match self {
99            QueryError::KeyNotFound(key) => write!(f, "key '{}' not found", key),
100            QueryError::IndexOutOfBounds(idx) => write!(f, "index [{}] out of bounds", idx),
101            QueryError::InvalidQuery(msg) => write!(f, "invalid query: {}", msg),
102        }
103    }
104}
105
106impl std::error::Error for QueryError {}
107
108// ═══════════════════════════════════════════════════════════════
109//  Core public API
110// ═══════════════════════════════════════════════════════════════
111
112pub fn format_json(value: &Value, opts: &FormatOptions) -> String {
113    let mut writer = Vec::new();
114    let indent_buf = vec![b' '; opts.indent];
115
116    if opts.color {
117        let formatter = ColorFormatter::new(&indent_buf, &opts.theme);
118        let mut ser = serde_json::Serializer::with_formatter(&mut writer, formatter);
119        value.serialize(&mut ser).unwrap();
120    } else {
121        let formatter = PrettyFormatter::with_indent(&indent_buf);
122        let mut ser = serde_json::Serializer::with_formatter(&mut writer, formatter);
123        value.serialize(&mut ser).unwrap();
124    }
125
126    String::from_utf8(writer).unwrap_or_default()
127}
128
129/// Format a JSON value as a compact (single-line, no whitespace) string.
130///
131/// ```rust
132/// use json_colorizer::format_json_compact;
133/// use serde_json::json;
134///
135/// let val = json!({"a": 1});
136/// assert_eq!(format_json_compact(&val), r#"{"a":1}"#);
137/// ```
138pub fn format_json_compact(value: &Value) -> String {
139    serde_json::to_string(value).unwrap_or_default()
140}
141
142/// Query a nested value inside `root` using a dot-path string.
143///
144/// Supported syntax:
145/// - `.key` — object key access
146/// - `[n]` — array index access
147/// - Chaining: `.data.users[0].name`
148///
149/// Query a nested value (or values) inside `root` using a dot-path string.
150///
151/// Supported syntax:
152/// - `.key` or `."quoted key"` — object key access
153/// - `[n]` — array index access (0-based)
154/// - `.*` — all values in an object
155/// - `[]` — all elements in an array
156/// - `[start:end]` — array slice (exclusive end)
157///
158/// Returns a vector of references to matching values.
159///
160/// ```rust
161/// use json_colorizer::query;
162/// use serde_json::json;
163///
164/// let data = json!({"users": [{"name": "Alice"}, {"name": "Bob"}]});
165/// let results = query(&data, ".users[].name").unwrap();
166/// assert_eq!(results.len(), 2);
167/// assert_eq!(results[0], &json!("Alice"));
168/// assert_eq!(results[1], &json!("Bob"));
169/// ```
170pub fn query<'a>(root: &'a Value, path: &str) -> Result<Vec<&'a Value>, QueryError> {
171    let segments = parse_query(path)?;
172    let mut current_matches = vec![root];
173
174    for seg in &segments {
175        let mut next_matches = Vec::new();
176        for val in current_matches {
177            match seg {
178                Segment::Key(key) => {
179                    if let Some(v) = val.get(key) {
180                        next_matches.push(v);
181                    }
182                }
183                Segment::Index(idx) => {
184                    if let Some(v) = val.get(*idx) {
185                        next_matches.push(v);
186                    }
187                }
188                Segment::Wildcard => {
189                    if let Some(obj) = val.as_object() {
190                        for v in obj.values() {
191                            next_matches.push(v);
192                        }
193                    } else if let Some(arr) = val.as_array() {
194                        for v in arr {
195                            next_matches.push(v);
196                        }
197                    }
198                }
199                Segment::Slice(start, end) => {
200                    if let Some(arr) = val.as_array() {
201                        let start = start.unwrap_or(0);
202                        let end = end.unwrap_or(arr.len()).min(arr.len());
203                        if start < end {
204                            for v in &arr[start..end] {
205                                next_matches.push(v);
206                            }
207                        }
208                    }
209                }
210            }
211        }
212        if next_matches.is_empty() {
213            // If any segment fails to match anything, it's an error for that path branch.
214            // But we only return error if NO matches were found at all?
215            // jq behavior: if you query .a.b and .a is null, it's an error or null.
216            // Let's be strict: if a key is not found, return KeyNotFound.
217            // But for wildcards, empty result is fine?
218            // Let's check segments:
219            match seg {
220                Segment::Key(key) if !path.is_empty() => return Err(QueryError::KeyNotFound(key.clone())),
221                Segment::Index(idx) => return Err(QueryError::IndexOutOfBounds(*idx)),
222                _ => {} // Empty wildcard or slice is fine
223            }
224        }
225        current_matches = next_matches;
226    }
227
228    Ok(current_matches)
229}
230
231/// Parse a raw JSON string and return the formatted (colorized) output.
232///
233/// This is a convenience function combining [`serde_json::from_str`] with
234/// [`format_json`].
235pub fn parse_and_format(json_str: &str, opts: &FormatOptions) -> Result<String, serde_json::Error> {
236    let value: Value = serde_json::from_str(json_str)?;
237    Ok(format_json(&value, opts))
238}
239
240// ═══════════════════════════════════════════════════════════════
241//  Query parser internals
242// ═══════════════════════════════════════════════════════════════
243
244#[derive(Debug, Clone, PartialEq, Eq)]
245enum Segment {
246    Key(String),
247    Index(usize),
248    Wildcard,                   // .* or []
249    Slice(Option<usize>, Option<usize>), // [start:end]
250}
251
252fn parse_query(query: &str) -> Result<Vec<Segment>, QueryError> {
253    let mut segments = Vec::new();
254    let q = query.strip_prefix('.').unwrap_or(query);
255
256    if q.is_empty() {
257        return Ok(segments);
258    }
259
260    let mut chars = q.chars().peekable();
261    let mut buf = String::new();
262
263    while let Some(&ch) = chars.peek() {
264        match ch {
265            '.' => {
266                if !buf.is_empty() {
267                    segments.push(Segment::Key(buf.clone()));
268                    buf.clear();
269                }
270                chars.next();
271                if let Some(&'*') = chars.peek() {
272                    segments.push(Segment::Wildcard);
273                    chars.next();
274                }
275            }
276            '*' => {
277                segments.push(Segment::Wildcard);
278                chars.next();
279            }
280            '[' => {
281                if !buf.is_empty() {
282                    segments.push(Segment::Key(buf.clone()));
283                    buf.clear();
284                }
285                chars.next(); // consume '['
286
287                let mut idx_buf = String::new();
288                let mut found_bracket = false;
289                while let Some(&c) = chars.peek() {
290                    if c == ']' {
291                        chars.next();
292                        found_bracket = true;
293                        break;
294                    }
295                    idx_buf.push(c);
296                    chars.next();
297                }
298
299                if !found_bracket {
300                    return Err(QueryError::InvalidQuery("unclosed bracket".to_string()));
301                }
302
303                if idx_buf.is_empty() {
304                    segments.push(Segment::Wildcard);
305                } else if idx_buf.contains(':') {
306                    // Slice [start:end]
307                    let parts: Vec<&str> = idx_buf.split(':').collect();
308                    let start = if parts[0].is_empty() {
309                        None
310                    } else {
311                        Some(parts[0].parse().map_err(|_| {
312                            QueryError::InvalidQuery(format!("invalid slice start: {}", parts[0]))
313                        })?)
314                    };
315                    let end = if parts.len() < 2 || parts[1].is_empty() {
316                        None
317                    } else {
318                        Some(parts[1].parse().map_err(|_| {
319                            QueryError::InvalidQuery(format!("invalid slice end: {}", parts[1]))
320                        })?)
321                    };
322                    segments.push(Segment::Slice(start, end));
323                } else if let Ok(idx) = idx_buf.parse::<usize>() {
324                    segments.push(Segment::Index(idx));
325                } else {
326                    // Quoted key or raw key in brackets
327                    let key = idx_buf.trim_matches('"').trim_matches('\'').to_string();
328                    segments.push(Segment::Key(key));
329                }
330            }
331            '"' => {
332                // Quoted key in dot notation: ."quoted key"
333                chars.next(); // consume '"'
334                let mut key_buf = String::new();
335                let mut found_quote = false;
336                while let Some(&c) = chars.peek() {
337                    if c == '"' {
338                        chars.next();
339                        found_quote = true;
340                        break;
341                    }
342                    key_buf.push(c);
343                    chars.next();
344                }
345                if !found_quote {
346                    return Err(QueryError::InvalidQuery("unclosed quote".to_string()));
347                }
348                segments.push(Segment::Key(key_buf));
349            }
350            _ => {
351                buf.push(ch);
352                chars.next();
353            }
354        }
355    }
356
357    if !buf.is_empty() {
358        segments.push(Segment::Key(buf));
359    }
360
361    Ok(segments)
362}
363
364// ═══════════════════════════════════════════════════════════════
365//  ColorFormatter implementation
366// ═══════════════════════════════════════════════════════════════
367
368struct ColorFormatter<'a> {
369    pretty: PrettyFormatter<'a>,
370    is_key: bool,
371    theme: &'a Theme,
372}
373
374impl<'a> ColorFormatter<'a> {
375    fn new(indent: &'a [u8], theme: &'a Theme) -> Self {
376        Self {
377            pretty: PrettyFormatter::with_indent(indent),
378            is_key: false,
379            theme,
380        }
381    }
382}
383
384impl<'a> Formatter for ColorFormatter<'a> {
385    #[inline]
386    fn begin_array<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
387        self.pretty.begin_array(writer)
388    }
389
390    #[inline]
391    fn end_array<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
392        self.pretty.end_array(writer)
393    }
394
395    #[inline]
396    fn begin_array_value<W: ?Sized + Write>(&mut self, writer: &mut W, first: bool) -> io::Result<()> {
397        self.pretty.begin_array_value(writer, first)
398    }
399
400    #[inline]
401    fn end_array_value<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
402        self.pretty.end_array_value(writer)
403    }
404
405    #[inline]
406    fn begin_object<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
407        self.pretty.begin_object(writer)
408    }
409
410    #[inline]
411    fn end_object<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
412        self.pretty.end_object(writer)
413    }
414
415    #[inline]
416    fn begin_object_key<W: ?Sized + Write>(&mut self, writer: &mut W, first: bool) -> io::Result<()> {
417        self.is_key = true;
418        self.pretty.begin_object_key(writer, first)
419    }
420
421    #[inline]
422    fn end_object_key<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
423        self.is_key = false;
424        self.pretty.end_object_key(writer)
425    }
426
427    #[inline]
428    fn begin_object_value<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
429        self.pretty.begin_object_value(writer)
430    }
431
432    #[inline]
433    fn end_object_value<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
434        self.pretty.end_object_value(writer)
435    }
436
437    #[inline]
438    fn write_null<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
439        writer.write_all("null".color(self.theme.null).to_string().as_bytes())
440    }
441
442    #[inline]
443    fn write_bool<W: ?Sized + Write>(&mut self, writer: &mut W, value: bool) -> io::Result<()> {
444        let s = if value { "true" } else { "false" };
445        writer.write_all(s.color(self.theme.boolean).bold().to_string().as_bytes())
446    }
447
448    #[inline]
449    fn write_i64<W: ?Sized + Write>(&mut self, writer: &mut W, value: i64) -> io::Result<()> {
450        writer.write_all(value.to_string().color(self.theme.number).to_string().as_bytes())
451    }
452
453    #[inline]
454    fn write_u64<W: ?Sized + Write>(&mut self, writer: &mut W, value: u64) -> io::Result<()> {
455        writer.write_all(value.to_string().color(self.theme.number).to_string().as_bytes())
456    }
457
458    #[inline]
459    fn write_f64<W: ?Sized + Write>(&mut self, writer: &mut W, value: f64) -> io::Result<()> {
460        writer.write_all(value.to_string().color(self.theme.number).to_string().as_bytes())
461    }
462
463    #[inline]
464    fn write_string_fragment<W: ?Sized + Write>(&mut self, writer: &mut W, fragment: &str) -> io::Result<()> {
465        if self.is_key {
466            writer.write_all(fragment.color(self.theme.key).bold().to_string().as_bytes())
467        } else {
468            writer.write_all(fragment.color(self.theme.string).to_string().as_bytes())
469        }
470    }
471
472    #[inline]
473    fn begin_string<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
474        if self.is_key {
475            writer.write_all("\"".color(self.theme.key).bold().to_string().as_bytes())
476        } else {
477            writer.write_all("\"".color(self.theme.string).to_string().as_bytes())
478        }
479    }
480
481    #[inline]
482    fn end_string<W: ?Sized + Write>(&mut self, writer: &mut W) -> io::Result<()> {
483        if self.is_key {
484            writer.write_all("\"".color(self.theme.key).bold().to_string().as_bytes())
485        } else {
486            writer.write_all("\"".color(self.theme.string).to_string().as_bytes())
487        }
488    }
489
490    #[inline]
491    fn write_raw_fragment<W: ?Sized + Write>(&mut self, writer: &mut W, fragment: &str) -> io::Result<()> {
492        writer.write_all(fragment.as_bytes())
493    }
494}
495
496// ═══════════════════════════════════════════════════════════════
497//  Tests
498// ═══════════════════════════════════════════════════════════════
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use serde_json::json;
504
505    #[test]
506    fn test_compact_output() {
507        let val = json!({"a": 1, "b": [2, 3]});
508        let out = format_json_compact(&val);
509        assert!(out.contains("\"a\":1"));
510        assert!(!out.contains('\n'));
511    }
512
513    #[test]
514    fn test_pretty_plain_output() {
515        let val = json!({"name": "Alice"});
516        let opts = FormatOptions { indent: 2, color: false, ..FormatOptions::default() };
517        let out = format_json(&val, &opts);
518        assert!(out.contains("\"name\""));
519        assert!(out.contains("\"Alice\""));
520        assert!(out.contains('\n'));
521    }
522
523    #[test]
524    fn test_query_simple_key() {
525        let val = json!({"greeting": "hello"});
526        let result = query(&val, ".greeting").unwrap();
527        assert_eq!(result, vec![&json!("hello")]);
528    }
529
530    #[test]
531    fn test_query_nested() {
532        let val = json!({"a": {"b": {"c": 42}}});
533        let result = query(&val, ".a.b.c").unwrap();
534        assert_eq!(result, vec![&json!(42)]);
535    }
536
537    #[test]
538    fn test_query_array_index() {
539        let val = json!({"items": [10, 20, 30]});
540        let result = query(&val, ".items[1]").unwrap();
541        assert_eq!(result, vec![&json!(20)]);
542    }
543
544    #[test]
545    fn test_query_mixed() {
546        let val = json!({"users": [{"name": "Alice"}, {"name": "Bob"}]});
547        let result = query(&val, ".users[1].name").unwrap();
548        assert_eq!(result, vec![&json!("Bob")]);
549    }
550
551    #[test]
552    fn test_query_root() {
553        let val = json!({"a": 1});
554        let result = query(&val, ".").unwrap();
555        assert_eq!(result, vec![&val]);
556    }
557
558    #[test]
559    fn test_query_wildcard_array() {
560        let val = json!([1, 2, 3]);
561        let result = query(&val, "[]").unwrap();
562        assert_eq!(result.len(), 3);
563        assert_eq!(result[0], &json!(1));
564    }
565
566    #[test]
567    fn test_query_wildcard_object() {
568        let val = json!({"a": 1, "b": 2});
569        let result = query(&val, ".*").unwrap();
570        assert_eq!(result.len(), 2);
571    }
572
573    #[test]
574    fn test_query_slice() {
575        let val = json!([0, 1, 2, 3, 4]);
576        let result = query(&val, "[1:4]").unwrap();
577        assert_eq!(result.len(), 3);
578        assert_eq!(result[0], &json!(1));
579        assert_eq!(result[2], &json!(3));
580    }
581
582    #[test]
583    fn test_query_quoted_key() {
584        let val = json!({"key with spaces": "val"});
585        let result = query(&val, ".\"key with spaces\"").unwrap();
586        assert_eq!(result, vec![&json!("val")]);
587
588        let result2 = query(&val, "[\"key with spaces\"]").unwrap();
589        assert_eq!(result2, vec![&json!("val")]);
590    }
591
592    #[test]
593    fn test_query_key_not_found() {
594        let val = json!({"a": 1});
595        let err = query(&val, ".b").unwrap_err();
596        assert!(matches!(err, QueryError::KeyNotFound(_)));
597    }
598
599    #[test]
600    fn test_query_index_out_of_bounds() {
601        let val = json!([1, 2]);
602        let err = query(&val, "[5]").unwrap_err();
603        assert!(matches!(err, QueryError::IndexOutOfBounds(5)));
604    }
605
606    #[test]
607    fn test_parse_and_format() {
608        let json_str = r#"{"key": "value"}"#;
609        let opts = FormatOptions { indent: 2, color: false, ..FormatOptions::default() };
610        let result = parse_and_format(json_str, &opts).unwrap();
611        assert!(result.contains("\"key\""));
612    }
613
614    #[test]
615    fn test_parse_and_format_invalid() {
616        let opts = FormatOptions::default();
617        assert!(parse_and_format("not json", &opts).is_err());
618    }
619
620    #[test]
621    fn test_empty_object_and_array() {
622        let opts = FormatOptions { indent: 2, color: false, ..FormatOptions::default() };
623        assert_eq!(format_json(&json!({}), &opts), "{}");
624        assert_eq!(format_json(&json!([]), &opts), "[]");
625    }
626
627    #[test]
628    fn test_escape_special_characters() {
629        let val = json!({"msg": "line1\nline2\ttab"});
630        let opts = FormatOptions { indent: 2, color: false, ..FormatOptions::default() };
631        let out = format_json(&val, &opts);
632        assert!(out.contains("\\n"));
633        assert!(out.contains("\\t"));
634    }
635}