use crate::pycompat::py_float_repr;
use serde_json::Value;
use std::fmt::Write;
pub fn py_float(x: f64) -> Value {
Value::Number(serde_json::Number::from_f64(x).expect("finite float"))
}
pub fn dumps_indent2(value: &Value) -> String {
let mut out = String::new();
write_value(&mut out, value, true, Some(0));
out
}
pub fn dumps_indent2_no_ascii(value: &Value) -> String {
let mut out = String::new();
write_value(&mut out, value, false, Some(0));
out
}
pub fn dumps_compact(value: &Value) -> String {
let mut out = String::new();
write_value(&mut out, value, false, None);
out
}
pub fn dumps_canonical_sorted(value: &Value) -> String {
let mut out = String::new();
write_canonical(&mut out, value);
out
}
fn write_canonical(out: &mut String, value: &Value) {
match value {
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_canonical(out, item);
}
out.push(']');
}
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
out.push('{');
for (i, key) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_string(out, key, false);
out.push(':');
write_canonical(out, &map[key.as_str()]);
}
out.push('}');
}
other => write_value(out, other, false, None),
}
}
fn write_value(out: &mut String, value: &Value, ensure_ascii: bool, indent: Option<usize>) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(true) => out.push_str("true"),
Value::Bool(false) => out.push_str("false"),
Value::Number(n) => write_number(out, n),
Value::String(s) => write_string(out, s, ensure_ascii),
Value::Array(items) => {
if items.is_empty() {
out.push_str("[]");
return;
}
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(item_sep(indent));
}
open_line(out, indent, 1);
write_value(out, item, ensure_ascii, indent.map(|d| d + 1));
}
open_line(out, indent, 0);
out.push(']');
}
Value::Object(map) => {
if map.is_empty() {
out.push_str("{}");
return;
}
out.push('{');
for (i, (key, item)) in map.iter().enumerate() {
if i > 0 {
out.push_str(item_sep(indent));
}
open_line(out, indent, 1);
write_string(out, key, ensure_ascii);
out.push_str(": ");
write_value(out, item, ensure_ascii, indent.map(|d| d + 1));
}
open_line(out, indent, 0);
out.push('}');
}
}
}
fn item_sep(indent: Option<usize>) -> &'static str {
match indent {
Some(_) => ",",
None => ", ",
}
}
fn open_line(out: &mut String, indent: Option<usize>, extra: usize) {
if let Some(depth) = indent {
out.push('\n');
for _ in 0..(depth + extra) * 2 {
out.push(' ');
}
}
}
fn write_number(out: &mut String, n: &serde_json::Number) {
if let Some(i) = n.as_i64() {
out.push_str(&i.to_string());
} else if let Some(u) = n.as_u64() {
out.push_str(&u.to_string());
} else {
out.push_str(&py_float_repr(n.as_f64().expect("number is f64")));
}
}
fn write_string(out: &mut String, s: &str, ensure_ascii: bool) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{8}' => out.push_str("\\b"),
'\t' => out.push_str("\\t"),
'\n' => out.push_str("\\n"),
'\u{c}' => out.push_str("\\f"),
'\r' => out.push_str("\\r"),
c if (c as u32) < 0x20 => write!(out, "\\u{:04x}", c as u32).unwrap(),
c if ensure_ascii && (c as u32) > 0x7e => {
let cp = c as u32;
if let Some(sur) = crate::pycompat::sentinel_surrogate(c) {
write!(out, "\\u{sur:04x}").unwrap();
} else if cp <= 0xffff {
write!(out, "\\u{cp:04x}").unwrap();
} else {
let v = cp - 0x10000;
let hi = 0xd800 + (v >> 10);
let lo = 0xdc00 + (v & 0x3ff);
write!(out, "\\u{hi:04x}\\u{lo:04x}").unwrap();
}
}
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn indent2_layout() {
let v = json!({"a": [], "b": {}, "c": [1], "d": {"x": 1}});
assert_eq!(
dumps_indent2(&v),
"{\n \"a\": [],\n \"b\": {},\n \"c\": [\n 1\n ],\n \"d\": {\n \"x\": 1\n }\n}"
);
}
#[test]
fn ensure_ascii_split() {
let v = json!({"u": "café 🎉"});
assert_eq!(
dumps_indent2(&v),
"{\n \"u\": \"caf\\u00e9 \\ud83c\\udf89\"\n}"
);
assert_eq!(dumps_compact(&v), "{\"u\": \"café 🎉\"}");
}
#[test]
fn int_vs_float_form() {
let v = json!({"i": 2, "f": py_float(2.0), "t": 1e-5});
assert_eq!(dumps_compact(&v), "{\"i\": 2, \"f\": 2.0, \"t\": 1e-05}");
}
#[test]
fn canonical_sorted_dialect() {
let v = json!([{"title": "café — x", "identifier": "A", "category": null}]);
assert_eq!(
dumps_canonical_sorted(&v),
"[{\"category\":null,\"identifier\":\"A\",\"title\":\"café — x\"}]"
);
}
}