Skip to main content

haystack_core/codecs/json/
v3.rs

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