#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<Value>),
Object(Vec<(String, Value)>),
}
pub fn parse(text: &str) -> Result<Value, String> {
let mut p = Parser { b: text.as_bytes(), i: 0 };
let v = p.value()?;
p.ws();
if p.i != p.b.len() {
return Err("trailing characters after the JSON value".into());
}
Ok(v)
}
pub fn message(text: &str) -> Option<String> {
let obj = match parse(text).ok()? {
Value::Object(o) => o,
_ => return None,
};
for key in ["result", "message"] {
if let Some((_, Value::String(s))) = obj.iter().find(|(k, _)| k == key) {
return Some(s.clone());
}
}
if obj.len() == 1 {
if let (_, Value::String(s)) = &obj[0] {
return Some(s.clone());
}
}
None
}
pub fn field(text: &str, key: &str) -> Option<String> {
match parse(text).ok()? {
Value::Object(o) => o.iter().find(|(k, _)| k == key).and_then(|(_, v)| match v {
Value::String(s) => Some(s.clone()),
_ => None,
}),
_ => None,
}
}
pub fn quote(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("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
pub fn write(v: &Value) -> String {
match v {
Value::Null => "null".into(),
Value::Bool(b) => b.to_string(),
Value::Number(n) if n.fract() == 0.0 && n.is_finite() && n.abs() < 9.0e15 => format!("{}", *n as i64),
Value::Number(n) => format!("{n}"),
Value::String(s) => quote(s),
Value::Array(a) => format!("[{}]", a.iter().map(write).collect::<Vec<_>>().join(",")),
Value::Object(o) => {
let fields: Vec<String> = o.iter().map(|(k, v)| format!("{}:{}", quote(k), write(v))).collect();
format!("{{{}}}", fields.join(","))
}
}
}
struct Parser<'a> {
b: &'a [u8],
i: usize,
}
impl Parser<'_> {
fn ws(&mut self) {
while matches!(self.b.get(self.i), Some(b' ' | b'\t' | b'\n' | b'\r')) {
self.i += 1;
}
}
fn value(&mut self) -> Result<Value, String> {
self.ws();
match self.b.get(self.i) {
Some(b'{') => self.object(),
Some(b'[') => self.array(),
Some(b'"') => Ok(Value::String(self.string()?)),
Some(b't') => self.lit("true", Value::Bool(true)),
Some(b'f') => self.lit("false", Value::Bool(false)),
Some(b'n') => self.lit("null", Value::Null),
Some(c) if *c == b'-' || c.is_ascii_digit() => self.number(),
_ => Err("expected a JSON value".into()),
}
}
fn lit(&mut self, word: &str, v: Value) -> Result<Value, String> {
if self.b[self.i..].starts_with(word.as_bytes()) {
self.i += word.len();
Ok(v)
} else {
Err(format!("invalid literal (expected {word})"))
}
}
fn object(&mut self) -> Result<Value, String> {
self.i += 1; let mut out = Vec::new();
self.ws();
if self.b.get(self.i) == Some(&b'}') {
self.i += 1;
return Ok(Value::Object(out));
}
loop {
self.ws();
if self.b.get(self.i) != Some(&b'"') {
return Err("expected a string key".into());
}
let key = self.string()?;
self.ws();
if self.b.get(self.i) != Some(&b':') {
return Err("expected ':' after key".into());
}
self.i += 1;
out.push((key, self.value()?));
self.ws();
match self.b.get(self.i) {
Some(b',') => self.i += 1,
Some(b'}') => {
self.i += 1;
return Ok(Value::Object(out));
}
_ => return Err("expected ',' or '}' in object".into()),
}
}
}
fn array(&mut self) -> Result<Value, String> {
self.i += 1; let mut out = Vec::new();
self.ws();
if self.b.get(self.i) == Some(&b']') {
self.i += 1;
return Ok(Value::Array(out));
}
loop {
out.push(self.value()?);
self.ws();
match self.b.get(self.i) {
Some(b',') => self.i += 1,
Some(b']') => {
self.i += 1;
return Ok(Value::Array(out));
}
_ => return Err("expected ',' or ']' in array".into()),
}
}
}
fn string(&mut self) -> Result<String, String> {
self.i += 1; let mut s = String::new();
while let Some(&c) = self.b.get(self.i) {
match c {
b'"' => {
self.i += 1;
return Ok(s);
}
b'\\' => {
self.i += 1;
match self.b.get(self.i) {
Some(b'"') => s.push('"'),
Some(b'\\') => s.push('\\'),
Some(b'/') => s.push('/'),
Some(b'n') => s.push('\n'),
Some(b't') => s.push('\t'),
Some(b'r') => s.push('\r'),
Some(b'b') => s.push('\u{8}'),
Some(b'f') => s.push('\u{c}'),
Some(b'u') => {
let hex = self.b.get(self.i + 1..self.i + 5).ok_or("truncated \\u escape")?;
let cp = u32::from_str_radix(std::str::from_utf8(hex).map_err(|_| "bad \\u escape")?, 16)
.map_err(|_| "bad \\u escape")?;
s.push(char::from_u32(cp).unwrap_or('\u{fffd}'));
self.i += 4;
}
_ => return Err("bad escape in string".into()),
}
self.i += 1;
}
_ => {
let len = utf8_len(c);
let chunk = self.b.get(self.i..self.i + len).ok_or("invalid UTF-8 in string")?;
s.push_str(std::str::from_utf8(chunk).map_err(|_| "invalid UTF-8 in string")?);
self.i += len;
}
}
}
Err("unterminated string".into())
}
fn number(&mut self) -> Result<Value, String> {
let start = self.i;
while let Some(&c) = self.b.get(self.i) {
if c.is_ascii_digit() || matches!(c, b'-' | b'+' | b'.' | b'e' | b'E') {
self.i += 1;
} else {
break;
}
}
let text = std::str::from_utf8(&self.b[start..self.i]).map_err(|_| "bad number")?;
text.parse::<f64>().map(Value::Number).map_err(|_| format!("bad number '{text}'"))
}
}
fn utf8_len(first: u8) -> usize {
match first {
b if b < 0x80 => 1,
b if b >> 5 == 0b110 => 2,
b if b >> 4 == 0b1110 => 3,
_ => 4,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_scalars_arrays_and_objects() {
assert_eq!(parse("null").unwrap(), Value::Null);
assert_eq!(parse(" true ").unwrap(), Value::Bool(true));
assert_eq!(parse("-12.5e1").unwrap(), Value::Number(-125.0));
assert_eq!(parse(r#""hi\n\"x\"""#).unwrap(), Value::String("hi\n\"x\"".into()));
assert_eq!(parse("[1, 2]").unwrap(), Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]));
assert_eq!(
parse(r#"{"a": "b", "n": 1}"#).unwrap(),
Value::Object(vec![("a".into(), Value::String("b".into())), ("n".into(), Value::Number(1.0))])
);
}
#[test]
fn rejects_malformed_json() {
assert!(parse("{").is_err());
assert!(parse(r#"{"a": }"#).is_err());
assert!(parse("[1, 2").is_err());
assert!(parse(r#""unterminated"#).is_err());
assert!(parse("nul").is_err());
assert!(parse(r#"{"a":1} junk"#).is_err());
}
#[test]
fn message_prefers_result_then_message_then_single_field() {
assert_eq!(message(r#"{"result": "done", "other": 1}"#).as_deref(), Some("done"));
assert_eq!(message(r#"{"message": "hi", "x": 2}"#).as_deref(), Some("hi"));
assert_eq!(message(r#"{"ab": "the sum is eight"}"#).as_deref(), Some("the sum is eight"));
assert_eq!(message(r#"{"pq": "two\n(note: defaulted)"}"#).as_deref(), Some("two\n(note: defaulted)"));
}
#[test]
fn field_reads_a_string_value_and_quote_roundtrips() {
assert_eq!(field(r#"{"result": "2 + 3 = 5", "n": 1}"#, "result").as_deref(), Some("2 + 3 = 5"));
assert_eq!(field(r#"{"result": 5}"#, "result"), None); assert_eq!(field(r#"{"a": "b"}"#, "result"), None); let raw = "line1\n\"quoted\"\tend\\x";
let entry = format!("{{\"result\": {}}}", quote(raw));
assert_eq!(field(&entry, "result").as_deref(), Some(raw));
}
#[test]
fn message_is_none_without_an_obvious_field() {
assert_eq!(message(r#"{"a": 1, "b": 2}"#), None); assert_eq!(message("[1, 2, 3]"), None); assert_eq!(message("not json"), None); assert_eq!(message(r#"{"a": "x", "b": "y"}"#), None); }
}