//! 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::(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) ); } }