Skip to main content

haystack_core/codecs/json/
v4.rs

1// JSON v4 codec — application/json with `_kind` discriminator.
2
3use crate::codecs::shared;
4use crate::codecs::{Codec, CodecError};
5use crate::data::{HCol, HDict, HGrid};
6use crate::kinds::*;
7use chrono::{NaiveDate, NaiveTime, Timelike};
8use serde_json::{Map, Value};
9
10/// JSON v4 wire format codec (application/json).
11///
12/// Uses `_kind` discriminator for type-tagged values.
13pub struct Json4Codec;
14
15impl Codec for Json4Codec {
16    fn mime_type(&self) -> &str {
17        "application/json"
18    }
19
20    fn encode_grid(&self, grid: &HGrid) -> Result<String, CodecError> {
21        let val = encode_grid_value(grid)?;
22        serde_json::to_string(&val).map_err(|e| CodecError::Encode(e.to_string()))
23    }
24
25    fn decode_grid(&self, input: &str) -> Result<HGrid, CodecError> {
26        let val: Value = serde_json::from_str(input).map_err(|e| CodecError::Parse {
27            pos: 0,
28            message: e.to_string(),
29        })?;
30        decode_grid_value(&val)
31    }
32
33    fn encode_scalar(&self, val: &Kind) -> Result<String, CodecError> {
34        let json = encode_kind(val)?;
35        serde_json::to_string(&json).map_err(|e| CodecError::Encode(e.to_string()))
36    }
37
38    fn decode_scalar(&self, input: &str) -> Result<Kind, CodecError> {
39        let val: Value = serde_json::from_str(input).map_err(|e| CodecError::Parse {
40            pos: 0,
41            message: e.to_string(),
42        })?;
43        decode_kind(&val)
44    }
45}
46
47// ── Encoding ──
48
49/// Encode a Kind value to a serde_json Value.
50pub fn encode_kind(val: &Kind) -> Result<Value, CodecError> {
51    match val {
52        Kind::Null => Ok(Value::Null),
53        Kind::Bool(b) => Ok(Value::Bool(*b)),
54        Kind::Marker => Ok(kind_obj("marker", |_| {})),
55        Kind::NA => Ok(kind_obj("na", |_| {})),
56        Kind::Remove => Ok(kind_obj("remove", |_| {})),
57        Kind::Number(n) => Ok(encode_number(n)),
58        Kind::Str(s) => Ok(Value::String(s.clone())),
59        Kind::Ref(r) => Ok(encode_ref(r)),
60        Kind::Uri(u) => Ok(kind_obj("uri", |m| {
61            m.insert("val".into(), Value::String(u.val().to_string()));
62        })),
63        Kind::Symbol(s) => Ok(kind_obj("symbol", |m| {
64            m.insert("val".into(), Value::String(s.val().to_string()));
65        })),
66        Kind::Date(d) => Ok(kind_obj("date", |m| {
67            m.insert(
68                "val".into(),
69                Value::String(d.format("%Y-%m-%d").to_string()),
70            );
71        })),
72        Kind::Time(t) => Ok(kind_obj("time", |m| {
73            m.insert("val".into(), Value::String(encode_time_str(t)));
74        })),
75        Kind::DateTime(hdt) => Ok(encode_datetime(hdt)),
76        Kind::Coord(c) => Ok(kind_obj("coord", |m| {
77            m.insert("lat".into(), json_number(c.lat));
78            m.insert("lng".into(), json_number(c.lng));
79        })),
80        Kind::XStr(x) => Ok(kind_obj("xstr", |m| {
81            m.insert("type".into(), Value::String(x.type_name.clone()));
82            m.insert("val".into(), Value::String(x.val.clone()));
83        })),
84        Kind::List(items) => {
85            let arr: Result<Vec<Value>, CodecError> = items.iter().map(encode_kind).collect();
86            Ok(Value::Array(arr?))
87        }
88        Kind::Dict(d) => encode_dict(d),
89        Kind::Grid(g) => encode_grid_value(g).map(Value::Object),
90    }
91}
92
93/// Build a `{"_kind": kind_name, ...}` JSON object.
94fn kind_obj(kind_name: &str, f: impl FnOnce(&mut Map<String, Value>)) -> Value {
95    let mut m = Map::new();
96    m.insert("_kind".into(), Value::String(kind_name.into()));
97    f(&mut m);
98    Value::Object(m)
99}
100
101/// Encode a Number to JSON v4 format.
102fn encode_number(n: &Number) -> Value {
103    let mut m = Map::new();
104    m.insert("_kind".into(), Value::String("number".into()));
105    if n.val.is_infinite() {
106        if n.val > 0.0 {
107            m.insert("val".into(), Value::String("INF".into()));
108        } else {
109            m.insert("val".into(), Value::String("-INF".into()));
110        }
111    } else if n.val.is_nan() {
112        m.insert("val".into(), Value::String("NaN".into()));
113    } else {
114        m.insert("val".into(), json_number(n.val));
115    }
116    if let Some(ref u) = n.unit {
117        m.insert("unit".into(), Value::String(u.clone()));
118    }
119    Value::Object(m)
120}
121
122/// Encode a Ref to JSON v4 format.
123fn encode_ref(r: &HRef) -> Value {
124    kind_obj("ref", |m| {
125        m.insert("val".into(), Value::String(r.val.clone()));
126        if let Some(ref dis) = r.dis {
127            m.insert("dis".into(), Value::String(dis.clone()));
128        }
129    })
130}
131
132/// Encode a DateTime to JSON v4 format.
133fn encode_datetime(hdt: &HDateTime) -> Value {
134    let dt_str = hdt.dt.format("%Y-%m-%dT%H:%M:%S").to_string();
135    let frac = shared::format_frac_seconds(hdt.dt.nanosecond());
136    let offset_str = hdt.dt.format("%:z").to_string();
137    let val_str = format!("{dt_str}{frac}{offset_str}");
138
139    kind_obj("dateTime", |m| {
140        m.insert("val".into(), Value::String(val_str));
141        if !hdt.tz_name.is_empty() {
142            m.insert("tz".into(), Value::String(hdt.tz_name.clone()));
143        }
144    })
145}
146
147/// Format a NaiveTime to string (HH:MM:SS with optional fractional seconds).
148fn encode_time_str(t: &NaiveTime) -> String {
149    shared::format_time(t)
150}
151
152/// Encode an HDict as a plain JSON object (no _kind key).
153fn encode_dict(d: &HDict) -> Result<Value, CodecError> {
154    let mut m = Map::new();
155    for (k, v) in d.sorted_tags() {
156        m.insert(k.to_string(), encode_kind(v)?);
157    }
158    Ok(Value::Object(m))
159}
160
161/// Encode an HGrid as a JSON object (with `_kind: "grid"`).
162fn encode_grid_value(grid: &HGrid) -> Result<Map<String, Value>, CodecError> {
163    let mut m = Map::new();
164    m.insert("_kind".into(), Value::String("grid".into()));
165
166    // meta — only emit when non-empty
167    if !grid.meta.is_empty() {
168        let meta_val = encode_dict(&grid.meta)?;
169        m.insert("meta".into(), meta_val);
170    }
171
172    // cols
173    let cols: Result<Vec<Value>, CodecError> = grid
174        .cols
175        .iter()
176        .map(|col| {
177            let mut cm = Map::new();
178            cm.insert("name".into(), Value::String(col.name.clone()));
179            if !col.meta.is_empty() {
180                let meta_val = encode_dict(&col.meta)?;
181                cm.insert("meta".into(), meta_val);
182            }
183            Ok(Value::Object(cm))
184        })
185        .collect();
186    m.insert("cols".into(), Value::Array(cols?));
187
188    // rows
189    let rows: Result<Vec<Value>, CodecError> = grid.rows.iter().map(encode_dict).collect();
190    m.insert("rows".into(), Value::Array(rows?));
191
192    Ok(m)
193}
194
195/// Create a serde_json Number from an f64, handling integer display.
196fn json_number(v: f64) -> Value {
197    // serde_json::Number::from_f64 returns None for NaN/Infinity
198    match serde_json::Number::from_f64(v) {
199        Some(n) => Value::Number(n),
200        None => Value::String(format!("{v}")),
201    }
202}
203
204// ── Decoding ──
205
206/// Maximum nesting depth for recursive JSON decoding to prevent stack overflow.
207const MAX_NESTING_DEPTH: usize = 64;
208
209/// Decode a serde_json Value to a Kind.
210pub fn decode_kind(val: &Value) -> Result<Kind, CodecError> {
211    decode_kind_depth(val, 0)
212}
213
214/// Decode a serde_json Value to a Kind, tracking recursion depth.
215fn decode_kind_depth(val: &Value, depth: usize) -> Result<Kind, CodecError> {
216    if depth > MAX_NESTING_DEPTH {
217        return Err(CodecError::Parse {
218            pos: 0,
219            message: "maximum nesting depth exceeded".into(),
220        });
221    }
222    match val {
223        Value::Null => Ok(Kind::Null),
224        Value::Bool(b) => Ok(Kind::Bool(*b)),
225        Value::Number(n) => {
226            // Plain JSON numbers decode as Number(val, None)
227            let v = n.as_f64().ok_or_else(|| CodecError::Parse {
228                pos: 0,
229                message: format!("cannot convert JSON number to f64: {n}"),
230            })?;
231            Ok(Kind::Number(Number::unitless(v)))
232        }
233        Value::String(s) => {
234            // Plain JSON strings decode as Str
235            Ok(Kind::Str(s.clone()))
236        }
237        Value::Array(arr) => {
238            let items: Result<Vec<Kind>, CodecError> = arr
239                .iter()
240                .map(|v| decode_kind_depth(v, depth + 1))
241                .collect();
242            Ok(Kind::List(items?))
243        }
244        Value::Object(m) => decode_object_depth(m, depth + 1),
245    }
246}
247
248/// Decode a JSON object, tracking recursion depth.
249fn decode_object_depth(m: &Map<String, Value>, depth: usize) -> Result<Kind, CodecError> {
250    if depth > MAX_NESTING_DEPTH {
251        return Err(CodecError::Parse {
252            pos: 0,
253            message: "maximum nesting depth exceeded".into(),
254        });
255    }
256    match m.get("_kind") {
257        Some(Value::String(k)) => decode_typed_depth(k, m, depth),
258        _ => {
259            // Plain dict — decode all values
260            let mut dict = HDict::new();
261            for (key, val) in m {
262                dict.set(key.clone(), decode_kind_depth(val, depth + 1)?);
263            }
264            Ok(Kind::Dict(Box::new(dict)))
265        }
266    }
267}
268
269/// Decode a type-tagged JSON object (depth-aware).
270fn decode_typed_depth(
271    kind: &str,
272    m: &Map<String, Value>,
273    depth: usize,
274) -> Result<Kind, CodecError> {
275    match kind {
276        "marker" => Ok(Kind::Marker),
277        "na" => Ok(Kind::NA),
278        "remove" => Ok(Kind::Remove),
279        "number" => decode_number(m),
280        "ref" => decode_ref(m),
281        "uri" => {
282            let val = get_str(m, "val")?;
283            Ok(Kind::Uri(Uri::new(val)))
284        }
285        "symbol" => {
286            let val = get_str(m, "val")?;
287            Ok(Kind::Symbol(Symbol::new(val)))
288        }
289        "date" => {
290            let val = get_str(m, "val")?;
291            let d = NaiveDate::parse_from_str(&val, "%Y-%m-%d").map_err(|e| CodecError::Parse {
292                pos: 0,
293                message: format!("invalid date: {e}"),
294            })?;
295            Ok(Kind::Date(d))
296        }
297        "time" => {
298            let val = get_str(m, "val")?;
299            let t = parse_time(&val)?;
300            Ok(Kind::Time(t))
301        }
302        "dateTime" => decode_datetime(m),
303        "coord" => decode_coord(m),
304        "xstr" => {
305            let type_name = get_str(m, "type")?;
306            let val = get_str(m, "val")?;
307            Ok(Kind::XStr(XStr::new(type_name, val)))
308        }
309        "grid" => {
310            let grid = decode_grid_value_depth(&Value::Object(m.clone()), depth)?;
311            Ok(Kind::Grid(Box::new(grid)))
312        }
313        other => Err(CodecError::Parse {
314            pos: 0,
315            message: format!("unknown _kind: {other}"),
316        }),
317    }
318}
319
320/// Decode a number from a `_kind: "number"` object.
321fn decode_number(m: &Map<String, Value>) -> Result<Kind, CodecError> {
322    let val = m.get("val").ok_or_else(|| CodecError::Parse {
323        pos: 0,
324        message: "number missing 'val' field".into(),
325    })?;
326    let v = match val {
327        Value::Number(n) => n.as_f64().ok_or_else(|| CodecError::Parse {
328            pos: 0,
329            message: "cannot convert number val to f64".into(),
330        })?,
331        Value::String(s) => match s.as_str() {
332            "INF" => f64::INFINITY,
333            "-INF" => f64::NEG_INFINITY,
334            "NaN" => f64::NAN,
335            _ => s.parse::<f64>().map_err(|e| CodecError::Parse {
336                pos: 0,
337                message: format!("invalid number string: {e}"),
338            })?,
339        },
340        _ => {
341            return Err(CodecError::Parse {
342                pos: 0,
343                message: "number 'val' must be a number or string".into(),
344            });
345        }
346    };
347    let unit = match m.get("unit") {
348        Some(Value::String(u)) => Some(u.clone()),
349        _ => None,
350    };
351    Ok(Kind::Number(Number::new(v, unit)))
352}
353
354/// Decode a ref from a `_kind: "ref"` object.
355fn decode_ref(m: &Map<String, Value>) -> Result<Kind, CodecError> {
356    let val = get_str(m, "val")?;
357    let dis = match m.get("dis") {
358        Some(Value::String(d)) => Some(d.clone()),
359        _ => None,
360    };
361    Ok(Kind::Ref(HRef::new(val, dis)))
362}
363
364/// Decode a datetime from a `_kind: "dateTime"` object.
365fn decode_datetime(m: &Map<String, Value>) -> Result<Kind, CodecError> {
366    let val = get_str(m, "val")?;
367    let dt = chrono::DateTime::parse_from_rfc3339(&val)
368        .or_else(|_| {
369            // Try a more lenient format
370            chrono::DateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%S%:z")
371        })
372        .map_err(|e| CodecError::Parse {
373            pos: 0,
374            message: format!("invalid datetime: {e}"),
375        })?;
376    let tz = match m.get("tz") {
377        Some(Value::String(t)) => t.clone(),
378        _ => String::new(),
379    };
380    Ok(Kind::DateTime(HDateTime::new(dt, tz)))
381}
382
383/// Decode a coord from a `_kind: "coord"` object.
384fn decode_coord(m: &Map<String, Value>) -> Result<Kind, CodecError> {
385    let lat = get_f64(m, "lat")?;
386    let lng = get_f64(m, "lng")?;
387    Ok(Kind::Coord(Coord::new(lat, lng)))
388}
389
390/// Decode a grid from a JSON Value.
391pub fn decode_grid_value(val: &Value) -> Result<HGrid, CodecError> {
392    decode_grid_value_depth(val, 0)
393}
394
395/// Decode a grid from a JSON Value, tracking recursion depth.
396fn decode_grid_value_depth(val: &Value, depth: usize) -> Result<HGrid, CodecError> {
397    if depth > MAX_NESTING_DEPTH {
398        return Err(CodecError::Parse {
399            pos: 0,
400            message: "maximum nesting depth exceeded".into(),
401        });
402    }
403
404    let m = match val {
405        Value::Object(m) => m,
406        _ => {
407            return Err(CodecError::Parse {
408                pos: 0,
409                message: "grid must be a JSON object".into(),
410            });
411        }
412    };
413
414    // meta
415    let meta = match m.get("meta") {
416        Some(Value::Object(meta_map)) => {
417            let mut dict = HDict::new();
418            for (key, val) in meta_map {
419                if key == "_kind" {
420                    continue; // skip _kind in meta if present
421                }
422                dict.set(key.clone(), decode_kind_depth(val, depth + 1)?);
423            }
424            dict
425        }
426        _ => HDict::new(),
427    };
428
429    // cols
430    let cols = match m.get("cols") {
431        Some(Value::Array(arr)) => {
432            let mut cols = Vec::with_capacity(arr.len());
433            for col_val in arr {
434                let col_obj = col_val.as_object().ok_or_else(|| CodecError::Parse {
435                    pos: 0,
436                    message: "col must be a JSON object".into(),
437                })?;
438                let name = get_str(col_obj, "name")?;
439                let col_meta = match col_obj.get("meta") {
440                    Some(Value::Object(meta_map)) => {
441                        let mut dict = HDict::new();
442                        for (key, val) in meta_map {
443                            dict.set(key.clone(), decode_kind_depth(val, depth + 1)?);
444                        }
445                        dict
446                    }
447                    _ => HDict::new(),
448                };
449                cols.push(HCol::with_meta(name, col_meta));
450            }
451            cols
452        }
453        _ => Vec::new(),
454    };
455
456    // rows
457    let rows = match m.get("rows") {
458        Some(Value::Array(arr)) => {
459            let mut rows = Vec::with_capacity(arr.len());
460            for row_val in arr {
461                let row_obj = row_val.as_object().ok_or_else(|| CodecError::Parse {
462                    pos: 0,
463                    message: "row must be a JSON object".into(),
464                })?;
465                let mut dict = HDict::new();
466                for (key, val) in row_obj {
467                    dict.set(key.clone(), decode_kind_depth(val, depth + 1)?);
468                }
469                rows.push(dict);
470            }
471            rows
472        }
473        _ => Vec::new(),
474    };
475
476    Ok(HGrid::from_parts(meta, cols, rows))
477}
478
479/// Parse a time string (HH:MM:SS with optional fractional seconds).
480fn parse_time(s: &str) -> Result<NaiveTime, CodecError> {
481    // Try with fractional seconds first, then without
482    NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
483        .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S"))
484        .map_err(|e| CodecError::Parse {
485            pos: 0,
486            message: format!("invalid time: {e}"),
487        })
488}
489
490/// Helper: get a string field from a JSON object.
491fn get_str(m: &Map<String, Value>, key: &str) -> Result<String, CodecError> {
492    match m.get(key) {
493        Some(Value::String(s)) => Ok(s.clone()),
494        _ => Err(CodecError::Parse {
495            pos: 0,
496            message: format!("missing or invalid string field '{key}'"),
497        }),
498    }
499}
500
501/// Helper: get an f64 field from a JSON object.
502fn get_f64(m: &Map<String, Value>, key: &str) -> Result<f64, CodecError> {
503    match m.get(key) {
504        Some(Value::Number(n)) => n.as_f64().ok_or_else(|| CodecError::Parse {
505            pos: 0,
506            message: format!("cannot convert '{key}' to f64"),
507        }),
508        _ => Err(CodecError::Parse {
509            pos: 0,
510            message: format!("missing or invalid number field '{key}'"),
511        }),
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518    use crate::data::{HCol, HDict, HGrid};
519    use chrono::{FixedOffset, NaiveDate, NaiveTime, TimeZone};
520
521    fn roundtrip_scalar(kind: Kind) -> Kind {
522        let codec = Json4Codec;
523        let encoded = codec.encode_scalar(&kind).unwrap();
524        codec.decode_scalar(&encoded).unwrap()
525    }
526
527    // ── Null ──
528
529    #[test]
530    fn null_roundtrip() {
531        assert_eq!(roundtrip_scalar(Kind::Null), Kind::Null);
532    }
533
534    #[test]
535    fn null_encodes_to_json_null() {
536        let codec = Json4Codec;
537        assert_eq!(codec.encode_scalar(&Kind::Null).unwrap(), "null");
538    }
539
540    // ── Bool ──
541
542    #[test]
543    fn bool_true_roundtrip() {
544        assert_eq!(roundtrip_scalar(Kind::Bool(true)), Kind::Bool(true));
545    }
546
547    #[test]
548    fn bool_false_roundtrip() {
549        assert_eq!(roundtrip_scalar(Kind::Bool(false)), Kind::Bool(false));
550    }
551
552    #[test]
553    fn bool_encodes_to_json_bool() {
554        let codec = Json4Codec;
555        assert_eq!(codec.encode_scalar(&Kind::Bool(true)).unwrap(), "true");
556        assert_eq!(codec.encode_scalar(&Kind::Bool(false)).unwrap(), "false");
557    }
558
559    // ── Marker ──
560
561    #[test]
562    fn marker_roundtrip() {
563        assert_eq!(roundtrip_scalar(Kind::Marker), Kind::Marker);
564    }
565
566    #[test]
567    fn marker_encodes_with_kind() {
568        let codec = Json4Codec;
569        let encoded = codec.encode_scalar(&Kind::Marker).unwrap();
570        assert!(encoded.contains("\"_kind\""));
571        assert!(encoded.contains("\"marker\""));
572    }
573
574    // ── NA ──
575
576    #[test]
577    fn na_roundtrip() {
578        assert_eq!(roundtrip_scalar(Kind::NA), Kind::NA);
579    }
580
581    // ── Remove ──
582
583    #[test]
584    fn remove_roundtrip() {
585        assert_eq!(roundtrip_scalar(Kind::Remove), Kind::Remove);
586    }
587
588    // ── Number ──
589
590    #[test]
591    fn number_unitless_roundtrip() {
592        let k = Kind::Number(Number::unitless(72.5));
593        assert_eq!(roundtrip_scalar(k.clone()), k);
594    }
595
596    #[test]
597    fn number_with_unit_roundtrip() {
598        let k = Kind::Number(Number::new(72.5, Some("\u{00B0}F".into())));
599        assert_eq!(roundtrip_scalar(k.clone()), k);
600    }
601
602    #[test]
603    fn number_zero_roundtrip() {
604        let k = Kind::Number(Number::unitless(0.0));
605        assert_eq!(roundtrip_scalar(k.clone()), k);
606    }
607
608    #[test]
609    fn number_negative_roundtrip() {
610        let k = Kind::Number(Number::new(-23.45, Some("m\u{00B2}".into())));
611        assert_eq!(roundtrip_scalar(k.clone()), k);
612    }
613
614    #[test]
615    fn number_inf_roundtrip() {
616        let k = Kind::Number(Number::unitless(f64::INFINITY));
617        assert_eq!(roundtrip_scalar(k.clone()), k);
618    }
619
620    #[test]
621    fn number_neg_inf_roundtrip() {
622        let k = Kind::Number(Number::unitless(f64::NEG_INFINITY));
623        assert_eq!(roundtrip_scalar(k.clone()), k);
624    }
625
626    #[test]
627    fn number_nan_roundtrip() {
628        let codec = Json4Codec;
629        let k = Kind::Number(Number::unitless(f64::NAN));
630        let encoded = codec.encode_scalar(&k).unwrap();
631        let decoded = codec.decode_scalar(&encoded).unwrap();
632        match decoded {
633            Kind::Number(n) => {
634                assert!(n.val.is_nan());
635                assert_eq!(n.unit, None);
636            }
637            other => panic!("expected Number, got {other:?}"),
638        }
639    }
640
641    #[test]
642    fn number_integer_roundtrip() {
643        let k = Kind::Number(Number::unitless(42.0));
644        assert_eq!(roundtrip_scalar(k.clone()), k);
645    }
646
647    #[test]
648    fn plain_json_number_decodes_as_number() {
649        let codec = Json4Codec;
650        let decoded = codec.decode_scalar("42.5").unwrap();
651        assert_eq!(decoded, Kind::Number(Number::unitless(42.5)));
652    }
653
654    #[test]
655    fn plain_json_integer_decodes_as_number() {
656        let codec = Json4Codec;
657        let decoded = codec.decode_scalar("100").unwrap();
658        assert_eq!(decoded, Kind::Number(Number::unitless(100.0)));
659    }
660
661    // ── String ──
662
663    #[test]
664    fn string_simple_roundtrip() {
665        let k = Kind::Str("hello".into());
666        assert_eq!(roundtrip_scalar(k.clone()), k);
667    }
668
669    #[test]
670    fn string_empty_roundtrip() {
671        let k = Kind::Str(String::new());
672        assert_eq!(roundtrip_scalar(k.clone()), k);
673    }
674
675    #[test]
676    fn string_with_special_chars_roundtrip() {
677        let k = Kind::Str("line1\nline2\ttab".into());
678        assert_eq!(roundtrip_scalar(k.clone()), k);
679    }
680
681    #[test]
682    fn string_encodes_as_plain_json_string() {
683        let codec = Json4Codec;
684        let encoded = codec.encode_scalar(&Kind::Str("hello".into())).unwrap();
685        assert_eq!(encoded, "\"hello\"");
686    }
687
688    #[test]
689    fn plain_json_string_decodes_as_str() {
690        let codec = Json4Codec;
691        let decoded = codec.decode_scalar("\"world\"").unwrap();
692        assert_eq!(decoded, Kind::Str("world".into()));
693    }
694
695    // ── Ref ──
696
697    #[test]
698    fn ref_simple_roundtrip() {
699        let k = Kind::Ref(HRef::from_val("site-1"));
700        assert_eq!(roundtrip_scalar(k.clone()), k);
701    }
702
703    #[test]
704    fn ref_with_dis_roundtrip() {
705        let k = Kind::Ref(HRef::new("site-1", Some("Main Site".into())));
706        let rt = roundtrip_scalar(k.clone());
707        match rt {
708            Kind::Ref(r) => {
709                assert_eq!(r.val, "site-1");
710                assert_eq!(r.dis, Some("Main Site".into()));
711            }
712            other => panic!("expected Ref, got {other:?}"),
713        }
714    }
715
716    // ── Uri ──
717
718    #[test]
719    fn uri_roundtrip() {
720        let k = Kind::Uri(Uri::new("http://example.com/api"));
721        assert_eq!(roundtrip_scalar(k.clone()), k);
722    }
723
724    // ── Symbol ──
725
726    #[test]
727    fn symbol_roundtrip() {
728        let k = Kind::Symbol(Symbol::new("hot-water"));
729        assert_eq!(roundtrip_scalar(k.clone()), k);
730    }
731
732    // ── Date ──
733
734    #[test]
735    fn date_roundtrip() {
736        let k = Kind::Date(NaiveDate::from_ymd_opt(2024, 3, 13).unwrap());
737        assert_eq!(roundtrip_scalar(k.clone()), k);
738    }
739
740    // ── Time ──
741
742    #[test]
743    fn time_roundtrip() {
744        let k = Kind::Time(NaiveTime::from_hms_opt(8, 12, 5).unwrap());
745        assert_eq!(roundtrip_scalar(k.clone()), k);
746    }
747
748    #[test]
749    fn time_with_frac_roundtrip() {
750        let k = Kind::Time(NaiveTime::from_hms_milli_opt(14, 30, 0, 123).unwrap());
751        assert_eq!(roundtrip_scalar(k.clone()), k);
752    }
753
754    // ── DateTime ──
755
756    #[test]
757    fn datetime_roundtrip() {
758        let offset = FixedOffset::west_opt(5 * 3600).unwrap();
759        let dt = offset.with_ymd_and_hms(2024, 1, 1, 8, 12, 5).unwrap();
760        let k = Kind::DateTime(HDateTime::new(dt, "New_York"));
761        assert_eq!(roundtrip_scalar(k.clone()), k);
762    }
763
764    #[test]
765    fn datetime_utc_roundtrip() {
766        let offset = FixedOffset::east_opt(0).unwrap();
767        let dt = offset.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
768        let k = Kind::DateTime(HDateTime::new(dt, "UTC"));
769        assert_eq!(roundtrip_scalar(k.clone()), k);
770    }
771
772    // ── Coord ──
773
774    #[test]
775    fn coord_roundtrip() {
776        let k = Kind::Coord(Coord::new(37.5458266, -77.4491888));
777        assert_eq!(roundtrip_scalar(k.clone()), k);
778    }
779
780    // ── XStr ──
781
782    #[test]
783    fn xstr_roundtrip() {
784        let k = Kind::XStr(XStr::new("Color", "red"));
785        assert_eq!(roundtrip_scalar(k.clone()), k);
786    }
787
788    // ── List ──
789
790    #[test]
791    fn list_empty_roundtrip() {
792        let k = Kind::List(vec![]);
793        assert_eq!(roundtrip_scalar(k.clone()), k);
794    }
795
796    #[test]
797    fn list_mixed_roundtrip() {
798        let k = Kind::List(vec![
799            Kind::Number(Number::unitless(1.0)),
800            Kind::Str("two".into()),
801            Kind::Marker,
802            Kind::Bool(true),
803            Kind::Null,
804        ]);
805        assert_eq!(roundtrip_scalar(k.clone()), k);
806    }
807
808    #[test]
809    fn list_nested_roundtrip() {
810        let k = Kind::List(vec![
811            Kind::List(vec![Kind::Number(Number::unitless(1.0))]),
812            Kind::List(vec![Kind::Str("inner".into())]),
813        ]);
814        assert_eq!(roundtrip_scalar(k.clone()), k);
815    }
816
817    // ── Dict ──
818
819    #[test]
820    fn dict_empty_roundtrip() {
821        let k = Kind::Dict(Box::new(HDict::new()));
822        assert_eq!(roundtrip_scalar(k.clone()), k);
823    }
824
825    #[test]
826    fn dict_with_values_roundtrip() {
827        let mut d = HDict::new();
828        d.set("site", Kind::Marker);
829        d.set("dis", Kind::Str("Main".into()));
830        d.set(
831            "area",
832            Kind::Number(Number::new(4500.0, Some("ft\u{00B2}".into()))),
833        );
834        let k = Kind::Dict(Box::new(d));
835        assert_eq!(roundtrip_scalar(k.clone()), k);
836    }
837
838    #[test]
839    fn dict_no_kind_key() {
840        // A dict should not have _kind in output — it's a plain JSON object
841        let codec = Json4Codec;
842        let mut d = HDict::new();
843        d.set("site", Kind::Marker);
844        let k = Kind::Dict(Box::new(d));
845        let encoded = codec.encode_scalar(&k).unwrap();
846        // The _kind should only appear inside the marker value, not at the dict level
847        let val: Value = serde_json::from_str(&encoded).unwrap();
848        let obj = val.as_object().unwrap();
849        assert!(obj.get("_kind").is_none());
850    }
851
852    // ── Grid ──
853
854    #[test]
855    fn grid_empty_roundtrip() {
856        let codec = Json4Codec;
857        let g = HGrid::new();
858        let encoded = codec.encode_grid(&g).unwrap();
859        let decoded = codec.decode_grid(&encoded).unwrap();
860        assert!(decoded.is_empty());
861        assert_eq!(decoded.num_cols(), 0);
862    }
863
864    #[test]
865    fn grid_with_data_roundtrip() {
866        let codec = Json4Codec;
867
868        let cols = vec![HCol::new("dis"), HCol::new("area")];
869        let mut row1 = HDict::new();
870        row1.set("dis", Kind::Str("Site One".into()));
871        row1.set("area", Kind::Number(Number::unitless(4500.0)));
872        let mut row2 = HDict::new();
873        row2.set("dis", Kind::Str("Site Two".into()));
874        row2.set("area", Kind::Number(Number::unitless(3200.0)));
875
876        let g = HGrid::from_parts(HDict::new(), cols, vec![row1, row2]);
877        let encoded = codec.encode_grid(&g).unwrap();
878        let decoded = codec.decode_grid(&encoded).unwrap();
879
880        assert_eq!(decoded.num_cols(), 2);
881        assert_eq!(decoded.len(), 2);
882        assert_eq!(decoded.col_names().collect::<Vec<_>>(), vec!["dis", "area"]);
883        assert_eq!(
884            decoded.row(0).unwrap().get("dis"),
885            Some(&Kind::Str("Site One".into()))
886        );
887        assert_eq!(
888            decoded.row(1).unwrap().get("dis"),
889            Some(&Kind::Str("Site Two".into()))
890        );
891    }
892
893    #[test]
894    fn grid_with_meta_roundtrip() {
895        let codec = Json4Codec;
896
897        let mut meta = HDict::new();
898        meta.set("err", Kind::Marker);
899        meta.set("dis", Kind::Str("some error".into()));
900
901        let g = HGrid::from_parts(meta, vec![], vec![]);
902        let encoded = codec.encode_grid(&g).unwrap();
903        let decoded = codec.decode_grid(&encoded).unwrap();
904
905        assert!(decoded.is_err());
906        assert_eq!(
907            decoded.meta.get("dis"),
908            Some(&Kind::Str("some error".into()))
909        );
910    }
911
912    #[test]
913    fn grid_with_col_meta_roundtrip() {
914        let codec = Json4Codec;
915
916        let mut col_meta = HDict::new();
917        col_meta.set("unit", Kind::Str("kW".into()));
918
919        let cols = vec![HCol::new("name"), HCol::with_meta("power", col_meta)];
920        let g = HGrid::from_parts(HDict::new(), cols, vec![]);
921        let encoded = codec.encode_grid(&g).unwrap();
922        let decoded = codec.decode_grid(&encoded).unwrap();
923
924        assert_eq!(decoded.num_cols(), 2);
925        let power_col = decoded.col("power").unwrap();
926        assert_eq!(power_col.meta.get("unit"), Some(&Kind::Str("kW".into())));
927    }
928
929    #[test]
930    fn grid_with_missing_cells() {
931        let codec = Json4Codec;
932
933        let cols = vec![HCol::new("a"), HCol::new("b")];
934        let mut row1 = HDict::new();
935        row1.set("a", Kind::Number(Number::unitless(1.0)));
936        // b missing in row1
937
938        let g = HGrid::from_parts(HDict::new(), cols, vec![row1]);
939        let encoded = codec.encode_grid(&g).unwrap();
940        let decoded = codec.decode_grid(&encoded).unwrap();
941
942        let r = decoded.row(0).unwrap();
943        assert!(r.has("a"));
944        assert!(r.missing("b"));
945    }
946
947    #[test]
948    fn grid_nested_in_scalar() {
949        let codec = Json4Codec;
950
951        let cols = vec![HCol::new("x")];
952        let mut row = HDict::new();
953        row.set("x", Kind::Number(Number::unitless(42.0)));
954        let g = HGrid::from_parts(HDict::new(), cols, vec![row]);
955
956        let k = Kind::Grid(Box::new(g));
957        let encoded = codec.encode_scalar(&k).unwrap();
958        let decoded = codec.decode_scalar(&encoded).unwrap();
959
960        match decoded {
961            Kind::Grid(g) => {
962                assert_eq!(g.len(), 1);
963                assert_eq!(g.num_cols(), 1);
964            }
965            other => panic!("expected Grid, got {other:?}"),
966        }
967    }
968
969    // ── Edge cases ──
970
971    #[test]
972    fn decode_plain_object_as_dict() {
973        let codec = Json4Codec;
974        let decoded = codec.decode_scalar(r#"{"a": 1, "b": "hello"}"#).unwrap();
975        match decoded {
976            Kind::Dict(d) => {
977                assert_eq!(d.get("a"), Some(&Kind::Number(Number::unitless(1.0))));
978                assert_eq!(d.get("b"), Some(&Kind::Str("hello".into())));
979            }
980            other => panic!("expected Dict, got {other:?}"),
981        }
982    }
983
984    #[test]
985    fn number_with_unit_encoding_format() {
986        let codec = Json4Codec;
987        let k = Kind::Number(Number::new(72.5, Some("\u{00B0}F".into())));
988        let encoded = codec.encode_scalar(&k).unwrap();
989        let val: Value = serde_json::from_str(&encoded).unwrap();
990        let obj = val.as_object().unwrap();
991        assert_eq!(obj.get("_kind").unwrap(), "number");
992        assert_eq!(obj.get("val").unwrap(), 72.5);
993        assert_eq!(obj.get("unit").unwrap(), "\u{00B0}F");
994    }
995
996    #[test]
997    fn number_inf_encoding_format() {
998        let codec = Json4Codec;
999        let k = Kind::Number(Number::unitless(f64::INFINITY));
1000        let encoded = codec.encode_scalar(&k).unwrap();
1001        let val: Value = serde_json::from_str(&encoded).unwrap();
1002        let obj = val.as_object().unwrap();
1003        assert_eq!(obj.get("val").unwrap(), "INF");
1004    }
1005
1006    #[test]
1007    fn number_nan_encoding_format() {
1008        let codec = Json4Codec;
1009        let k = Kind::Number(Number::unitless(f64::NAN));
1010        let encoded = codec.encode_scalar(&k).unwrap();
1011        let val: Value = serde_json::from_str(&encoded).unwrap();
1012        let obj = val.as_object().unwrap();
1013        assert_eq!(obj.get("val").unwrap(), "NaN");
1014    }
1015
1016    #[test]
1017    fn nested_dict_with_typed_values() {
1018        let mut inner = HDict::new();
1019        inner.set(
1020            "temp",
1021            Kind::Number(Number::new(72.5, Some("\u{00B0}F".into()))),
1022        );
1023        inner.set("site", Kind::Ref(HRef::from_val("s1")));
1024        let k = Kind::Dict(Box::new(inner));
1025        assert_eq!(roundtrip_scalar(k.clone()), k);
1026    }
1027
1028    #[test]
1029    fn list_with_all_types() {
1030        let offset = FixedOffset::west_opt(5 * 3600).unwrap();
1031        let dt = offset.with_ymd_and_hms(2024, 1, 1, 8, 0, 0).unwrap();
1032        let k = Kind::List(vec![
1033            Kind::Null,
1034            Kind::Marker,
1035            Kind::NA,
1036            Kind::Remove,
1037            Kind::Bool(true),
1038            Kind::Number(Number::new(42.0, Some("kW".into()))),
1039            Kind::Str("hello".into()),
1040            Kind::Ref(HRef::new("x", Some("Dis".into()))),
1041            Kind::Uri(Uri::new("http://a.com")),
1042            Kind::Symbol(Symbol::new("tag")),
1043            Kind::Date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
1044            Kind::Time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()),
1045            Kind::DateTime(HDateTime::new(dt, "New_York")),
1046            Kind::Coord(Coord::new(37.5, -77.4)),
1047            Kind::XStr(XStr::new("Color", "red")),
1048        ]);
1049        assert_eq!(roundtrip_scalar(k.clone()), k);
1050    }
1051}