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_iter() {
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/// Decode a serde_json Value to a Kind.
207pub fn decode_kind(val: &Value) -> Result<Kind, CodecError> {
208    match val {
209        Value::Null => Ok(Kind::Null),
210        Value::Bool(b) => Ok(Kind::Bool(*b)),
211        Value::Number(n) => {
212            // Plain JSON numbers decode as Number(val, None)
213            let v = n.as_f64().ok_or_else(|| CodecError::Parse {
214                pos: 0,
215                message: format!("cannot convert JSON number to f64: {n}"),
216            })?;
217            Ok(Kind::Number(Number::unitless(v)))
218        }
219        Value::String(s) => {
220            // Plain JSON strings decode as Str
221            Ok(Kind::Str(s.clone()))
222        }
223        Value::Array(arr) => {
224            let items: Result<Vec<Kind>, CodecError> = arr.iter().map(decode_kind).collect();
225            Ok(Kind::List(items?))
226        }
227        Value::Object(m) => decode_object(m),
228    }
229}
230
231/// Decode a JSON object (may be a typed value with _kind, a grid, or a plain dict).
232fn decode_object(m: &Map<String, Value>) -> Result<Kind, CodecError> {
233    match m.get("_kind") {
234        Some(Value::String(k)) => decode_typed(k, m),
235        _ => {
236            // Plain dict — decode all values
237            let mut dict = HDict::new();
238            for (key, val) in m {
239                dict.set(key.clone(), decode_kind(val)?);
240            }
241            Ok(Kind::Dict(Box::new(dict)))
242        }
243    }
244}
245
246/// Decode a type-tagged JSON object.
247fn decode_typed(kind: &str, m: &Map<String, Value>) -> Result<Kind, CodecError> {
248    match kind {
249        "marker" => Ok(Kind::Marker),
250        "na" => Ok(Kind::NA),
251        "remove" => Ok(Kind::Remove),
252        "number" => decode_number(m),
253        "ref" => decode_ref(m),
254        "uri" => {
255            let val = get_str(m, "val")?;
256            Ok(Kind::Uri(Uri::new(val)))
257        }
258        "symbol" => {
259            let val = get_str(m, "val")?;
260            Ok(Kind::Symbol(Symbol::new(val)))
261        }
262        "date" => {
263            let val = get_str(m, "val")?;
264            let d = NaiveDate::parse_from_str(&val, "%Y-%m-%d").map_err(|e| CodecError::Parse {
265                pos: 0,
266                message: format!("invalid date: {e}"),
267            })?;
268            Ok(Kind::Date(d))
269        }
270        "time" => {
271            let val = get_str(m, "val")?;
272            let t = parse_time(&val)?;
273            Ok(Kind::Time(t))
274        }
275        "dateTime" => decode_datetime(m),
276        "coord" => decode_coord(m),
277        "xstr" => {
278            let type_name = get_str(m, "type")?;
279            let val = get_str(m, "val")?;
280            Ok(Kind::XStr(XStr::new(type_name, val)))
281        }
282        "grid" => {
283            let grid = decode_grid_value(&Value::Object(m.clone()))?;
284            Ok(Kind::Grid(Box::new(grid)))
285        }
286        other => Err(CodecError::Parse {
287            pos: 0,
288            message: format!("unknown _kind: {other}"),
289        }),
290    }
291}
292
293/// Decode a number from a `_kind: "number"` object.
294fn decode_number(m: &Map<String, Value>) -> Result<Kind, CodecError> {
295    let val = m.get("val").ok_or_else(|| CodecError::Parse {
296        pos: 0,
297        message: "number missing 'val' field".into(),
298    })?;
299    let v = match val {
300        Value::Number(n) => n.as_f64().ok_or_else(|| CodecError::Parse {
301            pos: 0,
302            message: "cannot convert number val to f64".into(),
303        })?,
304        Value::String(s) => match s.as_str() {
305            "INF" => f64::INFINITY,
306            "-INF" => f64::NEG_INFINITY,
307            "NaN" => f64::NAN,
308            _ => s.parse::<f64>().map_err(|e| CodecError::Parse {
309                pos: 0,
310                message: format!("invalid number string: {e}"),
311            })?,
312        },
313        _ => {
314            return Err(CodecError::Parse {
315                pos: 0,
316                message: "number 'val' must be a number or string".into(),
317            });
318        }
319    };
320    let unit = match m.get("unit") {
321        Some(Value::String(u)) => Some(u.clone()),
322        _ => None,
323    };
324    Ok(Kind::Number(Number::new(v, unit)))
325}
326
327/// Decode a ref from a `_kind: "ref"` object.
328fn decode_ref(m: &Map<String, Value>) -> Result<Kind, CodecError> {
329    let val = get_str(m, "val")?;
330    let dis = match m.get("dis") {
331        Some(Value::String(d)) => Some(d.clone()),
332        _ => None,
333    };
334    Ok(Kind::Ref(HRef::new(val, dis)))
335}
336
337/// Decode a datetime from a `_kind: "dateTime"` object.
338fn decode_datetime(m: &Map<String, Value>) -> Result<Kind, CodecError> {
339    let val = get_str(m, "val")?;
340    let dt = chrono::DateTime::parse_from_rfc3339(&val)
341        .or_else(|_| {
342            // Try a more lenient format
343            chrono::DateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%S%:z")
344        })
345        .map_err(|e| CodecError::Parse {
346            pos: 0,
347            message: format!("invalid datetime: {e}"),
348        })?;
349    let tz = match m.get("tz") {
350        Some(Value::String(t)) => t.clone(),
351        _ => String::new(),
352    };
353    Ok(Kind::DateTime(HDateTime::new(dt, tz)))
354}
355
356/// Decode a coord from a `_kind: "coord"` object.
357fn decode_coord(m: &Map<String, Value>) -> Result<Kind, CodecError> {
358    let lat = get_f64(m, "lat")?;
359    let lng = get_f64(m, "lng")?;
360    Ok(Kind::Coord(Coord::new(lat, lng)))
361}
362
363/// Decode a grid from a JSON Value.
364pub fn decode_grid_value(val: &Value) -> Result<HGrid, CodecError> {
365    let m = match val {
366        Value::Object(m) => m,
367        _ => {
368            return Err(CodecError::Parse {
369                pos: 0,
370                message: "grid must be a JSON object".into(),
371            });
372        }
373    };
374
375    // meta
376    let meta = match m.get("meta") {
377        Some(Value::Object(meta_map)) => {
378            let mut dict = HDict::new();
379            for (key, val) in meta_map {
380                if key == "_kind" {
381                    continue; // skip _kind in meta if present
382                }
383                dict.set(key.clone(), decode_kind(val)?);
384            }
385            dict
386        }
387        _ => HDict::new(),
388    };
389
390    // cols
391    let cols = match m.get("cols") {
392        Some(Value::Array(arr)) => {
393            let mut cols = Vec::with_capacity(arr.len());
394            for col_val in arr {
395                let col_obj = col_val.as_object().ok_or_else(|| CodecError::Parse {
396                    pos: 0,
397                    message: "col must be a JSON object".into(),
398                })?;
399                let name = get_str(col_obj, "name")?;
400                let col_meta = match col_obj.get("meta") {
401                    Some(Value::Object(meta_map)) => {
402                        let mut dict = HDict::new();
403                        for (key, val) in meta_map {
404                            dict.set(key.clone(), decode_kind(val)?);
405                        }
406                        dict
407                    }
408                    _ => HDict::new(),
409                };
410                cols.push(HCol::with_meta(name, col_meta));
411            }
412            cols
413        }
414        _ => Vec::new(),
415    };
416
417    // rows
418    let rows = match m.get("rows") {
419        Some(Value::Array(arr)) => {
420            let mut rows = Vec::with_capacity(arr.len());
421            for row_val in arr {
422                let row_obj = row_val.as_object().ok_or_else(|| CodecError::Parse {
423                    pos: 0,
424                    message: "row must be a JSON object".into(),
425                })?;
426                let mut dict = HDict::new();
427                for (key, val) in row_obj {
428                    dict.set(key.clone(), decode_kind(val)?);
429                }
430                rows.push(dict);
431            }
432            rows
433        }
434        _ => Vec::new(),
435    };
436
437    Ok(HGrid::from_parts(meta, cols, rows))
438}
439
440/// Parse a time string (HH:MM:SS with optional fractional seconds).
441fn parse_time(s: &str) -> Result<NaiveTime, CodecError> {
442    // Try with fractional seconds first, then without
443    NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
444        .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S"))
445        .map_err(|e| CodecError::Parse {
446            pos: 0,
447            message: format!("invalid time: {e}"),
448        })
449}
450
451/// Helper: get a string field from a JSON object.
452fn get_str(m: &Map<String, Value>, key: &str) -> Result<String, CodecError> {
453    match m.get(key) {
454        Some(Value::String(s)) => Ok(s.clone()),
455        _ => Err(CodecError::Parse {
456            pos: 0,
457            message: format!("missing or invalid string field '{key}'"),
458        }),
459    }
460}
461
462/// Helper: get an f64 field from a JSON object.
463fn get_f64(m: &Map<String, Value>, key: &str) -> Result<f64, CodecError> {
464    match m.get(key) {
465        Some(Value::Number(n)) => n.as_f64().ok_or_else(|| CodecError::Parse {
466            pos: 0,
467            message: format!("cannot convert '{key}' to f64"),
468        }),
469        _ => Err(CodecError::Parse {
470            pos: 0,
471            message: format!("missing or invalid number field '{key}'"),
472        }),
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479    use crate::data::{HCol, HDict, HGrid};
480    use chrono::{FixedOffset, NaiveDate, NaiveTime, TimeZone};
481
482    fn roundtrip_scalar(kind: Kind) -> Kind {
483        let codec = Json4Codec;
484        let encoded = codec.encode_scalar(&kind).unwrap();
485        codec.decode_scalar(&encoded).unwrap()
486    }
487
488    // ── Null ──
489
490    #[test]
491    fn null_roundtrip() {
492        assert_eq!(roundtrip_scalar(Kind::Null), Kind::Null);
493    }
494
495    #[test]
496    fn null_encodes_to_json_null() {
497        let codec = Json4Codec;
498        assert_eq!(codec.encode_scalar(&Kind::Null).unwrap(), "null");
499    }
500
501    // ── Bool ──
502
503    #[test]
504    fn bool_true_roundtrip() {
505        assert_eq!(roundtrip_scalar(Kind::Bool(true)), Kind::Bool(true));
506    }
507
508    #[test]
509    fn bool_false_roundtrip() {
510        assert_eq!(roundtrip_scalar(Kind::Bool(false)), Kind::Bool(false));
511    }
512
513    #[test]
514    fn bool_encodes_to_json_bool() {
515        let codec = Json4Codec;
516        assert_eq!(codec.encode_scalar(&Kind::Bool(true)).unwrap(), "true");
517        assert_eq!(codec.encode_scalar(&Kind::Bool(false)).unwrap(), "false");
518    }
519
520    // ── Marker ──
521
522    #[test]
523    fn marker_roundtrip() {
524        assert_eq!(roundtrip_scalar(Kind::Marker), Kind::Marker);
525    }
526
527    #[test]
528    fn marker_encodes_with_kind() {
529        let codec = Json4Codec;
530        let encoded = codec.encode_scalar(&Kind::Marker).unwrap();
531        assert!(encoded.contains("\"_kind\""));
532        assert!(encoded.contains("\"marker\""));
533    }
534
535    // ── NA ──
536
537    #[test]
538    fn na_roundtrip() {
539        assert_eq!(roundtrip_scalar(Kind::NA), Kind::NA);
540    }
541
542    // ── Remove ──
543
544    #[test]
545    fn remove_roundtrip() {
546        assert_eq!(roundtrip_scalar(Kind::Remove), Kind::Remove);
547    }
548
549    // ── Number ──
550
551    #[test]
552    fn number_unitless_roundtrip() {
553        let k = Kind::Number(Number::unitless(72.5));
554        assert_eq!(roundtrip_scalar(k.clone()), k);
555    }
556
557    #[test]
558    fn number_with_unit_roundtrip() {
559        let k = Kind::Number(Number::new(72.5, Some("\u{00B0}F".into())));
560        assert_eq!(roundtrip_scalar(k.clone()), k);
561    }
562
563    #[test]
564    fn number_zero_roundtrip() {
565        let k = Kind::Number(Number::unitless(0.0));
566        assert_eq!(roundtrip_scalar(k.clone()), k);
567    }
568
569    #[test]
570    fn number_negative_roundtrip() {
571        let k = Kind::Number(Number::new(-23.45, Some("m\u{00B2}".into())));
572        assert_eq!(roundtrip_scalar(k.clone()), k);
573    }
574
575    #[test]
576    fn number_inf_roundtrip() {
577        let k = Kind::Number(Number::unitless(f64::INFINITY));
578        assert_eq!(roundtrip_scalar(k.clone()), k);
579    }
580
581    #[test]
582    fn number_neg_inf_roundtrip() {
583        let k = Kind::Number(Number::unitless(f64::NEG_INFINITY));
584        assert_eq!(roundtrip_scalar(k.clone()), k);
585    }
586
587    #[test]
588    fn number_nan_roundtrip() {
589        let codec = Json4Codec;
590        let k = Kind::Number(Number::unitless(f64::NAN));
591        let encoded = codec.encode_scalar(&k).unwrap();
592        let decoded = codec.decode_scalar(&encoded).unwrap();
593        match decoded {
594            Kind::Number(n) => {
595                assert!(n.val.is_nan());
596                assert_eq!(n.unit, None);
597            }
598            other => panic!("expected Number, got {other:?}"),
599        }
600    }
601
602    #[test]
603    fn number_integer_roundtrip() {
604        let k = Kind::Number(Number::unitless(42.0));
605        assert_eq!(roundtrip_scalar(k.clone()), k);
606    }
607
608    #[test]
609    fn plain_json_number_decodes_as_number() {
610        let codec = Json4Codec;
611        let decoded = codec.decode_scalar("42.5").unwrap();
612        assert_eq!(decoded, Kind::Number(Number::unitless(42.5)));
613    }
614
615    #[test]
616    fn plain_json_integer_decodes_as_number() {
617        let codec = Json4Codec;
618        let decoded = codec.decode_scalar("100").unwrap();
619        assert_eq!(decoded, Kind::Number(Number::unitless(100.0)));
620    }
621
622    // ── String ──
623
624    #[test]
625    fn string_simple_roundtrip() {
626        let k = Kind::Str("hello".into());
627        assert_eq!(roundtrip_scalar(k.clone()), k);
628    }
629
630    #[test]
631    fn string_empty_roundtrip() {
632        let k = Kind::Str(String::new());
633        assert_eq!(roundtrip_scalar(k.clone()), k);
634    }
635
636    #[test]
637    fn string_with_special_chars_roundtrip() {
638        let k = Kind::Str("line1\nline2\ttab".into());
639        assert_eq!(roundtrip_scalar(k.clone()), k);
640    }
641
642    #[test]
643    fn string_encodes_as_plain_json_string() {
644        let codec = Json4Codec;
645        let encoded = codec.encode_scalar(&Kind::Str("hello".into())).unwrap();
646        assert_eq!(encoded, "\"hello\"");
647    }
648
649    #[test]
650    fn plain_json_string_decodes_as_str() {
651        let codec = Json4Codec;
652        let decoded = codec.decode_scalar("\"world\"").unwrap();
653        assert_eq!(decoded, Kind::Str("world".into()));
654    }
655
656    // ── Ref ──
657
658    #[test]
659    fn ref_simple_roundtrip() {
660        let k = Kind::Ref(HRef::from_val("site-1"));
661        assert_eq!(roundtrip_scalar(k.clone()), k);
662    }
663
664    #[test]
665    fn ref_with_dis_roundtrip() {
666        let k = Kind::Ref(HRef::new("site-1", Some("Main Site".into())));
667        let rt = roundtrip_scalar(k.clone());
668        match rt {
669            Kind::Ref(r) => {
670                assert_eq!(r.val, "site-1");
671                assert_eq!(r.dis, Some("Main Site".into()));
672            }
673            other => panic!("expected Ref, got {other:?}"),
674        }
675    }
676
677    // ── Uri ──
678
679    #[test]
680    fn uri_roundtrip() {
681        let k = Kind::Uri(Uri::new("http://example.com/api"));
682        assert_eq!(roundtrip_scalar(k.clone()), k);
683    }
684
685    // ── Symbol ──
686
687    #[test]
688    fn symbol_roundtrip() {
689        let k = Kind::Symbol(Symbol::new("hot-water"));
690        assert_eq!(roundtrip_scalar(k.clone()), k);
691    }
692
693    // ── Date ──
694
695    #[test]
696    fn date_roundtrip() {
697        let k = Kind::Date(NaiveDate::from_ymd_opt(2024, 3, 13).unwrap());
698        assert_eq!(roundtrip_scalar(k.clone()), k);
699    }
700
701    // ── Time ──
702
703    #[test]
704    fn time_roundtrip() {
705        let k = Kind::Time(NaiveTime::from_hms_opt(8, 12, 5).unwrap());
706        assert_eq!(roundtrip_scalar(k.clone()), k);
707    }
708
709    #[test]
710    fn time_with_frac_roundtrip() {
711        let k = Kind::Time(NaiveTime::from_hms_milli_opt(14, 30, 0, 123).unwrap());
712        assert_eq!(roundtrip_scalar(k.clone()), k);
713    }
714
715    // ── DateTime ──
716
717    #[test]
718    fn datetime_roundtrip() {
719        let offset = FixedOffset::west_opt(5 * 3600).unwrap();
720        let dt = offset.with_ymd_and_hms(2024, 1, 1, 8, 12, 5).unwrap();
721        let k = Kind::DateTime(HDateTime::new(dt, "New_York"));
722        assert_eq!(roundtrip_scalar(k.clone()), k);
723    }
724
725    #[test]
726    fn datetime_utc_roundtrip() {
727        let offset = FixedOffset::east_opt(0).unwrap();
728        let dt = offset.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
729        let k = Kind::DateTime(HDateTime::new(dt, "UTC"));
730        assert_eq!(roundtrip_scalar(k.clone()), k);
731    }
732
733    // ── Coord ──
734
735    #[test]
736    fn coord_roundtrip() {
737        let k = Kind::Coord(Coord::new(37.5458266, -77.4491888));
738        assert_eq!(roundtrip_scalar(k.clone()), k);
739    }
740
741    // ── XStr ──
742
743    #[test]
744    fn xstr_roundtrip() {
745        let k = Kind::XStr(XStr::new("Color", "red"));
746        assert_eq!(roundtrip_scalar(k.clone()), k);
747    }
748
749    // ── List ──
750
751    #[test]
752    fn list_empty_roundtrip() {
753        let k = Kind::List(vec![]);
754        assert_eq!(roundtrip_scalar(k.clone()), k);
755    }
756
757    #[test]
758    fn list_mixed_roundtrip() {
759        let k = Kind::List(vec![
760            Kind::Number(Number::unitless(1.0)),
761            Kind::Str("two".into()),
762            Kind::Marker,
763            Kind::Bool(true),
764            Kind::Null,
765        ]);
766        assert_eq!(roundtrip_scalar(k.clone()), k);
767    }
768
769    #[test]
770    fn list_nested_roundtrip() {
771        let k = Kind::List(vec![
772            Kind::List(vec![Kind::Number(Number::unitless(1.0))]),
773            Kind::List(vec![Kind::Str("inner".into())]),
774        ]);
775        assert_eq!(roundtrip_scalar(k.clone()), k);
776    }
777
778    // ── Dict ──
779
780    #[test]
781    fn dict_empty_roundtrip() {
782        let k = Kind::Dict(Box::new(HDict::new()));
783        assert_eq!(roundtrip_scalar(k.clone()), k);
784    }
785
786    #[test]
787    fn dict_with_values_roundtrip() {
788        let mut d = HDict::new();
789        d.set("site", Kind::Marker);
790        d.set("dis", Kind::Str("Main".into()));
791        d.set(
792            "area",
793            Kind::Number(Number::new(4500.0, Some("ft\u{00B2}".into()))),
794        );
795        let k = Kind::Dict(Box::new(d));
796        assert_eq!(roundtrip_scalar(k.clone()), k);
797    }
798
799    #[test]
800    fn dict_no_kind_key() {
801        // A dict should not have _kind in output — it's a plain JSON object
802        let codec = Json4Codec;
803        let mut d = HDict::new();
804        d.set("site", Kind::Marker);
805        let k = Kind::Dict(Box::new(d));
806        let encoded = codec.encode_scalar(&k).unwrap();
807        // The _kind should only appear inside the marker value, not at the dict level
808        let val: Value = serde_json::from_str(&encoded).unwrap();
809        let obj = val.as_object().unwrap();
810        assert!(obj.get("_kind").is_none());
811    }
812
813    // ── Grid ──
814
815    #[test]
816    fn grid_empty_roundtrip() {
817        let codec = Json4Codec;
818        let g = HGrid::new();
819        let encoded = codec.encode_grid(&g).unwrap();
820        let decoded = codec.decode_grid(&encoded).unwrap();
821        assert!(decoded.is_empty());
822        assert_eq!(decoded.num_cols(), 0);
823    }
824
825    #[test]
826    fn grid_with_data_roundtrip() {
827        let codec = Json4Codec;
828
829        let cols = vec![HCol::new("dis"), HCol::new("area")];
830        let mut row1 = HDict::new();
831        row1.set("dis", Kind::Str("Site One".into()));
832        row1.set("area", Kind::Number(Number::unitless(4500.0)));
833        let mut row2 = HDict::new();
834        row2.set("dis", Kind::Str("Site Two".into()));
835        row2.set("area", Kind::Number(Number::unitless(3200.0)));
836
837        let g = HGrid::from_parts(HDict::new(), cols, vec![row1, row2]);
838        let encoded = codec.encode_grid(&g).unwrap();
839        let decoded = codec.decode_grid(&encoded).unwrap();
840
841        assert_eq!(decoded.num_cols(), 2);
842        assert_eq!(decoded.len(), 2);
843        assert_eq!(decoded.col_names().collect::<Vec<_>>(), vec!["dis", "area"]);
844        assert_eq!(
845            decoded.row(0).unwrap().get("dis"),
846            Some(&Kind::Str("Site One".into()))
847        );
848        assert_eq!(
849            decoded.row(1).unwrap().get("dis"),
850            Some(&Kind::Str("Site Two".into()))
851        );
852    }
853
854    #[test]
855    fn grid_with_meta_roundtrip() {
856        let codec = Json4Codec;
857
858        let mut meta = HDict::new();
859        meta.set("err", Kind::Marker);
860        meta.set("dis", Kind::Str("some error".into()));
861
862        let g = HGrid::from_parts(meta, vec![], vec![]);
863        let encoded = codec.encode_grid(&g).unwrap();
864        let decoded = codec.decode_grid(&encoded).unwrap();
865
866        assert!(decoded.is_err());
867        assert_eq!(
868            decoded.meta.get("dis"),
869            Some(&Kind::Str("some error".into()))
870        );
871    }
872
873    #[test]
874    fn grid_with_col_meta_roundtrip() {
875        let codec = Json4Codec;
876
877        let mut col_meta = HDict::new();
878        col_meta.set("unit", Kind::Str("kW".into()));
879
880        let cols = vec![HCol::new("name"), HCol::with_meta("power", col_meta)];
881        let g = HGrid::from_parts(HDict::new(), cols, vec![]);
882        let encoded = codec.encode_grid(&g).unwrap();
883        let decoded = codec.decode_grid(&encoded).unwrap();
884
885        assert_eq!(decoded.num_cols(), 2);
886        let power_col = decoded.col("power").unwrap();
887        assert_eq!(power_col.meta.get("unit"), Some(&Kind::Str("kW".into())));
888    }
889
890    #[test]
891    fn grid_with_missing_cells() {
892        let codec = Json4Codec;
893
894        let cols = vec![HCol::new("a"), HCol::new("b")];
895        let mut row1 = HDict::new();
896        row1.set("a", Kind::Number(Number::unitless(1.0)));
897        // b missing in row1
898
899        let g = HGrid::from_parts(HDict::new(), cols, vec![row1]);
900        let encoded = codec.encode_grid(&g).unwrap();
901        let decoded = codec.decode_grid(&encoded).unwrap();
902
903        let r = decoded.row(0).unwrap();
904        assert!(r.has("a"));
905        assert!(r.missing("b"));
906    }
907
908    #[test]
909    fn grid_nested_in_scalar() {
910        let codec = Json4Codec;
911
912        let cols = vec![HCol::new("x")];
913        let mut row = HDict::new();
914        row.set("x", Kind::Number(Number::unitless(42.0)));
915        let g = HGrid::from_parts(HDict::new(), cols, vec![row]);
916
917        let k = Kind::Grid(Box::new(g));
918        let encoded = codec.encode_scalar(&k).unwrap();
919        let decoded = codec.decode_scalar(&encoded).unwrap();
920
921        match decoded {
922            Kind::Grid(g) => {
923                assert_eq!(g.len(), 1);
924                assert_eq!(g.num_cols(), 1);
925            }
926            other => panic!("expected Grid, got {other:?}"),
927        }
928    }
929
930    // ── Edge cases ──
931
932    #[test]
933    fn decode_plain_object_as_dict() {
934        let codec = Json4Codec;
935        let decoded = codec.decode_scalar(r#"{"a": 1, "b": "hello"}"#).unwrap();
936        match decoded {
937            Kind::Dict(d) => {
938                assert_eq!(d.get("a"), Some(&Kind::Number(Number::unitless(1.0))));
939                assert_eq!(d.get("b"), Some(&Kind::Str("hello".into())));
940            }
941            other => panic!("expected Dict, got {other:?}"),
942        }
943    }
944
945    #[test]
946    fn number_with_unit_encoding_format() {
947        let codec = Json4Codec;
948        let k = Kind::Number(Number::new(72.5, Some("\u{00B0}F".into())));
949        let encoded = codec.encode_scalar(&k).unwrap();
950        let val: Value = serde_json::from_str(&encoded).unwrap();
951        let obj = val.as_object().unwrap();
952        assert_eq!(obj.get("_kind").unwrap(), "number");
953        assert_eq!(obj.get("val").unwrap(), 72.5);
954        assert_eq!(obj.get("unit").unwrap(), "\u{00B0}F");
955    }
956
957    #[test]
958    fn number_inf_encoding_format() {
959        let codec = Json4Codec;
960        let k = Kind::Number(Number::unitless(f64::INFINITY));
961        let encoded = codec.encode_scalar(&k).unwrap();
962        let val: Value = serde_json::from_str(&encoded).unwrap();
963        let obj = val.as_object().unwrap();
964        assert_eq!(obj.get("val").unwrap(), "INF");
965    }
966
967    #[test]
968    fn number_nan_encoding_format() {
969        let codec = Json4Codec;
970        let k = Kind::Number(Number::unitless(f64::NAN));
971        let encoded = codec.encode_scalar(&k).unwrap();
972        let val: Value = serde_json::from_str(&encoded).unwrap();
973        let obj = val.as_object().unwrap();
974        assert_eq!(obj.get("val").unwrap(), "NaN");
975    }
976
977    #[test]
978    fn nested_dict_with_typed_values() {
979        let mut inner = HDict::new();
980        inner.set(
981            "temp",
982            Kind::Number(Number::new(72.5, Some("\u{00B0}F".into()))),
983        );
984        inner.set("site", Kind::Ref(HRef::from_val("s1")));
985        let k = Kind::Dict(Box::new(inner));
986        assert_eq!(roundtrip_scalar(k.clone()), k);
987    }
988
989    #[test]
990    fn list_with_all_types() {
991        let offset = FixedOffset::west_opt(5 * 3600).unwrap();
992        let dt = offset.with_ymd_and_hms(2024, 1, 1, 8, 0, 0).unwrap();
993        let k = Kind::List(vec![
994            Kind::Null,
995            Kind::Marker,
996            Kind::NA,
997            Kind::Remove,
998            Kind::Bool(true),
999            Kind::Number(Number::new(42.0, Some("kW".into()))),
1000            Kind::Str("hello".into()),
1001            Kind::Ref(HRef::new("x", Some("Dis".into()))),
1002            Kind::Uri(Uri::new("http://a.com")),
1003            Kind::Symbol(Symbol::new("tag")),
1004            Kind::Date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
1005            Kind::Time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()),
1006            Kind::DateTime(HDateTime::new(dt, "New_York")),
1007            Kind::Coord(Coord::new(37.5, -77.4)),
1008            Kind::XStr(XStr::new("Color", "red")),
1009        ]);
1010        assert_eq!(roundtrip_scalar(k.clone()), k);
1011    }
1012}