#![allow(dead_code)]
use std::ffi::OsStr;
use std::path::Path;
pub enum Value {
Null,
Bool(bool),
UInt(u64),
Str(String),
Array(Vec<Value>),
Object(Vec<(&'static str, Value)>),
}
impl Value {
pub fn str(s: impl Into<String>) -> Value {
Value::Str(s.into())
}
pub fn from_path(p: &Path) -> Value {
Value::Str(p.to_string_lossy().into_owned())
}
pub fn from_os_str(s: &OsStr) -> Value {
Value::Str(s.to_string_lossy().into_owned())
}
pub fn str_array<I, S>(items: I) -> Value
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Value::Array(items.into_iter().map(|s| Value::Str(s.into())).collect())
}
pub fn obj(entries: Vec<(&'static str, Value)>) -> Value {
Value::Object(entries)
}
}
pub fn write(value: &Value) -> String {
let mut out = String::new();
write_value(&mut out, value);
out
}
fn write_value(out: &mut String, value: &Value) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::UInt(n) => write_uint(out, *n),
Value::Str(s) => escape_into(out, s),
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_value(out, item);
}
out.push(']');
}
Value::Object(entries) => {
out.push('{');
for (i, (k, v)) in entries.iter().enumerate() {
if i > 0 {
out.push(',');
}
escape_into(out, k);
out.push(':');
write_value(out, v);
}
out.push('}');
}
}
}
fn write_uint(out: &mut String, mut n: u64) {
if n == 0 {
out.push('0');
return;
}
let mut buf = [0u8; 20];
let mut i = buf.len();
while n > 0 {
i -= 1;
buf[i] = b'0' + (n % 10) as u8;
n /= 10;
}
out.push_str(std::str::from_utf8(&buf[i..]).expect("digits are always ASCII"));
}
fn escape_into(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 => {
let n = c as u32;
out.push_str("\\u00");
out.push(hex_digit(((n >> 4) & 0xf) as u8));
out.push(hex_digit((n & 0xf) as u8));
}
c => out.push(c),
}
}
out.push('"');
}
fn hex_digit(n: u8) -> char {
match n {
0..=9 => (b'0' + n) as char,
_ => (b'a' + (n - 10)) as char,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scalars() {
assert_eq!(write(&Value::Null), "null");
assert_eq!(write(&Value::Bool(true)), "true");
assert_eq!(write(&Value::Bool(false)), "false");
assert_eq!(write(&Value::UInt(0)), "0");
assert_eq!(write(&Value::UInt(65536)), "65536");
assert_eq!(write(&Value::UInt(u64::MAX)), u64::MAX.to_string());
}
#[test]
fn quote_and_backslash_are_escaped() {
assert_eq!(write(&Value::str("a\"b")), "\"a\\\"b\"");
assert_eq!(write(&Value::str("a\\b")), "\"a\\\\b\"");
}
#[test]
fn shorthand_control_escapes() {
assert_eq!(write(&Value::str("\n")), "\"\\n\"");
assert_eq!(write(&Value::str("\r")), "\"\\r\"");
assert_eq!(write(&Value::str("\t")), "\"\\t\"");
assert_eq!(write(&Value::str("\u{08}")), "\"\\b\"");
assert_eq!(write(&Value::str("\u{0C}")), "\"\\f\"");
}
#[test]
fn other_control_chars_use_u00xx() {
assert_eq!(write(&Value::str("\u{01}")), "\"\\u0001\"");
assert_eq!(write(&Value::str("\u{1f}")), "\"\\u001f\"");
assert_eq!(write(&Value::str("\u{00}")), "\"\\u0000\"");
}
#[test]
fn printable_non_ascii_passes_through_unescaped() {
assert_eq!(write(&Value::str("héllo ☃ 世界")), "\"héllo ☃ 世界\"");
}
#[test]
fn no_trailing_comma_arrays() {
assert_eq!(write(&Value::Array(vec![])), "[]");
assert_eq!(write(&Value::Array(vec![Value::str("a")])), "[\"a\"]");
assert_eq!(
write(&Value::Array(vec![Value::str("a"), Value::str("b")])),
"[\"a\",\"b\"]"
);
}
#[test]
fn no_trailing_comma_objects() {
assert_eq!(write(&Value::Object(vec![])), "{}");
assert_eq!(
write(&Value::Object(vec![("a", Value::Bool(true))])),
"{\"a\":true}"
);
assert_eq!(
write(&Value::Object(vec![
("a", Value::Bool(true)),
("b", Value::UInt(1))
])),
"{\"a\":true,\"b\":1}"
);
}
#[test]
fn nested_object_one_level_and_array_of_objects() {
let v = Value::Object(vec![
(
"backend",
Value::obj(vec![
("selected", Value::str("osc52")),
("available", Value::str_array(["osc52"])),
]),
),
(
"warnings",
Value::Array(vec![Value::obj(vec![
("code", Value::str("x")),
("message", Value::str("y")),
])]),
),
]);
assert_eq!(
write(&v),
"{\"backend\":{\"selected\":\"osc52\",\"available\":[\"osc52\"]},\
\"warnings\":[{\"code\":\"x\",\"message\":\"y\"}]}"
);
}
#[test]
fn lossy_path_round_trips_as_valid_json() {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
let raw = OsStr::from_bytes(b"bad\xffname.txt");
let path = Path::new(raw);
let value = Value::obj(vec![("path", Value::from_path(path))]);
let out = write(&value);
assert!(out.contains('\u{FFFD}'), "expected a replacement char in {out:?}");
assert_eq!(out, "{\"path\":\"bad\u{FFFD}name.txt\"}");
}
}
}