Skip to main content

aver/replay/
json.rs

1use std::collections::{BTreeMap, HashMap};
2use std::fmt::Write as _;
3
4use crate::value::{Value, aver_repr, list_from_vec, list_view};
5
6mod parser;
7
8#[derive(Debug, Clone, PartialEq)]
9pub enum JsonValue {
10    Null,
11    Bool(bool),
12    Int(i64),
13    Float(f64),
14    String(String),
15    Array(Vec<JsonValue>),
16    Object(BTreeMap<String, JsonValue>),
17}
18
19pub fn parse_json(input: &str) -> Result<JsonValue, String> {
20    parser::parse_json(input)
21}
22
23pub fn json_to_string(value: &JsonValue) -> String {
24    let mut out = String::new();
25    write_json_compact(&mut out, value);
26    out
27}
28
29pub fn format_json(value: &JsonValue) -> String {
30    let mut out = String::new();
31    write_json_pretty(&mut out, value, 0);
32    out
33}
34
35fn get_required<'a>(
36    obj: &'a BTreeMap<String, JsonValue>,
37    key: &str,
38    path: &str,
39) -> Result<&'a JsonValue, String> {
40    obj.get(key)
41        .ok_or_else(|| format!("{}: missing required field '{}'", path, key))
42}
43
44fn expect_object<'a>(
45    value: &'a JsonValue,
46    path: &str,
47) -> Result<&'a BTreeMap<String, JsonValue>, String> {
48    match value {
49        JsonValue::Object(obj) => Ok(obj),
50        _ => Err(format!("{} must be an object", path)),
51    }
52}
53
54fn parse_array<'a>(value: &'a JsonValue, path: &str) -> Result<&'a Vec<JsonValue>, String> {
55    match value {
56        JsonValue::Array(arr) => Ok(arr),
57        _ => Err(format!("{} must be an array", path)),
58    }
59}
60
61fn parse_string<'a>(value: &'a JsonValue, path: &str) -> Result<&'a str, String> {
62    match value {
63        JsonValue::String(s) => Ok(s),
64        _ => Err(format!("{} must be a string", path)),
65    }
66}
67
68pub fn values_to_json(values: &[Value]) -> Result<Vec<JsonValue>, String> {
69    values.iter().map(value_to_json).collect()
70}
71
72pub fn values_to_json_lossy(values: &[Value]) -> Vec<JsonValue> {
73    values.iter().map(value_to_json_lossy).collect()
74}
75
76pub fn json_values_to_values(values: &[JsonValue]) -> Result<Vec<Value>, String> {
77    values.iter().map(json_to_value).collect()
78}
79
80pub fn value_to_json(value: &Value) -> Result<JsonValue, String> {
81    if let Some(items) = list_view(value) {
82        let mut arr = Vec::with_capacity(items.len());
83        for item in items.iter() {
84            arr.push(value_to_json(item)?);
85        }
86        return Ok(JsonValue::Array(arr));
87    }
88
89    match value {
90        Value::Int(i) => i
91            .to_i64()
92            .map(JsonValue::Int)
93            .ok_or_else(|| "cannot serialize an integer outside the 64-bit JSON range".to_string()),
94        Value::Float(f) => {
95            if !f.is_finite() {
96                return Err("cannot serialize non-finite float (NaN/inf)".to_string());
97            }
98            Ok(JsonValue::Float(*f))
99        }
100        Value::Str(s) => Ok(JsonValue::String(s.clone())),
101        Value::Bool(b) => Ok(JsonValue::Bool(*b)),
102        Value::Unit => Ok(JsonValue::Null),
103        Value::Ok(inner) => Ok(wrap_marker("$ok", value_to_json(inner)?)),
104        Value::Err(inner) => Ok(wrap_marker("$err", value_to_json(inner)?)),
105        Value::Some(inner) => Ok(wrap_marker("$some", value_to_json(inner)?)),
106        Value::None => Ok(wrap_marker("$none", JsonValue::Bool(true))),
107        Value::List(_) => unreachable!("handled via list_view above"),
108        Value::Tuple(items) => {
109            let mut arr = Vec::with_capacity(items.len());
110            for item in items {
111                arr.push(value_to_json(item)?);
112            }
113            Ok(wrap_marker("$tuple", JsonValue::Array(arr)))
114        }
115        Value::Map(entries) => {
116            if entries.keys().all(|k| matches!(k, Value::Str(_))) {
117                let mut obj = BTreeMap::new();
118                for (k, v) in entries {
119                    let Value::Str(key) = k else {
120                        unreachable!("checked above");
121                    };
122                    obj.insert(key.clone(), value_to_json(v)?);
123                }
124                Ok(JsonValue::Object(obj))
125            } else {
126                let mut pairs = Vec::with_capacity(entries.len());
127                for (k, v) in entries {
128                    pairs.push(JsonValue::Array(vec![value_to_json(k)?, value_to_json(v)?]));
129                }
130                Ok(wrap_marker("$map", JsonValue::Array(pairs)))
131            }
132        }
133        Value::Record { type_name, fields } => {
134            let mut fields_obj = BTreeMap::new();
135            for (name, field_value) in fields.iter() {
136                fields_obj.insert(name.clone(), value_to_json(field_value)?);
137            }
138            let mut payload = BTreeMap::new();
139            payload.insert("type".to_string(), JsonValue::String(type_name.clone()));
140            payload.insert("fields".to_string(), JsonValue::Object(fields_obj));
141            Ok(wrap_marker("$record", JsonValue::Object(payload)))
142        }
143        Value::Variant {
144            type_name,
145            variant,
146            fields,
147        } => {
148            let mut field_vals = Vec::with_capacity(fields.len());
149            for field in fields.iter() {
150                field_vals.push(value_to_json(field)?);
151            }
152            let mut payload = BTreeMap::new();
153            payload.insert("type".to_string(), JsonValue::String(type_name.clone()));
154            payload.insert("name".to_string(), JsonValue::String(variant.clone()));
155            payload.insert("fields".to_string(), JsonValue::Array(field_vals));
156            Ok(wrap_marker("$variant", JsonValue::Object(payload)))
157        }
158        Value::Vector(vec) => {
159            let mut arr = Vec::with_capacity(vec.len());
160            for item in vec.iter() {
161                arr.push(value_to_json(item)?);
162            }
163            Ok(wrap_marker("$vector", JsonValue::Array(arr)))
164        }
165        Value::Fn(_) | Value::Builtin(_) | Value::Namespace { .. } => Err(format!(
166            "cannot serialize non-replay-safe value: {}",
167            aver_repr(value)
168        )),
169    }
170}
171
172pub fn json_to_value(json: &JsonValue) -> Result<Value, String> {
173    match json {
174        JsonValue::Null => Ok(Value::Unit),
175        JsonValue::Bool(b) => Ok(Value::Bool(*b)),
176        JsonValue::Int(i) => Ok(Value::int(*i)),
177        JsonValue::Float(f) => Ok(Value::Float(*f)),
178        JsonValue::String(s) => Ok(Value::Str(s.clone())),
179        JsonValue::Array(items) => {
180            let mut out = Vec::with_capacity(items.len());
181            for item in items {
182                out.push(json_to_value(item)?);
183            }
184            Ok(list_from_vec(out))
185        }
186        JsonValue::Object(obj) => {
187            if let Some((marker, payload)) = marker_single_key(obj) {
188                return decode_marker(marker, payload);
189            }
190            let mut map = HashMap::with_capacity(obj.len());
191            for (k, v) in obj {
192                map.insert(Value::Str(k.clone()), json_to_value(v)?);
193            }
194            Ok(Value::Map(map))
195        }
196    }
197}
198
199pub fn first_diff_path(expected: &JsonValue, got: &JsonValue) -> Option<String> {
200    first_diff_path_inner(expected, got, "$")
201}
202
203pub fn value_to_json_lossy(value: &Value) -> JsonValue {
204    match value_to_json(value) {
205        Ok(v) => v,
206        Err(_) => {
207            let mut obj = BTreeMap::new();
208            obj.insert("$opaque".to_string(), JsonValue::String(aver_repr(value)));
209            JsonValue::Object(obj)
210        }
211    }
212}
213
214fn wrap_marker(name: &str, value: JsonValue) -> JsonValue {
215    let mut obj = BTreeMap::new();
216    obj.insert(name.to_string(), value);
217    JsonValue::Object(obj)
218}
219
220fn marker_single_key(obj: &BTreeMap<String, JsonValue>) -> Option<(&str, &JsonValue)> {
221    if obj.len() != 1 {
222        return None;
223    }
224    // Iron — B3: only treat a single-key object as a marker wrap when
225    // the key starts with `$`. All real markers (`$ok`, `$err`,
226    // `$some`, `$none`, `$tuple`, `$map`, `$record`, `$variant`,
227    // `$vector`) use that sigil. A user-level `Map` with a single
228    // string key (e.g. `{"a" → 0}`) also encodes as a single-key
229    // JSON object — without the `$`-gate, the decoder was treating
230    // every 1-entry string-keyed map as an attempted-but-unknown
231    // marker wrap and erroring `unknown replay marker 'a'`. Found
232    // by `tests/replay_proptest.rs`.
233    obj.iter().next().and_then(|(k, v)| {
234        if k.starts_with('$') {
235            Some((k.as_str(), v))
236        } else {
237            None
238        }
239    })
240}
241
242fn decode_marker(marker: &str, payload: &JsonValue) -> Result<Value, String> {
243    match marker {
244        "$ok" => Ok(Value::Ok(Box::new(json_to_value(payload)?))),
245        "$err" => Ok(Value::Err(Box::new(json_to_value(payload)?))),
246        "$some" => Ok(Value::Some(Box::new(json_to_value(payload)?))),
247        "$none" => Ok(Value::None),
248        "$tuple" => decode_tuple(payload),
249        "$map" => decode_map(payload),
250        "$record" => decode_record(payload),
251        "$variant" => decode_variant(payload),
252        "$vector" => decode_vector(payload),
253        _ => Err(format!("unknown replay marker '{}'", marker)),
254    }
255}
256
257/// Iron — B3: `value_to_json` emits `wrap_marker("$vector", JsonValue::Array(...))`
258/// for `Value::Vector`, but pre-Iron `decode_marker` had no
259/// matching arm — encoding produced JSON the decoder rejected
260/// with "unknown replay marker '$vector'". Any replay recording
261/// that captured a Vector value failed to load back. Found by
262/// the replay-codec roundtrip property test (`tests/replay_proptest.rs`).
263fn decode_vector(payload: &JsonValue) -> Result<Value, String> {
264    let items = parse_array(payload, "$vector")?;
265    let mut out = Vec::with_capacity(items.len());
266    for item in items {
267        out.push(json_to_value(item)?);
268    }
269    Ok(Value::Vector(aver_rt::AverVector::from_vec(out)))
270}
271
272fn decode_tuple(payload: &JsonValue) -> Result<Value, String> {
273    let items = parse_array(payload, "$tuple")?;
274    let mut out = Vec::with_capacity(items.len());
275    for item in items {
276        out.push(json_to_value(item)?);
277    }
278    Ok(Value::Tuple(out))
279}
280
281fn decode_map(payload: &JsonValue) -> Result<Value, String> {
282    let pairs = parse_array(payload, "$map")?;
283    let mut out = HashMap::with_capacity(pairs.len());
284    for (idx, pair_json) in pairs.iter().enumerate() {
285        let pair = parse_array(pair_json, &format!("$map[{}]", idx))?;
286        if pair.len() != 2 {
287            return Err(format!("$map[{}] must be a 2-element array", idx));
288        }
289        let key = json_to_value(&pair[0])?;
290        let value = json_to_value(&pair[1])?;
291        out.insert(key, value);
292    }
293    Ok(Value::Map(out))
294}
295
296fn decode_record(payload: &JsonValue) -> Result<Value, String> {
297    let obj = expect_object(payload, "$record")?;
298    let type_name =
299        parse_string(get_required(obj, "type", "$record")?, "$record.type")?.to_string();
300    let fields_obj = expect_object(get_required(obj, "fields", "$record")?, "$record.fields")?;
301    let mut fields = Vec::with_capacity(fields_obj.len());
302    for (key, field_val) in fields_obj {
303        fields.push((key.clone(), json_to_value(field_val)?));
304    }
305    Ok(Value::Record {
306        type_name,
307        fields: fields.into(),
308    })
309}
310
311fn decode_variant(payload: &JsonValue) -> Result<Value, String> {
312    let obj = expect_object(payload, "$variant")?;
313    let type_name =
314        parse_string(get_required(obj, "type", "$variant")?, "$variant.type")?.to_string();
315    let variant =
316        parse_string(get_required(obj, "name", "$variant")?, "$variant.name")?.to_string();
317    let fields_arr = parse_array(get_required(obj, "fields", "$variant")?, "$variant.fields")?;
318    let mut fields = Vec::with_capacity(fields_arr.len());
319    for val in fields_arr {
320        fields.push(json_to_value(val)?);
321    }
322    Ok(Value::Variant {
323        type_name,
324        variant,
325        fields: fields.into(),
326    })
327}
328
329fn first_diff_path_inner(expected: &JsonValue, got: &JsonValue, path: &str) -> Option<String> {
330    match (expected, got) {
331        (JsonValue::Object(a), JsonValue::Object(b)) => {
332            let mut keys = a.keys().chain(b.keys()).cloned().collect::<Vec<_>>();
333            keys.sort();
334            keys.dedup();
335            for key in keys {
336                let next_path = if path == "$" {
337                    format!("$.{}", key)
338                } else {
339                    format!("{}.{}", path, key)
340                };
341                match (a.get(&key), b.get(&key)) {
342                    (Some(av), Some(bv)) => {
343                        if let Some(diff) = first_diff_path_inner(av, bv, &next_path) {
344                            return Some(diff);
345                        }
346                    }
347                    _ => return Some(next_path),
348                }
349            }
350            None
351        }
352        (JsonValue::Array(a), JsonValue::Array(b)) => {
353            if a.len() != b.len() {
354                return Some(format!("{}[len]", path));
355            }
356            for (idx, (av, bv)) in a.iter().zip(b.iter()).enumerate() {
357                let next_path = format!("{}[{}]", path, idx);
358                if let Some(diff) = first_diff_path_inner(av, bv, &next_path) {
359                    return Some(diff);
360                }
361            }
362            None
363        }
364        _ => {
365            if expected == got {
366                None
367            } else {
368                Some(path.to_string())
369            }
370        }
371    }
372}
373
374fn write_json_compact(out: &mut String, value: &JsonValue) {
375    match value {
376        JsonValue::Null => out.push_str("null"),
377        JsonValue::Bool(true) => out.push_str("true"),
378        JsonValue::Bool(false) => out.push_str("false"),
379        JsonValue::Int(i) => {
380            let _ = write!(out, "{}", i);
381        }
382        JsonValue::Float(f) => out.push_str(&format_float(*f)),
383        JsonValue::String(s) => write_json_string(out, s),
384        JsonValue::Array(arr) => {
385            out.push('[');
386            for (idx, item) in arr.iter().enumerate() {
387                if idx > 0 {
388                    out.push(',');
389                }
390                write_json_compact(out, item);
391            }
392            out.push(']');
393        }
394        JsonValue::Object(obj) => {
395            out.push('{');
396            for (idx, (k, v)) in obj.iter().enumerate() {
397                if idx > 0 {
398                    out.push(',');
399                }
400                write_json_string(out, k);
401                out.push(':');
402                write_json_compact(out, v);
403            }
404            out.push('}');
405        }
406    }
407}
408
409fn write_json_pretty(out: &mut String, value: &JsonValue, indent: usize) {
410    match value {
411        JsonValue::Array(arr) => {
412            if arr.is_empty() {
413                out.push_str("[]");
414                return;
415            }
416            out.push_str("[\n");
417            for (idx, item) in arr.iter().enumerate() {
418                push_indent(out, indent + 2);
419                write_json_pretty(out, item, indent + 2);
420                if idx + 1 < arr.len() {
421                    out.push(',');
422                }
423                out.push('\n');
424            }
425            push_indent(out, indent);
426            out.push(']');
427        }
428        JsonValue::Object(obj) => {
429            if obj.is_empty() {
430                out.push_str("{}");
431                return;
432            }
433            out.push_str("{\n");
434            for (idx, (k, v)) in obj.iter().enumerate() {
435                push_indent(out, indent + 2);
436                write_json_string(out, k);
437                out.push_str(": ");
438                write_json_pretty(out, v, indent + 2);
439                if idx + 1 < obj.len() {
440                    out.push(',');
441                }
442                out.push('\n');
443            }
444            push_indent(out, indent);
445            out.push('}');
446        }
447        _ => write_json_compact(out, value),
448    }
449}
450
451fn push_indent(out: &mut String, indent: usize) {
452    for _ in 0..indent {
453        out.push(' ');
454    }
455}
456
457fn write_json_string(out: &mut String, s: &str) {
458    out.push('"');
459    for ch in s.chars() {
460        match ch {
461            '"' => out.push_str("\\\""),
462            '\\' => out.push_str("\\\\"),
463            '\n' => out.push_str("\\n"),
464            '\r' => out.push_str("\\r"),
465            '\t' => out.push_str("\\t"),
466            '\u{08}' => out.push_str("\\b"),
467            '\u{0C}' => out.push_str("\\f"),
468            c if c < '\u{20}' => {
469                let _ = write!(out, "\\u{:04X}", c as u32);
470            }
471            c => out.push(c),
472        }
473    }
474    out.push('"');
475}
476
477fn format_float(f: f64) -> String {
478    let mut s = format!("{}", f);
479    if !s.contains('.') && !s.contains('e') && !s.contains('E') {
480        s.push_str(".0");
481    }
482    s
483}