use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Value {
String(String),
Int(i64),
Bool(bool),
Null,
Array(Vec<Value>),
Object(Vec<(String, Value)>),
}
impl Value {
pub fn s(v: impl Into<String>) -> Self { Self::String(v.into()) }
pub fn i(v: i64) -> Self { Self::Int(v) }
pub fn b(v: bool) -> Self { Self::Bool(v) }
pub fn arr(vs: impl IntoIterator<Item = Value>) -> Self {
Self::Array(vs.into_iter().collect())
}
pub fn obj() -> Self { Self::Object(Vec::new()) }
pub fn insert(&mut self, k: impl Into<String>, v: Value) -> &mut Self {
if let Self::Object(items) = self {
let key = k.into();
if let Some(idx) = items.iter().position(|(ek, _)| ek == &key) {
items[idx].1 = v;
} else {
items.push((key, v));
}
}
self
}
fn render(&self, indent: usize) -> String {
let pad = " ".repeat(indent);
let inner_pad = " ".repeat(indent + 1);
match self {
Self::String(s) => render_string(s),
Self::Int(n) => n.to_string(),
Self::Bool(true) => "true".into(),
Self::Bool(false) => "false".into(),
Self::Null => "null".into(),
Self::Array(items) if items.is_empty() => "[]".into(),
Self::Array(items) => {
let mut out = String::from("[\n");
for (i, v) in items.iter().enumerate() {
out.push_str(&inner_pad);
out.push_str(&v.render(indent + 1));
if i + 1 < items.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(&pad);
out.push(']');
out
}
Self::Object(items) if items.is_empty() => "{}".into(),
Self::Object(items) => {
let mut out = String::from("{\n");
for (i, (k, v)) in items.iter().enumerate() {
out.push_str(&inner_pad);
out.push_str(&render_string(k));
out.push_str(": ");
out.push_str(&v.render(indent + 1));
if i + 1 < items.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(&pad);
out.push('}');
out
}
}
}
}
fn render_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str(r"\\"),
'\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('"');
out
}
pub fn render(v: &Value) -> String {
let mut s = v.render(0);
s.push('\n');
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_escapes_quotes_and_backslashes() {
assert_eq!(render_string(r#"a"b\c"#), r#""a\"b\\c""#);
}
#[test]
fn object_renders_with_indent() {
let mut o = Value::obj();
o.insert("name", Value::s("demo"));
o.insert("version", Value::s("0.1.0"));
let out = render(&o);
assert!(out.contains(r#""name": "demo""#));
assert!(out.contains(r#""version": "0.1.0""#));
}
#[test]
fn nested_object_indented() {
let mut outer = Value::obj();
let mut inner = Value::obj();
inner.insert("type", Value::s("git"));
outer.insert("repository", inner);
let s = render(&outer);
assert!(s.contains(r#""type": "git""#));
}
}