use monty_types::{DictPairs, MontyObject};
use serde_json::{Map, Number, Value};
const MAX_CONVERSION_DEPTH: usize = 64;
const DEPTH_PLACEHOLDER: &str = "<truncated: nesting depth limit reached>";
pub fn json_to_monty(value: Value) -> MontyObject {
to_monty(value, MAX_CONVERSION_DEPTH)
}
fn to_monty(value: Value, budget: usize) -> MontyObject {
if budget == 0 {
return MontyObject::String(DEPTH_PLACEHOLDER.to_string());
}
match value {
Value::Null => MontyObject::None,
Value::Bool(b) => MontyObject::Bool(b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
MontyObject::Int(i)
} else {
match n.as_f64() {
Some(f) => MontyObject::Float(f),
None => MontyObject::String(n.to_string()),
}
}
}
Value::String(s) => MontyObject::String(s),
Value::Array(items) => {
MontyObject::List(items.into_iter().map(|v| to_monty(v, budget - 1)).collect())
}
Value::Object(map) => {
let pairs: Vec<(MontyObject, MontyObject)> = map
.into_iter()
.map(|(k, v)| (MontyObject::String(k), to_monty(v, budget - 1)))
.collect();
MontyObject::Dict(DictPairs::from(pairs))
}
}
}
pub fn monty_to_json(obj: &MontyObject) -> Value {
to_json(obj, MAX_CONVERSION_DEPTH)
}
fn to_json(obj: &MontyObject, budget: usize) -> Value {
if budget == 0 {
return Value::String(DEPTH_PLACEHOLDER.to_string());
}
match obj {
MontyObject::None => Value::Null,
MontyObject::Bool(b) => Value::Bool(*b),
MontyObject::Int(i) => Value::Number(Number::from(*i)),
MontyObject::Float(f) => Number::from_f64(*f).map_or(Value::Null, Value::Number),
MontyObject::String(s) => Value::String(s.clone()),
MontyObject::Path(p) => Value::String(p.clone()),
MontyObject::List(items)
| MontyObject::Tuple(items)
| MontyObject::Set(items)
| MontyObject::FrozenSet(items) => {
Value::Array(items.iter().map(|item| to_json(item, budget - 1)).collect())
}
MontyObject::Dict(pairs) => dict_to_json(pairs, budget),
other => Value::String(guarded_repr(other, budget)),
}
}
fn dict_to_json(pairs: &DictPairs, budget: usize) -> Value {
let mut map = Map::new();
for (key, value) in pairs {
map.insert(key_string(key, budget), to_json(value, budget - 1));
}
Value::Object(map)
}
pub(crate) fn monty_key_string(key: &MontyObject) -> String {
key_string(key, MAX_CONVERSION_DEPTH)
}
fn key_string(key: &MontyObject, budget: usize) -> String {
match key {
MontyObject::String(s) => s.clone(),
other => guarded_repr(other, budget),
}
}
fn guarded_repr(obj: &MontyObject, budget: usize) -> String {
if exceeds_depth(obj, budget) { DEPTH_PLACEHOLDER.to_string() } else { obj.py_repr() }
}
fn exceeds_depth(obj: &MontyObject, budget: usize) -> bool {
if budget == 0 {
return true;
}
match obj {
MontyObject::List(items)
| MontyObject::Tuple(items)
| MontyObject::Set(items)
| MontyObject::FrozenSet(items)
| MontyObject::NamedTuple { values: items, .. } => {
items.iter().any(|item| exceeds_depth(item, budget - 1))
}
MontyObject::Dict(pairs) => pairs
.into_iter()
.any(|(k, v)| exceeds_depth(k, budget - 1) || exceeds_depth(v, budget - 1)),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn round_trips_json_native_values() {
let cases = [
json!(null),
json!(true),
json!(42),
json!(3.5),
json!("hello"),
json!([1, 2, 3]),
json!({"nested": {"n": 7}, "list": [1, "two"]}),
];
for case in cases {
let monty = json_to_monty(case.clone());
assert_eq!(monty_to_json(&monty), case);
}
}
#[test]
fn large_u64_degrades_to_float() {
let big = json!(u64::MAX);
assert!(matches!(json_to_monty(big), MontyObject::Float(_)));
}
#[test]
fn non_json_native_value_degrades_to_repr() {
let tuple = MontyObject::Tuple(vec![MontyObject::Int(1), MontyObject::Int(2)]);
assert_eq!(monty_to_json(&tuple), json!([1, 2]));
let bytes = MontyObject::Bytes(vec![1, 2]);
assert!(monty_to_json(&bytes).is_string());
}
#[test]
fn nan_and_infinity_degrade_to_null() {
for f in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert_eq!(monty_to_json(&MontyObject::Float(f)), Value::Null);
}
}
#[test]
fn path_projects_to_string() {
let path = MontyObject::Path("/data/report.csv".to_string());
assert_eq!(monty_to_json(&path), json!("/data/report.csv"));
}
#[test]
fn colliding_dict_keys_last_one_wins() {
let pairs = DictPairs::from(vec![
(MontyObject::Int(1), MontyObject::String("int".to_string())),
(MontyObject::String("1".to_string()), MontyObject::String("str".to_string())),
]);
assert_eq!(monty_to_json(&MontyObject::Dict(pairs)), json!({"1": "str"}));
}
#[test]
fn deep_monty_nesting_degrades_instead_of_overflowing() {
let mut obj = MontyObject::Int(1);
for _ in 0..2_000 {
obj = MontyObject::List(vec![obj]);
}
let json = monty_to_json(&obj);
assert!(json.is_array());
assert!(json.to_string().contains(DEPTH_PLACEHOLDER));
}
#[test]
fn deep_json_nesting_degrades_instead_of_overflowing() {
let mut value = json!(1);
for _ in 0..2_000 {
value = Value::Array(vec![value]);
}
let monty = json_to_monty(value);
assert!(matches!(monty, MontyObject::List(_)));
}
#[test]
fn deep_tuple_dict_key_degrades_instead_of_overflowing() {
let mut key = MontyObject::Tuple(vec![MontyObject::Int(1)]);
for _ in 0..2_000 {
key = MontyObject::Tuple(vec![key]);
}
let pairs = DictPairs::from(vec![(key, MontyObject::Int(1))]);
let json = monty_to_json(&MontyObject::Dict(pairs));
assert_eq!(json, json!({DEPTH_PLACEHOLDER: 1}));
}
#[test]
fn shallow_values_are_unaffected_by_the_depth_cap() {
let value = json!({"a": [{"b": [{"c": 1}]}]});
assert_eq!(monty_to_json(&json_to_monty(value.clone())), value);
}
}