use std::fmt::{self, Write as _};
#[derive(Debug, Clone)]
pub enum Json {
Null,
Bool(bool),
Int(i64),
Str(String),
Array(Vec<Json>),
Object(Vec<(String, Json)>),
}
impl Json {
pub fn str(s: impl Into<String>) -> Json {
Json::Str(s.into())
}
pub fn opt_str(s: Option<impl Into<String>>) -> Json {
match s {
Some(v) => Json::Str(v.into()),
None => Json::Null,
}
}
fn write(&self, out: &mut String) {
match self {
Json::Null => out.push_str("null"),
Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Json::Int(n) => {
let _ = write!(out, "{n}");
}
Json::Str(s) => write_escaped(s, out),
Json::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
item.write(out);
}
out.push(']');
}
Json::Object(fields) => {
out.push('{');
for (i, (key, val)) in fields.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_escaped(key, out);
out.push(':');
val.write(out);
}
out.push('}');
}
}
}
}
impl fmt::Display for Json {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut out = String::new();
self.write(&mut out);
f.write_str(&out)
}
}
fn write_escaped(s: &str, out: &mut String) {
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 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scalars_serialize() {
assert_eq!(Json::Null.to_string(), "null");
assert_eq!(Json::Bool(true).to_string(), "true");
assert_eq!(Json::Bool(false).to_string(), "false");
assert_eq!(Json::Int(-7).to_string(), "-7");
assert_eq!(Json::str("hi").to_string(), "\"hi\"");
}
#[test]
fn strings_are_escaped_correctly() {
assert_eq!(
Json::str("a\"b\\c\nd\te").to_string(),
"\"a\\\"b\\\\c\\nd\\te\""
);
assert_eq!(Json::str("\u{01}").to_string(), "\"\\u0001\"");
assert_eq!(Json::str("/tmp/we\"ird").to_string(), "\"/tmp/we\\\"ird\"");
assert_eq!(Json::str("🚀").to_string(), "\"🚀\"");
}
#[test]
fn arrays_and_objects_preserve_order() {
let v = Json::Array(vec![Json::Int(1), Json::str("x"), Json::Bool(false)]);
assert_eq!(v.to_string(), "[1,\"x\",false]");
let o = Json::Object(vec![
("b".to_string(), Json::Int(2)),
("a".to_string(), Json::Int(1)),
]);
assert_eq!(o.to_string(), "{\"b\":2,\"a\":1}");
}
#[test]
fn opt_str_maps_none_to_null() {
assert_eq!(Json::opt_str(None::<String>).to_string(), "null");
assert_eq!(Json::opt_str(Some("v")).to_string(), "\"v\"");
}
}