1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! 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)
);
}
}