jarq 0.8.2

An interactive jq-like JSON query tool with a TUI
Documentation
//! JSON serialization utilities

use simd_json::OwnedValue as Value;
use simd_json::StaticNode;

// ANSI color codes (matching jq's scheme)
const RESET: &str = "\x1b[0m";
const NULL: &str = "\x1b[0;90m"; // dark gray
const STRING: &str = "\x1b[0;32m"; // green
const KEY: &str = "\x1b[1;34m"; // bold blue

/// Pretty-print a JSON value with 2-space indentation
pub fn to_string_pretty(value: &Value) -> String {
    let mut output = String::new();
    write_value(value, 0, false, false, &mut output);
    output
}

/// Pretty-print a JSON value with 2-space indentation and ANSI colors
pub fn to_string_pretty_colored(value: &Value) -> String {
    let mut output = String::new();
    write_value(value, 0, true, false, &mut output);
    output
}

/// Compact JSON output (no whitespace)
pub fn to_string_compact(value: &Value) -> String {
    let mut output = String::new();
    write_value(value, 0, false, true, &mut output);
    output
}

/// Get raw string value (without quotes) for -r flag
pub fn to_string_raw(value: &Value) -> Option<String> {
    match value {
        Value::String(s) => Some(s.to_string()),
        _ => None,
    }
}

fn write_value(value: &Value, indent: usize, color: bool, compact: bool, out: &mut String) {
    match value {
        Value::Static(StaticNode::Null) => {
            if color {
                out.push_str(NULL);
            }
            out.push_str("null");
            if color {
                out.push_str(RESET);
            }
        }
        Value::Static(StaticNode::Bool(b)) => {
            out.push_str(if *b { "true" } else { "false" });
        }
        Value::Static(StaticNode::I64(n)) => {
            out.push_str(&n.to_string());
        }
        Value::Static(StaticNode::U64(n)) => {
            out.push_str(&n.to_string());
        }
        Value::Static(StaticNode::F64(n)) => {
            if n.fract() == 0.0 && n.abs() < 1e15 {
                out.push_str(&format!("{:.0}", n));
            } else {
                out.push_str(&n.to_string());
            }
        }
        Value::String(s) => {
            if color {
                out.push_str(STRING);
            }
            out.push('"');
            escape_string(s, out);
            out.push('"');
            if color {
                out.push_str(RESET);
            }
        }
        Value::Array(arr) => {
            if arr.is_empty() {
                out.push_str("[]");
            } else if compact {
                out.push('[');
                for (i, item) in arr.iter().enumerate() {
                    write_value(item, 0, color, compact, out);
                    if i < arr.len() - 1 {
                        out.push(',');
                    }
                }
                out.push(']');
            } else {
                out.push_str("[\n");
                for (i, item) in arr.iter().enumerate() {
                    write_indent(indent + 1, out);
                    write_value(item, indent + 1, color, compact, out);
                    if i < arr.len() - 1 {
                        out.push(',');
                    }
                    out.push('\n');
                }
                write_indent(indent, out);
                out.push(']');
            }
        }
        Value::Object(obj) => {
            if obj.is_empty() {
                out.push_str("{}");
            } else if compact {
                out.push('{');
                let len = obj.len();
                for (i, (key, val)) in obj.iter().enumerate() {
                    out.push('"');
                    escape_string(key, out);
                    out.push_str("\":");
                    write_value(val, 0, color, compact, out);
                    if i < len - 1 {
                        out.push(',');
                    }
                }
                out.push('}');
            } else {
                out.push_str("{\n");
                let len = obj.len();
                for (i, (key, val)) in obj.iter().enumerate() {
                    write_indent(indent + 1, out);
                    if color {
                        out.push_str(KEY);
                    }
                    out.push('"');
                    escape_string(key, out);
                    out.push('"');
                    if color {
                        out.push_str(RESET);
                    }
                    out.push_str(": ");
                    write_value(val, indent + 1, color, compact, out);
                    if i < len - 1 {
                        out.push(',');
                    }
                    out.push('\n');
                }
                write_indent(indent, out);
                out.push('}');
            }
        }
    }
}

fn write_indent(level: usize, out: &mut String) {
    for _ in 0..level {
        out.push_str("  ");
    }
}

fn escape_string(s: &str, out: &mut String) {
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if c.is_control() => {
                out.push_str(&format!("\\u{:04x}", c as u32));
            }
            c => out.push(c),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use simd_json::json;

    #[test]
    fn test_primitives() {
        assert_eq!(to_string_pretty(&json!(null)), "null");
        assert_eq!(to_string_pretty(&json!(true)), "true");
        assert_eq!(to_string_pretty(&json!(false)), "false");
        assert_eq!(to_string_pretty(&json!(42)), "42");
        assert_eq!(to_string_pretty(&json!(-17)), "-17");
        assert_eq!(to_string_pretty(&json!("hello")), "\"hello\"");
    }

    #[test]
    fn test_string_escaping() {
        assert_eq!(to_string_pretty(&json!("a\"b")), "\"a\\\"b\"");
        assert_eq!(to_string_pretty(&json!("a\\b")), "\"a\\\\b\"");
        assert_eq!(to_string_pretty(&json!("a\nb")), "\"a\\nb\"");
    }

    #[test]
    fn test_empty_containers() {
        assert_eq!(to_string_pretty(&json!([])), "[]");
        assert_eq!(to_string_pretty(&json!({})), "{}");
    }

    #[test]
    fn test_array() {
        let expected = "[\n  1,\n  2,\n  3\n]";
        assert_eq!(to_string_pretty(&json!([1, 2, 3])), expected);
    }

    #[test]
    fn test_object() {
        // Note: simd_json objects may not preserve order, so we test a single-key object
        let expected = "{\n  \"a\": 1\n}";
        assert_eq!(to_string_pretty(&json!({"a": 1})), expected);
    }

    #[test]
    fn test_nested() {
        let expected = "{\n  \"arr\": [\n    1,\n    2\n  ]\n}";
        assert_eq!(to_string_pretty(&json!({"arr": [1, 2]})), expected);
    }
}