~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/asyncai

0
0
main
138 lines · 4.4 KB Raw History
//! JSON <-> Monty value conversion at the sandbox boundary.

use monty_types::{MontyObject, ObjectRef};
use serde_json::{Map, Number, Value};

/// Deepest nesting converted back to JSON; deeper values become their `repr()`.
const MAX_DEPTH: usize = 256;

/// Converts a JSON value into a Python value.
pub fn json_to_monty(value: &Value) -> MontyObject {
    match value {
        Value::Null => MontyObject::none(),
        Value::Bool(flag) => MontyObject::bool(*flag),
        Value::Number(number) => match number.as_i64() {
            Some(integer) => MontyObject::int(integer),
            None => MontyObject::float(number.as_f64().unwrap_or(f64::NAN)),
        },
        Value::String(text) => MontyObject::string(text.as_str()),
        Value::Array(items) => MontyObject::list(items.iter().map(json_to_monty)),
        Value::Object(map) => MontyObject::dict(
            map.iter()
                .map(|(key, value)| (MontyObject::string(key.as_str()), json_to_monty(value))),
        ),
    }
}

/// Parses tool output text: JSON becomes structured data, anything else a `str`.
pub fn tool_text_to_monty(text: &str) -> MontyObject {
    match serde_json::from_str::<Value>(text) {
        Ok(value) => json_to_monty(&value),
        Err(_) => MontyObject::string(text),
    }
}

/// Converts a Python value into JSON.
pub fn monty_to_json(value: &MontyObject) -> Value {
    object_to_json(value.as_ref(), 0)
}

/// Converts a borrowed Python value into JSON.
pub fn object_ref_to_json(value: ObjectRef<'_>) -> Value {
    object_to_json(value, 0)
}

fn object_to_json(value: ObjectRef<'_>, depth: usize) -> Value {
    if depth > MAX_DEPTH {
        return Value::String(value.py_repr());
    }

    match value.type_name() {
        "NoneType" => Value::Null,
        "bool" => value
            .as_bool()
            .map(Value::Bool)
            .unwrap_or_else(|| Value::String(value.py_repr())),
        "int" => value
            .as_int()
            .map(|integer| Value::Number(integer.into()))
            .unwrap_or_else(|| Value::String(value.py_repr())),
        "float" => value
            .as_float()
            .and_then(Number::from_f64)
            .map(Value::Number)
            .unwrap_or_else(|| Value::String(value.py_repr())),
        "str" => value
            .as_str()
            .map(|text| Value::String(text.to_string()))
            .unwrap_or_else(|| Value::String(value.py_repr())),
        _ => {
            if let Some(items) = value.items() {
                return Value::Array(
                    items
                        .into_iter()
                        .map(|item| object_to_json(item, depth + 1))
                        .collect(),
                );
            }
            if let Some(pairs) = value.pairs() {
                let mut map = Map::new();
                for (key, item) in pairs {
                    let key = match key.as_str() {
                        Some(text) => text.to_string(),
                        None => key.py_repr(),
                    };
                    map.insert(key, object_to_json(item, depth + 1));
                }
                return Value::Object(map);
            }
            Value::String(value.py_repr())
        }
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;

    #[test]
    fn json_round_trips_through_monty() {
        let value = json!({
            "items": [{"name": "a", "replicas": 3, "ready": true, "ratio": 0.5, "owner": null}],
            "kind": "List",
        });
        assert_eq!(monty_to_json(&json_to_monty(&value)), value);
    }

    #[test]
    fn tuples_sets_and_non_string_keys_convert() {
        let tuple = MontyObject::tuple([MontyObject::int(1), MontyObject::string("x")]);
        assert_eq!(monty_to_json(&tuple), json!([1, "x"]));

        let dict = MontyObject::dict([(MontyObject::int(1), MontyObject::string("a"))]);
        assert_eq!(monty_to_json(&dict), json!({"1": "a"}));
    }

    #[test]
    fn non_json_tool_text_stays_a_string() {
        assert_eq!(
            monty_to_json(&tool_text_to_monty("plain log line")),
            json!("plain log line")
        );
        assert_eq!(
            monty_to_json(&tool_text_to_monty(r#"{"a": [1, 2]}"#)),
            json!({"a": [1, 2]})
        );
    }

    #[test]
    fn huge_integers_become_floats() {
        let value = json!(18446744073709551615u64);
        assert_eq!(
            monty_to_json(&json_to_monty(&value)),
            json!(1.8446744073709552e19)
        );
    }
}