varar-core 0.8.1

Markdown-native BDD — pure functional core engine
Documentation
//! A tiny recursive-descent JSON reader over the dynamic [`Value`] model.
//!
//! Two callers: `varar.lock.json` parsing in [`crate::drift`], and the
//! conformance gates, which compare an artifact against a golden by CONTENT
//! rather than by bytes — the golden is one file generated by the TypeScript
//! reference, and key order, indentation and escaping belong to whichever
//! writer produced it, not to the contract.

use crate::value::Value;
use std::collections::BTreeMap;

/// Parses JSON text into a [`Value`], or `None` on malformed input. Integral
/// numbers become `Value::Int`, the rest `Value::Float` — `Value`'s derived
/// equality deliberately treats `Int(2)` and `Float(2.0)` as different.
pub fn parse_json_value(text: &str) -> Option<Value> {
    JsonReader::new(text).parse_whole()
}

/// A tiny recursive-descent JSON reader — enough for `varar.lock.json`, returning
/// `None` on malformed input (Java's caught-exception → null).
struct JsonReader {
    chars: Vec<char>,
    i: usize,
}

impl JsonReader {
    fn new(text: &str) -> JsonReader {
        JsonReader {
            chars: text.chars().collect(),
            i: 0,
        }
    }

    fn parse_whole(&mut self) -> Option<Value> {
        let v = self.value()?;
        self.skip_ws();
        if self.i != self.chars.len() {
            return None;
        }
        Some(v)
    }

    fn value(&mut self) -> Option<Value> {
        self.skip_ws();
        match self.peek()? {
            '{' => self.object(),
            '[' => self.array(),
            '"' => self.string().map(Value::String),
            't' | 'f' => self.boolean(),
            'n' => self.null(),
            _ => self.number(),
        }
    }

    fn object(&mut self) -> Option<Value> {
        self.expect('{')?;
        let mut map = BTreeMap::new();
        self.skip_ws();
        if self.peek()? == '}' {
            self.i += 1;
            return Some(Value::Map(map));
        }
        loop {
            self.skip_ws();
            let key = self.string()?;
            self.skip_ws();
            self.expect(':')?;
            map.insert(key, self.value()?);
            self.skip_ws();
            match self.next()? {
                '}' => return Some(Value::Map(map)),
                ',' => {}
                _ => return None,
            }
        }
    }

    fn array(&mut self) -> Option<Value> {
        self.expect('[')?;
        let mut list = Vec::new();
        self.skip_ws();
        if self.peek()? == ']' {
            self.i += 1;
            return Some(Value::List(list));
        }
        loop {
            list.push(self.value()?);
            self.skip_ws();
            match self.next()? {
                ']' => return Some(Value::List(list)),
                ',' => {}
                _ => return None,
            }
        }
    }

    fn string(&mut self) -> Option<String> {
        self.expect('"')?;
        let mut out = String::new();
        loop {
            match self.next()? {
                '"' => return Some(out),
                '\\' => match self.next()? {
                    '"' => out.push('"'),
                    '\\' => out.push('\\'),
                    '/' => out.push('/'),
                    'n' => out.push('\n'),
                    'r' => out.push('\r'),
                    't' => out.push('\t'),
                    'b' => out.push('\u{0008}'),
                    'f' => out.push('\u{000c}'),
                    'u' => {
                        let code = self.hex4()?;
                        out.push(char::from_u32(code)?);
                    }
                    _ => return None,
                },
                c => out.push(c),
            }
        }
    }

    fn hex4(&mut self) -> Option<u32> {
        if self.i + 4 > self.chars.len() {
            return None;
        }
        let slice: String = self.chars[self.i..self.i + 4].iter().collect();
        self.i += 4;
        u32::from_str_radix(&slice, 16).ok()
    }

    fn number(&mut self) -> Option<Value> {
        let start = self.i;
        while self.i < self.chars.len() && "-+.eE0123456789".contains(self.chars[self.i]) {
            self.i += 1;
        }
        if self.i == start {
            return None;
        }
        let num: String = self.chars[start..self.i].iter().collect();
        if num.contains(['.', 'e', 'E']) {
            num.parse::<f64>().ok().map(Value::Float)
        } else {
            num.parse::<i64>().ok().map(Value::Int)
        }
    }

    fn boolean(&mut self) -> Option<Value> {
        if self.starts_with("true") {
            self.i += 4;
            Some(Value::Bool(true))
        } else if self.starts_with("false") {
            self.i += 5;
            Some(Value::Bool(false))
        } else {
            None
        }
    }

    fn null(&mut self) -> Option<Value> {
        if self.starts_with("null") {
            self.i += 4;
            Some(Value::Null)
        } else {
            None
        }
    }

    fn starts_with(&self, lit: &str) -> bool {
        let lit: Vec<char> = lit.chars().collect();
        self.i + lit.len() <= self.chars.len() && self.chars[self.i..self.i + lit.len()] == lit[..]
    }

    fn skip_ws(&mut self) {
        while self.i < self.chars.len() && matches!(self.chars[self.i], ' ' | '\n' | '\r' | '\t') {
            self.i += 1;
        }
    }

    fn peek(&self) -> Option<char> {
        self.chars.get(self.i).copied()
    }

    fn next(&mut self) -> Option<char> {
        let c = self.chars.get(self.i).copied()?;
        self.i += 1;
        Some(c)
    }

    fn expect(&mut self, c: char) -> Option<()> {
        if self.next()? == c { Some(()) } else { None }
    }
}