pub struct Json {
buf: String,
comma: bool,
}
impl Default for Json {
fn default() -> Self {
Self::new()
}
}
impl Json {
pub fn new() -> Self {
Self {
buf: String::with_capacity(64 << 10),
comma: false,
}
}
pub fn into_string(self) -> String {
self.buf
}
fn sep(&mut self) {
if self.comma {
self.buf.push(',');
}
self.comma = true;
}
pub fn obj(&mut self, f: impl FnOnce(&mut Self)) {
self.sep();
self.buf.push('{');
self.comma = false;
f(self);
self.buf.push('}');
self.comma = true;
}
pub fn arr(&mut self, f: impl FnOnce(&mut Self)) {
self.sep();
self.buf.push('[');
self.comma = false;
f(self);
self.buf.push(']');
self.comma = true;
}
pub fn key(&mut self, k: &str) {
self.sep();
escape(&mut self.buf, k);
self.buf.push(':');
self.comma = false;
}
pub fn str(&mut self, s: &str) {
self.sep();
escape(&mut self.buf, s);
}
pub fn i64(&mut self, v: i64) {
self.sep();
self.buf.push_str(itoa(v).as_str());
}
pub fn u64(&mut self, v: u64) {
self.sep();
self.buf.push_str(&v.to_string());
}
pub fn i64_str(&mut self, v: i64) {
self.quoted_digits(&v.to_string());
}
pub fn u64_str(&mut self, v: u64) {
self.quoted_digits(&v.to_string());
}
fn quoted_digits(&mut self, digits: &str) {
self.sep();
self.buf.push('"');
self.buf.push_str(digits);
self.buf.push('"');
}
pub fn f64(&mut self, v: f64) {
self.sep();
if v.is_finite() {
self.buf.push_str(&ryu_lite(v));
} else {
self.buf.push_str("null");
}
}
pub fn bool(&mut self, v: bool) {
self.sep();
self.buf.push_str(if v { "true" } else { "false" });
}
pub fn null(&mut self) {
self.sep();
self.buf.push_str("null");
}
pub fn raw(&mut self, fragment: &str) {
if fragment.is_empty() {
return;
}
self.sep();
self.buf.push_str(fragment);
}
pub fn hex(&mut self, bytes: &[u8]) {
self.sep();
self.buf.push('"');
for b in bytes {
self.buf.push(HEX[(b >> 4) as usize] as char);
self.buf.push(HEX[(b & 0xf) as usize] as char);
}
self.buf.push('"');
}
}
const HEX: &[u8; 16] = b"0123456789abcdef";
fn itoa(v: i64) -> String {
v.to_string()
}
fn ryu_lite(v: f64) -> String {
format!("{v}")
}
fn escape(out: &mut String, s: &str) {
out.push('"');
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"),
'\u{08}' => out.push_str("\\b"),
'\u{0c}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
out.push_str("\\u00");
out.push(HEX[(c as usize >> 4) & 0xf] as char);
out.push(HEX[c as usize & 0xf] as char);
}
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nesting_commas_and_hostile_strings() {
let mut j = Json::new();
j.obj(|j| {
j.key("rows");
j.arr(|j| {
j.obj(|j| {
j.key("body");
j.str("line1\nline2\t\"quoted\"\\ \u{1}");
j.key("trace_id");
j.hex(&[0x4b, 0xf9, 0x2f]);
});
j.obj(|j| {
j.key("n");
j.i64(-17);
j.key("big");
j.i64_str(-1_000_000_000_000_000_001);
j.key("ubig");
j.u64_str(u64::MAX);
});
});
j.key("nan");
j.f64(f64::NAN);
j.key("ratio");
j.f64(0.5);
j.key("whole");
j.f64(3.0);
j.key("empty");
j.arr(|_| {});
});
assert_eq!(
j.into_string(),
r#"{"rows":[{"body":"line1\nline2\t\"quoted\"\\ \u0001","trace_id":"4bf92f"},{"n":-17,"big":"-1000000000000000001","ubig":"18446744073709551615"}],"nan":null,"ratio":0.5,"whole":3,"empty":[]}"#
);
}
#[test]
fn an_empty_fragment_is_invisible_and_every_named_escape_is_named() {
let mut j = Json::default();
j.obj(|j| {
j.key("body");
j.str("\u{08}\u{0c}\r\n\t\u{1f} ");
j.raw("");
j.raw(r#""a":1"#);
j.raw("");
j.raw(r#""b":2"#);
});
assert_eq!(
j.into_string(),
"{\"body\":\"\\b\\f\\r\\n\\t\\u001f \",\"a\":1,\"b\":2}"
);
}
#[test]
fn no_finite_double_is_written_with_a_trailing_point_zero() {
for v in [3.0f64, -0.0, 1e20, 1e-7, f64::MAX, f64::MIN_POSITIVE, 0.5] {
let mut j = Json::new();
j.f64(v);
let s = j.into_string();
assert!(!s.ends_with(".0"), "{v:?} rendered as {s}");
assert_eq!(s.parse::<f64>().expect("round trips"), v);
}
}
}