Menu
Public repository / master
AkurAI-Framework / crates/json/src/parse.rs
Latest change 3ad787ae157fc60f711c536c1811b3b199b0ac1a - init: AkurAI-Framework foundation (history reset after FS corruption) by Ólafur Búi Ólafsson
//! Recursive-descent JSON parser.
//!
//! Operates over a `Vec<char>` for simple, correct indexing (the input is
//! already valid UTF-8). Speed work can come later; correctness first.
use crate::error::Error;
use crate::value::Value;
/// Parse a complete JSON document. Trailing non-whitespace is an error.
pub fn parse(input: &str) -> Result<Value, Error> {
let mut p = Parser {
chars: input.chars().collect(),
pos: 0,
};
p.skip_ws();
let value = p.parse_value()?;
p.skip_ws();
if p.pos != p.chars.len() {
return Err(Error::new("trailing characters after JSON value", p.pos));
}
Ok(value)
}
struct Parser {
chars: Vec<char>,
pos: usize,
}
impl Parser {
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
fn bump(&mut self) -> Option<char> {
let c = self.peek();
if c.is_some() {
self.pos += 1;
}
c
}
fn skip_ws(&mut self) {
while matches!(self.peek(), Some(' ' | '\t' | '\n' | '\r')) {
self.pos += 1;
}
}
fn parse_value(&mut self) -> Result<Value, Error> {
match self.peek() {
Some('{') => self.parse_object(),
Some('[') => self.parse_array(),
Some('"') => Ok(Value::Str(self.parse_string()?)),
Some('t') | Some('f') => self.parse_bool(),
Some('n') => self.parse_null(),
Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
Some(c) => Err(Error::new(format!("unexpected character '{c}'"), self.pos)),
None => Err(Error::new("unexpected end of input", self.pos)),
}
}
fn expect_literal(&mut self, literal: &str, value: Value) -> Result<Value, Error> {
let start = self.pos;
for expected in literal.chars() {
if self.bump() != Some(expected) {
return Err(Error::new(format!("expected '{literal}'"), start));
}
}
Ok(value)
}
fn parse_bool(&mut self) -> Result<Value, Error> {
if self.peek() == Some('t') {
self.expect_literal("true", Value::Bool(true))
} else {
self.expect_literal("false", Value::Bool(false))
}
}
fn parse_null(&mut self) -> Result<Value, Error> {
self.expect_literal("null", Value::Null)
}
fn parse_number(&mut self) -> Result<Value, Error> {
let start = self.pos;
let mut is_float = false;
if self.peek() == Some('-') {
self.pos += 1;
}
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.pos += 1;
}
if self.peek() == Some('.') {
is_float = true;
self.pos += 1;
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.pos += 1;
}
}
if matches!(self.peek(), Some('e' | 'E')) {
is_float = true;
self.pos += 1;
if matches!(self.peek(), Some('+' | '-')) {
self.pos += 1;
}
while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
self.pos += 1;
}
}
let text: String = self.chars[start..self.pos].iter().collect();
if is_float {
text.parse::<f64>()
.map(Value::Float)
.map_err(|_| Error::new("invalid number", start))
} else {
// Integers that overflow i64 fall back to float rather than failing.
match text.parse::<i64>() {
Ok(n) => Ok(Value::Int(n)),
Err(_) => text
.parse::<f64>()
.map(Value::Float)
.map_err(|_| Error::new("invalid number", start)),
}
}
}
fn parse_string(&mut self) -> Result<String, Error> {
let open = self.pos;
self.bump(); // consume opening quote
let mut s = String::new();
loop {
match self.bump() {
None => return Err(Error::new("unterminated string", open)),
Some('"') => return Ok(s),
Some('\\') => self.parse_escape(&mut s)?,
Some(c) if (c as u32) < 0x20 => {
return Err(Error::new("control character in string", self.pos - 1));
}
Some(c) => s.push(c),
}
}
}
fn parse_escape(&mut self, s: &mut String) -> Result<(), Error> {
let at = self.pos - 1;
match self.bump() {
Some('"') => s.push('"'),
Some('\\') => s.push('\\'),
Some('/') => s.push('/'),
Some('n') => s.push('\n'),
Some('t') => s.push('\t'),
Some('r') => s.push('\r'),
Some('b') => s.push('\u{08}'),
Some('f') => s.push('\u{0c}'),
Some('u') => self.parse_unicode_escape(s, at)?,
_ => return Err(Error::new("invalid escape", at)),
}
Ok(())
}
fn parse_unicode_escape(&mut self, s: &mut String, at: usize) -> Result<(), Error> {
let hi = self.read_hex4(at)?;
// Surrogate pair: a high surrogate must be followed by `\uXXXX` low.
if (0xD800..=0xDBFF).contains(&hi) {
if self.bump() != Some('\\') || self.bump() != Some('u') {
return Err(Error::new("expected low surrogate", at));
}
let lo = self.read_hex4(at)?;
if !(0xDC00..=0xDFFF).contains(&lo) {
return Err(Error::new("invalid low surrogate", at));
}
let c = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
match char::from_u32(c) {
Some(c) => s.push(c),
None => return Err(Error::new("invalid surrogate pair", at)),
}
} else {
match char::from_u32(hi) {
Some(c) => s.push(c),
None => return Err(Error::new("invalid unicode escape", at)),
}
}
Ok(())
}
fn read_hex4(&mut self, at: usize) -> Result<u32, Error> {
let mut value = 0u32;
for _ in 0..4 {
let c = self
.bump()
.ok_or_else(|| Error::new("truncated unicode escape", at))?;
let digit = c
.to_digit(16)
.ok_or_else(|| Error::new("invalid hex digit", self.pos - 1))?;
value = value * 16 + digit;
}
Ok(value)
}
fn parse_array(&mut self) -> Result<Value, Error> {
self.bump(); // '['
let mut items = Vec::new();
self.skip_ws();
if self.peek() == Some(']') {
self.bump();
return Ok(Value::Array(items));
}
loop {
self.skip_ws();
items.push(self.parse_value()?);
self.skip_ws();
match self.bump() {
Some(',') => continue,
Some(']') => return Ok(Value::Array(items)),
_ => return Err(Error::new("expected ',' or ']' in array", self.pos - 1)),
}
}
}
fn parse_object(&mut self) -> Result<Value, Error> {
self.bump(); // '{'
let mut pairs = Vec::new();
self.skip_ws();
if self.peek() == Some('}') {
self.bump();
return Ok(Value::Object(pairs));
}
loop {
self.skip_ws();
if self.peek() != Some('"') {
return Err(Error::new("expected string key in object", self.pos));
}
let key = self.parse_string()?;
self.skip_ws();
if self.bump() != Some(':') {
return Err(Error::new("expected ':' after object key", self.pos - 1));
}
self.skip_ws();
let value = self.parse_value()?;
pairs.push((key, value));
self.skip_ws();
match self.bump() {
Some(',') => continue,
Some('}') => return Ok(Value::Object(pairs)),
_ => return Err(Error::new("expected ',' or '}' in object", self.pos - 1)),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_primitives() {
assert_eq!(parse("null").unwrap(), Value::Null);
assert_eq!(parse("true").unwrap(), Value::Bool(true));
assert_eq!(parse("false").unwrap(), Value::Bool(false));
assert_eq!(parse(" 42 ").unwrap(), Value::Int(42));
assert_eq!(parse("-7").unwrap(), Value::Int(-7));
assert_eq!(parse("2.5").unwrap(), Value::Float(2.5));
assert_eq!(parse("1e3").unwrap(), Value::Float(1000.0));
assert_eq!(parse("\"hello\"").unwrap(), Value::Str("hello".into()));
}
#[test]
fn integers_stay_integers() {
// The classic JSON foot-gun: a big id must not become an inexact float.
assert_eq!(
parse("9007199254740993").unwrap(),
Value::Int(9007199254740993)
);
}
#[test]
fn parses_string_escapes() {
assert_eq!(
parse(r#""a\"b\\c\n""#).unwrap(),
Value::Str("a\"b\\c\n".into())
);
assert_eq!(parse(r#""Aé""#).unwrap(), Value::Str("Aé".into()));
}
#[test]
fn parses_surrogate_pair() {
// U+1F600 GRINNING FACE encoded as a UTF-16 surrogate pair.
assert_eq!(parse(r#""😀""#).unwrap(), Value::Str("😀".into()));
}
#[test]
fn parses_nested_structures() {
let v = parse(r#"{ "a": [1, 2, {"b": null}], "c": true }"#).unwrap();
assert_eq!(
v.get("a").unwrap(),
&Value::Array(vec![
Value::Int(1),
Value::Int(2),
Value::Object(vec![("b".into(), Value::Null)]),
])
);
assert_eq!(v.get("c"), Some(&Value::Bool(true)));
}
#[test]
fn parses_empty_containers() {
assert_eq!(parse("[]").unwrap(), Value::Array(vec![]));
assert_eq!(parse("{}").unwrap(), Value::Object(vec![]));
}
#[test]
fn round_trips() {
let src = r#"{"name":"demo","n":42,"f":1.5,"ok":true,"xs":[1,2,3],"nil":null}"#;
assert_eq!(parse(src).unwrap().to_json(), src);
}
#[test]
fn rejects_malformed() {
assert!(parse("").is_err());
assert!(parse("{").is_err());
assert!(parse("[1,]").is_err());
assert!(parse("{\"a\":}").is_err());
assert!(parse("nul").is_err());
assert!(parse("1 2").is_err()); // trailing characters
assert!(parse("\"unterminated").is_err());
assert!(parse("{a:1}").is_err()); // unquoted key
}
}