AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/json/src/value.rs

Latest change 3ad787ae157fc60f711c536c1811b3b199b0ac1a - init: AkurAI-Framework foundation (history reset after FS corruption) by Ólafur Búi Ólafsson

//! The JSON [`Value`] type and its serializer.

use std::fmt::Write as _;

/// A JSON value. Objects keep insertion order (a `Vec` of pairs, not a map) so
/// serialized output is stable and reads the way it was written — and so we
/// stay dependency-free.
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
    Null,
    Bool(bool),
    /// An integer literal (no `.`/`e`), preserved exactly — record IDs depend
    /// on this not being coerced to `f64`.
    Int(i64),
    Float(f64),
    Str(String),
    Array(Vec<Value>),
    Object(Vec<(String, Value)>),
}

impl Value {
    /// Look up a key in an object, returning `None` for non-objects or missing
    /// keys. First match wins.
    pub fn get(&self, key: &str) -> Option<&Value> {
        match self {
            Value::Object(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v),
            _ => None,
        }
    }

    pub fn as_str(&self) -> Option<&str> {
        match self {
            Value::Str(s) => Some(s),
            _ => None,
        }
    }

    pub fn as_i64(&self) -> Option<i64> {
        match self {
            Value::Int(n) => Some(*n),
            _ => None,
        }
    }

    pub fn as_f64(&self) -> Option<f64> {
        match self {
            Value::Float(n) => Some(*n),
            Value::Int(n) => Some(*n as f64),
            _ => None,
        }
    }

    pub fn as_bool(&self) -> Option<bool> {
        match self {
            Value::Bool(b) => Some(*b),
            _ => None,
        }
    }

    /// Serialize to a compact JSON string.
    pub fn to_json(&self) -> String {
        let mut out = String::new();
        self.write_json(&mut out);
        out
    }

    fn write_json(&self, out: &mut String) {
        match self {
            Value::Null => out.push_str("null"),
            Value::Bool(true) => out.push_str("true"),
            Value::Bool(false) => out.push_str("false"),
            Value::Int(n) => {
                let _ = write!(out, "{n}");
            }
            Value::Float(n) => {
                if n.is_finite() {
                    let _ = write!(out, "{n}");
                } else {
                    // JSON has no Infinity/NaN; null is the conventional fallback.
                    out.push_str("null");
                }
            }
            Value::Str(s) => write_escaped(s, out),
            Value::Array(items) => {
                out.push('[');
                for (i, item) in items.iter().enumerate() {
                    if i > 0 {
                        out.push(',');
                    }
                    item.write_json(out);
                }
                out.push(']');
            }
            Value::Object(pairs) => {
                out.push('{');
                for (i, (k, v)) in pairs.iter().enumerate() {
                    if i > 0 {
                        out.push(',');
                    }
                    write_escaped(k, out);
                    out.push(':');
                    v.write_json(out);
                }
                out.push('}');
            }
        }
    }
}

/// Write a JSON string literal, escaping per RFC 8259.
fn write_escaped(s: &str, out: &mut String) {
    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 _ = write!(out, "\\u{:04x}", c as u32);
            }
            c => out.push(c),
        }
    }
    out.push('"');
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serializes_primitives() {
        assert_eq!(Value::Null.to_json(), "null");
        assert_eq!(Value::Bool(true).to_json(), "true");
        assert_eq!(Value::Int(42).to_json(), "42");
        assert_eq!(Value::Int(-7).to_json(), "-7");
        assert_eq!(Value::Str("hi".into()).to_json(), "\"hi\"");
    }

    #[test]
    fn escapes_strings() {
        assert_eq!(
            Value::Str("a\"b\\c\nd\te".into()).to_json(),
            "\"a\\\"b\\\\c\\nd\\te\""
        );
        assert_eq!(Value::Str("\u{01}".into()).to_json(), "\"\\u0001\"");
    }

    #[test]
    fn serializes_nested_object_in_order() {
        let v = Value::Object(vec![
            ("name".into(), Value::Str("demo".into())),
            ("ok".into(), Value::Bool(true)),
            (
                "tags".into(),
                Value::Array(vec![Value::Int(1), Value::Int(2)]),
            ),
        ]);
        assert_eq!(v.to_json(), r#"{"name":"demo","ok":true,"tags":[1,2]}"#);
    }

    #[test]
    fn non_finite_floats_become_null() {
        assert_eq!(Value::Float(f64::NAN).to_json(), "null");
        assert_eq!(Value::Float(f64::INFINITY).to_json(), "null");
        assert_eq!(Value::Float(1.5).to_json(), "1.5");
    }

    #[test]
    fn accessors_work() {
        let v = Value::Object(vec![("id".into(), Value::Int(9))]);
        assert_eq!(v.get("id").and_then(Value::as_i64), Some(9));
        assert_eq!(v.get("missing"), None);
    }
}