use super::convert::JqJson;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Indent {
Compact,
Spaces(u8),
Tab,
}
pub(super) fn sort_keys(v: JqJson) -> JqJson {
match v {
JqJson::Object(map) => {
let mut sorted: Vec<(String, JqJson)> =
map.into_iter().map(|(k, v)| (k, sort_keys(v))).collect();
sorted.sort_by(|a, b| a.0.cmp(&b.0));
JqJson::Object(sorted)
}
JqJson::Array(arr) => JqJson::Array(arr.into_iter().map(sort_keys).collect()),
other => other,
}
}
pub(super) fn render(v: &JqJson, indent: Indent) -> String {
let mut out = String::new();
match indent {
Indent::Compact => render_compact(v, &mut out),
Indent::Spaces(0) => render_compact(v, &mut out),
Indent::Spaces(n) => render_pretty(v, &mut out, &" ".repeat(n as usize), 0),
Indent::Tab => render_pretty(v, &mut out, "\t", 0),
}
out
}
fn render_compact(v: &JqJson, out: &mut String) {
match v {
JqJson::Null => out.push_str("null"),
JqJson::Bool(true) => out.push_str("true"),
JqJson::Bool(false) => out.push_str("false"),
JqJson::Number(s) => out.push_str(s),
JqJson::String(s) => write_json_string(s, out),
JqJson::Array(arr) => {
out.push('[');
for (i, item) in arr.iter().enumerate() {
if i > 0 {
out.push(',');
}
render_compact(item, out);
}
out.push(']');
}
JqJson::Object(map) => {
out.push('{');
for (i, (k, item)) in map.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_json_string(k, out);
out.push(':');
render_compact(item, out);
}
out.push('}');
}
}
}
fn render_pretty(v: &JqJson, out: &mut String, indent: &str, level: usize) {
match v {
JqJson::Null => out.push_str("null"),
JqJson::Bool(true) => out.push_str("true"),
JqJson::Bool(false) => out.push_str("false"),
JqJson::Number(s) => out.push_str(s),
JqJson::String(s) => write_json_string(s, out),
JqJson::Array(arr) => {
if arr.is_empty() {
out.push_str("[]");
return;
}
out.push('[');
for (i, item) in arr.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push('\n');
push_indent(out, indent, level + 1);
render_pretty(item, out, indent, level + 1);
}
out.push('\n');
push_indent(out, indent, level);
out.push(']');
}
JqJson::Object(map) => {
if map.is_empty() {
out.push_str("{}");
return;
}
out.push('{');
for (i, (k, item)) in map.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push('\n');
push_indent(out, indent, level + 1);
write_json_string(k, out);
out.push_str(": ");
render_pretty(item, out, indent, level + 1);
}
out.push('\n');
push_indent(out, indent, level);
out.push('}');
}
}
}
fn push_indent(out: &mut String, indent: &str, level: usize) {
if indent.is_empty() {
return;
}
for _ in 0..level {
out.push_str(indent);
}
}
fn write_json_string(s: &str, out: &mut String) {
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x08' => out.push_str("\\b"),
'\x0c' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
fn n(s: &str) -> JqJson {
JqJson::Number(s.to_string())
}
#[test]
fn compact_array() {
let v = JqJson::Array(vec![n("1"), n("2"), n("3")]);
assert_eq!(render(&v, Indent::Compact), "[1,2,3]");
}
#[test]
fn pretty_two_space() {
let v = JqJson::Object(vec![
("a".into(), n("1")),
("b".into(), JqJson::Array(vec![n("2")])),
]);
let out = render(&v, Indent::Spaces(2));
assert!(out.contains("\n \"a\": 1"));
assert!(out.contains("\n 2"));
}
#[test]
fn pretty_four_space() {
let v = JqJson::Object(vec![("a".into(), n("1"))]);
let out = render(&v, Indent::Spaces(4));
assert!(out.contains("\n \"a\": 1"));
}
#[test]
fn indent_zero_renders_compact() {
let v = JqJson::Object(vec![("a".into(), n("1"))]);
let out = render(&v, Indent::Spaces(0));
assert_eq!(out, "{\"a\":1}");
}
#[test]
fn tab_indent() {
let v = JqJson::Object(vec![("a".into(), n("1"))]);
let out = render(&v, Indent::Tab);
assert!(out.contains("\n\t\"a\": 1"));
}
#[test]
fn empty_array_compact_pretty() {
let v = JqJson::Array(vec![]);
assert_eq!(render(&v, Indent::Compact), "[]");
assert_eq!(render(&v, Indent::Spaces(2)), "[]");
}
#[test]
fn empty_object_compact_pretty() {
let v = JqJson::Object(vec![]);
assert_eq!(render(&v, Indent::Compact), "{}");
assert_eq!(render(&v, Indent::Spaces(2)), "{}");
}
#[test]
fn preserves_float_zero_decimal() {
let v = n("1.0");
assert_eq!(render(&v, Indent::Compact), "1.0");
assert_eq!(render(&v, Indent::Spaces(2)), "1.0");
}
#[test]
fn string_escapes_quotes_and_newline() {
let v = JqJson::String("a\"b\nc".into());
assert_eq!(render(&v, Indent::Compact), "\"a\\\"b\\nc\"");
}
#[test]
fn string_does_not_escape_forward_slash() {
let v = JqJson::String("/path/to".into());
assert_eq!(render(&v, Indent::Compact), "\"/path/to\"");
}
#[test]
fn string_escapes_control_chars() {
let v = JqJson::String("\x01".into());
assert_eq!(render(&v, Indent::Compact), "\"\\u0001\"");
}
#[test]
fn sort_keys_recursive() {
let v = JqJson::Object(vec![
("b".into(), n("1")),
(
"a".into(),
JqJson::Object(vec![("z".into(), n("9")), ("y".into(), n("8"))]),
),
]);
let sorted = sort_keys(v);
if let JqJson::Object(map) = &sorted {
assert_eq!(map[0].0, "a");
assert_eq!(map[1].0, "b");
if let JqJson::Object(inner) = &map[0].1 {
assert_eq!(inner[0].0, "y");
assert_eq!(inner[1].0, "z");
} else {
panic!("nested not object");
}
} else {
panic!("not object");
}
}
}