1use 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
10pub 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
47pub 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
79fn 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
88fn 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
96fn 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
108fn encode_time_str(t: &NaiveTime) -> String {
110 shared::format_time(t)
111}
112
113fn encode_dict(d: &HDict) -> Result<Value, CodecError> {
115 let mut m = Map::new();
116 for (k, v) in d.sorted_iter() {
117 m.insert(k.to_string(), encode_kind(v)?);
118 }
119 Ok(Value::Object(m))
120}
121
122fn encode_grid_value(grid: &HGrid) -> Result<Map<String, Value>, CodecError> {
124 let mut m = Map::new();
125
126 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_iter() {
130 meta_map.insert(k.to_string(), encode_kind(v)?);
131 }
132 m.insert("meta".into(), Value::Object(meta_map));
133
134 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 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 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
161pub fn decode_kind(val: &Value) -> Result<Kind, CodecError> {
165 match val {
166 Value::Null => Ok(Kind::Null),
167 Value::Bool(b) => Ok(Kind::Bool(*b)),
168 Value::Number(n) => {
169 let v = n.as_f64().ok_or_else(|| CodecError::Parse {
171 pos: 0,
172 message: format!("cannot convert JSON number to f64: {n}"),
173 })?;
174 Ok(Kind::Number(Number::unitless(v)))
175 }
176 Value::String(s) => decode_prefixed_string(s),
177 Value::Array(arr) => {
178 let items: Result<Vec<Kind>, CodecError> = arr.iter().map(decode_kind).collect();
179 Ok(Kind::List(items?))
180 }
181 Value::Object(m) => decode_object(m),
182 }
183}
184
185fn decode_prefixed_string(s: &str) -> Result<Kind, CodecError> {
187 if s.len() >= 2 {
189 let prefix = &s[..2];
190 let rest = &s[2..];
191 match prefix {
192 "m:" => {
193 if rest.is_empty() {
194 return Ok(Kind::Marker);
195 }
196 }
197 "z:" => {
198 if rest.is_empty() {
199 return Ok(Kind::NA);
200 }
201 }
202 "-:" => {
203 if rest.is_empty() {
204 return Ok(Kind::Remove);
205 }
206 }
207 "s:" => return Ok(Kind::Str(rest.to_string())),
208 "n:" => return decode_number_str(rest),
209 "r:" => return decode_ref_str(rest),
210 "u:" => return Ok(Kind::Uri(Uri::new(rest))),
211 "y:" => return Ok(Kind::Symbol(Symbol::new(rest))),
212 "d:" => return decode_date_str(rest),
213 "h:" => return decode_time_str(rest),
214 "t:" => return decode_datetime_str(rest),
215 "c:" => return decode_coord_str(rest),
216 "x:" => return decode_xstr_str(rest),
217 _ => {}
218 }
219 }
220 Ok(Kind::Str(s.to_string()))
224}
225
226fn decode_number_str(s: &str) -> Result<Kind, CodecError> {
228 let (val_str, unit) = match s.find(' ') {
231 Some(pos) => {
232 let val_part = &s[..pos];
233 let unit_part = &s[pos + 1..];
234 (
235 val_part,
236 if unit_part.is_empty() {
237 None
238 } else {
239 Some(unit_part.to_string())
240 },
241 )
242 }
243 None => (s, None),
244 };
245
246 let v = match val_str {
247 "INF" => f64::INFINITY,
248 "-INF" => f64::NEG_INFINITY,
249 "NaN" => f64::NAN,
250 _ => val_str.parse::<f64>().map_err(|e| CodecError::Parse {
251 pos: 0,
252 message: format!("invalid v3 number: {e}"),
253 })?,
254 };
255 Ok(Kind::Number(Number::new(v, unit)))
256}
257
258fn decode_ref_str(s: &str) -> Result<Kind, CodecError> {
260 match s.find(' ') {
262 Some(pos) => {
263 let val = &s[..pos];
264 let dis = &s[pos + 1..];
265 Ok(Kind::Ref(HRef::new(val, Some(dis.to_string()))))
266 }
267 None => Ok(Kind::Ref(HRef::from_val(s))),
268 }
269}
270
271fn decode_date_str(s: &str) -> Result<Kind, CodecError> {
273 let d = NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| CodecError::Parse {
274 pos: 0,
275 message: format!("invalid v3 date: {e}"),
276 })?;
277 Ok(Kind::Date(d))
278}
279
280fn decode_time_str(s: &str) -> Result<Kind, CodecError> {
282 NaiveTime::parse_from_str(s, "%H:%M:%S%.f")
283 .or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S"))
284 .map(Kind::Time)
285 .map_err(|e| CodecError::Parse {
286 pos: 0,
287 message: format!("invalid v3 time: {e}"),
288 })
289}
290
291fn decode_datetime_str(s: &str) -> Result<Kind, CodecError> {
294 let (dt_str, tz_name) = split_datetime_tz(s);
300
301 let dt = chrono::DateTime::parse_from_rfc3339(dt_str)
302 .or_else(|_| chrono::DateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M:%S%:z"))
303 .or_else(|_| chrono::DateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M:%S%.f%:z"))
304 .map_err(|e| CodecError::Parse {
305 pos: 0,
306 message: format!("invalid v3 datetime: {e}"),
307 })?;
308
309 Ok(Kind::DateTime(HDateTime::new(dt, tz_name)))
310}
311
312fn split_datetime_tz(s: &str) -> (&str, &str) {
318 if let Some(t_pos) = s.find('T') {
323 let after_t = &s[t_pos..];
324 let offset_end = find_offset_end(after_t);
327 if let Some(end) = offset_end {
328 let abs_end = t_pos + end;
329 if abs_end < s.len() {
330 let rest = &s[abs_end..];
331 if let Some(space_pos) = rest.find(' ') {
332 let dt_part = &s[..abs_end + space_pos];
333 let tz_part = &s[abs_end + space_pos + 1..];
334 return (dt_part, tz_part);
335 }
336 }
337 return (s, "");
338 }
339 }
340 (s, "")
341}
342
343fn find_offset_end(s: &str) -> Option<usize> {
346 if s.ends_with('Z') {
348 return Some(s.len());
349 }
350 for (i, c) in s.char_indices().rev() {
353 if (c == '+' || c == '-') && i + 6 <= s.len() {
354 let candidate = &s[i..];
356 if candidate.len() >= 6 {
357 let hh = &candidate[1..3];
358 let colon = &candidate[3..4];
359 let mm = &candidate[4..6];
360 if colon == ":"
361 && hh.chars().all(|c| c.is_ascii_digit())
362 && mm.chars().all(|c| c.is_ascii_digit())
363 {
364 return Some(i + 6);
365 }
366 }
367 }
368 }
369 None
370}
371
372fn decode_coord_str(s: &str) -> Result<Kind, CodecError> {
374 let parts: Vec<&str> = s.splitn(2, ',').collect();
375 if parts.len() != 2 {
376 return Err(CodecError::Parse {
377 pos: 0,
378 message: format!("invalid v3 coord: expected 'lat,lng', got '{s}'"),
379 });
380 }
381 let lat = parts[0].parse::<f64>().map_err(|e| CodecError::Parse {
382 pos: 0,
383 message: format!("invalid v3 coord lat: {e}"),
384 })?;
385 let lng = parts[1].parse::<f64>().map_err(|e| CodecError::Parse {
386 pos: 0,
387 message: format!("invalid v3 coord lng: {e}"),
388 })?;
389 Ok(Kind::Coord(Coord::new(lat, lng)))
390}
391
392fn decode_xstr_str(s: &str) -> Result<Kind, CodecError> {
394 match s.find(':') {
395 Some(pos) => {
396 let type_name = &s[..pos];
397 let val = &s[pos + 1..];
398 Ok(Kind::XStr(XStr::new(type_name, val)))
399 }
400 None => Err(CodecError::Parse {
401 pos: 0,
402 message: format!("invalid v3 xstr: expected 'Type:value', got '{s}'"),
403 }),
404 }
405}
406
407fn decode_object(m: &Map<String, Value>) -> Result<Kind, CodecError> {
409 if m.contains_key("meta") && m.contains_key("cols") {
411 let grid = decode_grid_from_map(m)?;
412 return Ok(Kind::Grid(Box::new(grid)));
413 }
414 let mut dict = HDict::new();
416 for (key, val) in m {
417 dict.set(key.clone(), decode_kind(val)?);
418 }
419 Ok(Kind::Dict(Box::new(dict)))
420}
421
422pub fn decode_grid_value(val: &Value) -> Result<HGrid, CodecError> {
424 let m = match val {
425 Value::Object(m) => m,
426 _ => {
427 return Err(CodecError::Parse {
428 pos: 0,
429 message: "grid must be a JSON object".into(),
430 });
431 }
432 };
433 decode_grid_from_map(m)
434}
435
436fn decode_grid_from_map(m: &Map<String, Value>) -> Result<HGrid, CodecError> {
438 let meta = match m.get("meta") {
440 Some(Value::Object(meta_map)) => {
441 let mut dict = HDict::new();
442 for (key, val) in meta_map {
443 if key == "ver" {
444 continue;
445 }
446 dict.set(key.clone(), decode_kind(val)?);
447 }
448 dict
449 }
450 _ => HDict::new(),
451 };
452
453 let cols = match m.get("cols") {
455 Some(Value::Array(arr)) => {
456 let mut cols = Vec::with_capacity(arr.len());
457 for col_val in arr {
458 let col_obj = col_val.as_object().ok_or_else(|| CodecError::Parse {
459 pos: 0,
460 message: "col must be a JSON object".into(),
461 })?;
462 let name = match col_obj.get("name") {
463 Some(Value::String(n)) => n.clone(),
464 _ => {
465 return Err(CodecError::Parse {
466 pos: 0,
467 message: "col missing 'name' field".into(),
468 });
469 }
470 };
471 let mut col_meta = HDict::new();
472 for (key, val) in col_obj {
473 if key != "name" {
474 col_meta.set(key.clone(), decode_kind(val)?);
475 }
476 }
477 cols.push(HCol::with_meta(name, col_meta));
478 }
479 cols
480 }
481 _ => Vec::new(),
482 };
483
484 let rows = match m.get("rows") {
486 Some(Value::Array(arr)) => {
487 let mut rows = Vec::with_capacity(arr.len());
488 for row_val in arr {
489 let row_obj = row_val.as_object().ok_or_else(|| CodecError::Parse {
490 pos: 0,
491 message: "row must be a JSON object".into(),
492 })?;
493 let mut dict = HDict::new();
494 for (key, val) in row_obj {
495 dict.set(key.clone(), decode_kind(val)?);
496 }
497 rows.push(dict);
498 }
499 rows
500 }
501 _ => Vec::new(),
502 };
503
504 Ok(HGrid::from_parts(meta, cols, rows))
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use crate::data::{HCol, HDict, HGrid};
511 use chrono::{FixedOffset, NaiveDate, NaiveTime, TimeZone};
512
513 fn roundtrip_scalar(kind: Kind) -> Kind {
514 let codec = Json3Codec;
515 let encoded = codec.encode_scalar(&kind).unwrap();
516 codec.decode_scalar(&encoded).unwrap()
517 }
518
519 #[test]
522 fn null_roundtrip() {
523 assert_eq!(roundtrip_scalar(Kind::Null), Kind::Null);
524 }
525
526 #[test]
527 fn null_encodes_to_json_null() {
528 let codec = Json3Codec;
529 assert_eq!(codec.encode_scalar(&Kind::Null).unwrap(), "null");
530 }
531
532 #[test]
535 fn bool_true_roundtrip() {
536 assert_eq!(roundtrip_scalar(Kind::Bool(true)), Kind::Bool(true));
537 }
538
539 #[test]
540 fn bool_false_roundtrip() {
541 assert_eq!(roundtrip_scalar(Kind::Bool(false)), Kind::Bool(false));
542 }
543
544 #[test]
545 fn bool_encodes_to_json_bool() {
546 let codec = Json3Codec;
547 assert_eq!(codec.encode_scalar(&Kind::Bool(true)).unwrap(), "true");
548 assert_eq!(codec.encode_scalar(&Kind::Bool(false)).unwrap(), "false");
549 }
550
551 #[test]
554 fn marker_roundtrip() {
555 assert_eq!(roundtrip_scalar(Kind::Marker), Kind::Marker);
556 }
557
558 #[test]
559 fn marker_encodes_as_prefix() {
560 let codec = Json3Codec;
561 assert_eq!(codec.encode_scalar(&Kind::Marker).unwrap(), "\"m:\"");
562 }
563
564 #[test]
567 fn na_roundtrip() {
568 assert_eq!(roundtrip_scalar(Kind::NA), Kind::NA);
569 }
570
571 #[test]
572 fn na_encodes_as_prefix() {
573 let codec = Json3Codec;
574 assert_eq!(codec.encode_scalar(&Kind::NA).unwrap(), "\"z:\"");
575 }
576
577 #[test]
580 fn remove_roundtrip() {
581 assert_eq!(roundtrip_scalar(Kind::Remove), Kind::Remove);
582 }
583
584 #[test]
585 fn remove_encodes_as_prefix() {
586 let codec = Json3Codec;
587 assert_eq!(codec.encode_scalar(&Kind::Remove).unwrap(), "\"-:\"");
588 }
589
590 #[test]
593 fn number_unitless_roundtrip() {
594 let k = Kind::Number(Number::unitless(72.5));
595 assert_eq!(roundtrip_scalar(k.clone()), k);
596 }
597
598 #[test]
599 fn number_with_unit_roundtrip() {
600 let k = Kind::Number(Number::new(72.5, Some("\u{00B0}F".into())));
601 assert_eq!(roundtrip_scalar(k.clone()), k);
602 }
603
604 #[test]
605 fn number_zero_roundtrip() {
606 let k = Kind::Number(Number::unitless(0.0));
607 assert_eq!(roundtrip_scalar(k.clone()), k);
608 }
609
610 #[test]
611 fn number_negative_roundtrip() {
612 let k = Kind::Number(Number::new(-23.45, Some("m\u{00B2}".into())));
613 assert_eq!(roundtrip_scalar(k.clone()), k);
614 }
615
616 #[test]
617 fn number_integer_roundtrip() {
618 let k = Kind::Number(Number::unitless(42.0));
619 assert_eq!(roundtrip_scalar(k.clone()), k);
620 }
621
622 #[test]
623 fn number_inf_roundtrip() {
624 let k = Kind::Number(Number::unitless(f64::INFINITY));
625 assert_eq!(roundtrip_scalar(k.clone()), k);
626 }
627
628 #[test]
629 fn number_neg_inf_roundtrip() {
630 let k = Kind::Number(Number::unitless(f64::NEG_INFINITY));
631 assert_eq!(roundtrip_scalar(k.clone()), k);
632 }
633
634 #[test]
635 fn number_nan_roundtrip() {
636 let codec = Json3Codec;
637 let k = Kind::Number(Number::unitless(f64::NAN));
638 let encoded = codec.encode_scalar(&k).unwrap();
639 let decoded = codec.decode_scalar(&encoded).unwrap();
640 match decoded {
641 Kind::Number(n) => {
642 assert!(n.val.is_nan());
643 assert_eq!(n.unit, None);
644 }
645 other => panic!("expected Number, got {other:?}"),
646 }
647 }
648
649 #[test]
650 fn number_encoding_format() {
651 let codec = Json3Codec;
652 let k = Kind::Number(Number::new(72.5, Some("\u{00B0}F".into())));
653 assert_eq!(codec.encode_scalar(&k).unwrap(), "\"n:72.5 \u{00B0}F\"");
654 }
655
656 #[test]
657 fn number_unitless_encoding_format() {
658 let codec = Json3Codec;
659 let k = Kind::Number(Number::unitless(42.0));
660 assert_eq!(codec.encode_scalar(&k).unwrap(), "\"n:42\"");
661 }
662
663 #[test]
664 fn number_inf_encoding_format() {
665 let codec = Json3Codec;
666 let k = Kind::Number(Number::unitless(f64::INFINITY));
667 assert_eq!(codec.encode_scalar(&k).unwrap(), "\"n:INF\"");
668 }
669
670 #[test]
671 fn plain_json_number_decodes_as_number() {
672 let codec = Json3Codec;
673 let decoded = codec.decode_scalar("42.5").unwrap();
674 assert_eq!(decoded, Kind::Number(Number::unitless(42.5)));
675 }
676
677 #[test]
680 fn string_simple_roundtrip() {
681 let k = Kind::Str("hello".into());
682 assert_eq!(roundtrip_scalar(k.clone()), k);
683 }
684
685 #[test]
686 fn string_empty_roundtrip() {
687 let k = Kind::Str(String::new());
688 assert_eq!(roundtrip_scalar(k.clone()), k);
689 }
690
691 #[test]
692 fn string_with_special_chars_roundtrip() {
693 let k = Kind::Str("line1\nline2\ttab".into());
694 assert_eq!(roundtrip_scalar(k.clone()), k);
695 }
696
697 #[test]
698 fn string_encodes_with_s_prefix() {
699 let codec = Json3Codec;
700 assert_eq!(
701 codec.encode_scalar(&Kind::Str("hello".into())).unwrap(),
702 "\"s:hello\""
703 );
704 }
705
706 #[test]
707 fn string_empty_encodes_with_s_prefix() {
708 let codec = Json3Codec;
709 assert_eq!(
710 codec.encode_scalar(&Kind::Str(String::new())).unwrap(),
711 "\"s:\""
712 );
713 }
714
715 #[test]
718 fn ref_simple_roundtrip() {
719 let k = Kind::Ref(HRef::from_val("site-1"));
720 assert_eq!(roundtrip_scalar(k.clone()), k);
721 }
722
723 #[test]
724 fn ref_with_dis_roundtrip() {
725 let k = Kind::Ref(HRef::new("site-1", Some("Main Site".into())));
726 let rt = roundtrip_scalar(k);
727 match rt {
728 Kind::Ref(r) => {
729 assert_eq!(r.val, "site-1");
730 assert_eq!(r.dis, Some("Main Site".into()));
731 }
732 other => panic!("expected Ref, got {other:?}"),
733 }
734 }
735
736 #[test]
737 fn ref_encoding_format() {
738 let codec = Json3Codec;
739 let k = Kind::Ref(HRef::new("abc", Some("Display Name".into())));
740 assert_eq!(codec.encode_scalar(&k).unwrap(), "\"r:abc Display Name\"");
741 }
742
743 #[test]
746 fn uri_roundtrip() {
747 let k = Kind::Uri(Uri::new("http://example.com/api"));
748 assert_eq!(roundtrip_scalar(k.clone()), k);
749 }
750
751 #[test]
752 fn uri_encoding_format() {
753 let codec = Json3Codec;
754 let k = Kind::Uri(Uri::new("http://example.com"));
755 assert_eq!(codec.encode_scalar(&k).unwrap(), "\"u:http://example.com\"");
756 }
757
758 #[test]
761 fn symbol_roundtrip() {
762 let k = Kind::Symbol(Symbol::new("hot-water"));
763 assert_eq!(roundtrip_scalar(k.clone()), k);
764 }
765
766 #[test]
767 fn symbol_encoding_format() {
768 let codec = Json3Codec;
769 let k = Kind::Symbol(Symbol::new("hot-water"));
770 assert_eq!(codec.encode_scalar(&k).unwrap(), "\"y:hot-water\"");
771 }
772
773 #[test]
776 fn date_roundtrip() {
777 let k = Kind::Date(NaiveDate::from_ymd_opt(2024, 3, 13).unwrap());
778 assert_eq!(roundtrip_scalar(k.clone()), k);
779 }
780
781 #[test]
782 fn date_encoding_format() {
783 let codec = Json3Codec;
784 let k = Kind::Date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
785 assert_eq!(codec.encode_scalar(&k).unwrap(), "\"d:2024-01-01\"");
786 }
787
788 #[test]
791 fn time_roundtrip() {
792 let k = Kind::Time(NaiveTime::from_hms_opt(8, 12, 5).unwrap());
793 assert_eq!(roundtrip_scalar(k.clone()), k);
794 }
795
796 #[test]
797 fn time_with_frac_roundtrip() {
798 let k = Kind::Time(NaiveTime::from_hms_milli_opt(14, 30, 0, 123).unwrap());
799 assert_eq!(roundtrip_scalar(k.clone()), k);
800 }
801
802 #[test]
803 fn time_encoding_format() {
804 let codec = Json3Codec;
805 let k = Kind::Time(NaiveTime::from_hms_opt(12, 30, 45).unwrap());
806 assert_eq!(codec.encode_scalar(&k).unwrap(), "\"h:12:30:45\"");
807 }
808
809 #[test]
812 fn datetime_roundtrip() {
813 let offset = FixedOffset::west_opt(5 * 3600).unwrap();
814 let dt = offset.with_ymd_and_hms(2024, 1, 1, 8, 12, 5).unwrap();
815 let k = Kind::DateTime(HDateTime::new(dt, "New_York"));
816 assert_eq!(roundtrip_scalar(k.clone()), k);
817 }
818
819 #[test]
820 fn datetime_utc_roundtrip() {
821 let offset = FixedOffset::east_opt(0).unwrap();
822 let dt = offset.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
823 let k = Kind::DateTime(HDateTime::new(dt, "UTC"));
824 assert_eq!(roundtrip_scalar(k.clone()), k);
825 }
826
827 #[test]
828 fn datetime_encoding_format() {
829 let codec = Json3Codec;
830 let offset = FixedOffset::west_opt(5 * 3600).unwrap();
831 let dt = offset.with_ymd_and_hms(2024, 1, 1, 12, 30, 45).unwrap();
832 let k = Kind::DateTime(HDateTime::new(dt, "New_York"));
833 assert_eq!(
834 codec.encode_scalar(&k).unwrap(),
835 "\"t:2024-01-01T12:30:45-05:00 New_York\""
836 );
837 }
838
839 #[test]
842 fn coord_roundtrip() {
843 let k = Kind::Coord(Coord::new(37.5458266, -77.4491888));
844 assert_eq!(roundtrip_scalar(k.clone()), k);
845 }
846
847 #[test]
848 fn coord_encoding_format() {
849 let codec = Json3Codec;
850 let k = Kind::Coord(Coord::new(40.7, -74.0));
851 assert_eq!(codec.encode_scalar(&k).unwrap(), "\"c:40.7,-74\"");
852 }
853
854 #[test]
857 fn xstr_roundtrip() {
858 let k = Kind::XStr(XStr::new("Color", "red"));
859 assert_eq!(roundtrip_scalar(k.clone()), k);
860 }
861
862 #[test]
863 fn xstr_encoding_format() {
864 let codec = Json3Codec;
865 let k = Kind::XStr(XStr::new("Color", "red"));
866 assert_eq!(codec.encode_scalar(&k).unwrap(), "\"x:Color:red\"");
867 }
868
869 #[test]
872 fn list_empty_roundtrip() {
873 let k = Kind::List(vec![]);
874 assert_eq!(roundtrip_scalar(k.clone()), k);
875 }
876
877 #[test]
878 fn list_mixed_roundtrip() {
879 let k = Kind::List(vec![
880 Kind::Number(Number::unitless(1.0)),
881 Kind::Str("two".into()),
882 Kind::Marker,
883 Kind::Bool(true),
884 Kind::Null,
885 ]);
886 assert_eq!(roundtrip_scalar(k.clone()), k);
887 }
888
889 #[test]
890 fn list_nested_roundtrip() {
891 let k = Kind::List(vec![
892 Kind::List(vec![Kind::Number(Number::unitless(1.0))]),
893 Kind::List(vec![Kind::Str("inner".into())]),
894 ]);
895 assert_eq!(roundtrip_scalar(k.clone()), k);
896 }
897
898 #[test]
901 fn dict_empty_roundtrip() {
902 let k = Kind::Dict(Box::new(HDict::new()));
903 assert_eq!(roundtrip_scalar(k.clone()), k);
904 }
905
906 #[test]
907 fn dict_with_values_roundtrip() {
908 let mut d = HDict::new();
909 d.set("site", Kind::Marker);
910 d.set("dis", Kind::Str("Main".into()));
911 d.set(
912 "area",
913 Kind::Number(Number::new(4500.0, Some("ft\u{00B2}".into()))),
914 );
915 let k = Kind::Dict(Box::new(d));
916 assert_eq!(roundtrip_scalar(k.clone()), k);
917 }
918
919 #[test]
922 fn grid_empty_roundtrip() {
923 let codec = Json3Codec;
924 let g = HGrid::new();
925 let encoded = codec.encode_grid(&g).unwrap();
926 let decoded = codec.decode_grid(&encoded).unwrap();
927 assert!(decoded.is_empty());
928 assert_eq!(decoded.num_cols(), 0);
929 }
930
931 #[test]
932 fn grid_with_data_roundtrip() {
933 let codec = Json3Codec;
934
935 let cols = vec![HCol::new("dis"), HCol::new("area")];
936 let mut row1 = HDict::new();
937 row1.set("dis", Kind::Str("Site One".into()));
938 row1.set("area", Kind::Number(Number::unitless(4500.0)));
939 let mut row2 = HDict::new();
940 row2.set("dis", Kind::Str("Site Two".into()));
941 row2.set("area", Kind::Number(Number::unitless(3200.0)));
942
943 let g = HGrid::from_parts(HDict::new(), cols, vec![row1, row2]);
944 let encoded = codec.encode_grid(&g).unwrap();
945 let decoded = codec.decode_grid(&encoded).unwrap();
946
947 assert_eq!(decoded.num_cols(), 2);
948 assert_eq!(decoded.len(), 2);
949 assert_eq!(decoded.col_names().collect::<Vec<_>>(), vec!["dis", "area"]);
950 assert_eq!(
951 decoded.row(0).unwrap().get("dis"),
952 Some(&Kind::Str("Site One".into()))
953 );
954 assert_eq!(
955 decoded.row(1).unwrap().get("dis"),
956 Some(&Kind::Str("Site Two".into()))
957 );
958 }
959
960 #[test]
961 fn grid_with_meta_roundtrip() {
962 let codec = Json3Codec;
963
964 let mut meta = HDict::new();
965 meta.set("err", Kind::Marker);
966 meta.set("dis", Kind::Str("some error".into()));
967
968 let g = HGrid::from_parts(meta, vec![], vec![]);
969 let encoded = codec.encode_grid(&g).unwrap();
970 let decoded = codec.decode_grid(&encoded).unwrap();
971
972 assert!(decoded.is_err());
973 assert_eq!(
974 decoded.meta.get("dis"),
975 Some(&Kind::Str("some error".into()))
976 );
977 }
978
979 #[test]
980 fn grid_with_col_meta_roundtrip() {
981 let codec = Json3Codec;
982
983 let mut col_meta = HDict::new();
984 col_meta.set("unit", Kind::Str("kW".into()));
985
986 let cols = vec![HCol::new("name"), HCol::with_meta("power", col_meta)];
987 let g = HGrid::from_parts(HDict::new(), cols, vec![]);
988 let encoded = codec.encode_grid(&g).unwrap();
989 let decoded = codec.decode_grid(&encoded).unwrap();
990
991 assert_eq!(decoded.num_cols(), 2);
992 let power_col = decoded.col("power").unwrap();
993 assert_eq!(power_col.meta.get("unit"), Some(&Kind::Str("kW".into())));
994 }
995
996 #[test]
997 fn grid_encoding_has_ver() {
998 let codec = Json3Codec;
999 let g = HGrid::new();
1000 let encoded = codec.encode_grid(&g).unwrap();
1001 let val: Value = serde_json::from_str(&encoded).unwrap();
1002 let meta = val.get("meta").unwrap().as_object().unwrap();
1003 assert_eq!(meta.get("ver").unwrap(), "3.0");
1004 }
1005
1006 #[test]
1007 fn grid_missing_cells() {
1008 let codec = Json3Codec;
1009
1010 let cols = vec![HCol::new("a"), HCol::new("b")];
1011 let mut row1 = HDict::new();
1012 row1.set("a", Kind::Number(Number::unitless(1.0)));
1013 let g = HGrid::from_parts(HDict::new(), cols, vec![row1]);
1016 let encoded = codec.encode_grid(&g).unwrap();
1017 let decoded = codec.decode_grid(&encoded).unwrap();
1018
1019 let r = decoded.row(0).unwrap();
1020 assert!(r.has("a"));
1021 assert!(r.missing("b"));
1022 }
1023
1024 #[test]
1027 fn disambiguation_str_vs_marker() {
1028 let codec = Json3Codec;
1030 let decoded = codec.decode_scalar("\"m:\"").unwrap();
1031 assert_eq!(decoded, Kind::Marker);
1032 }
1033
1034 #[test]
1035 fn disambiguation_str_with_colon() {
1036 let codec = Json3Codec;
1038 let decoded = codec.decode_scalar("\"s:hello:world\"").unwrap();
1039 assert_eq!(decoded, Kind::Str("hello:world".into()));
1040 }
1041
1042 #[test]
1043 fn string_that_looks_like_prefix() {
1044 let k = Kind::Str("m:".into());
1046 let rt = roundtrip_scalar(k.clone());
1047 assert_eq!(rt, k);
1048 }
1049
1050 #[test]
1051 fn list_with_all_types() {
1052 let offset = FixedOffset::west_opt(5 * 3600).unwrap();
1053 let dt = offset.with_ymd_and_hms(2024, 1, 1, 8, 0, 0).unwrap();
1054 let k = Kind::List(vec![
1055 Kind::Null,
1056 Kind::Marker,
1057 Kind::NA,
1058 Kind::Remove,
1059 Kind::Bool(true),
1060 Kind::Number(Number::new(42.0, Some("kW".into()))),
1061 Kind::Str("hello".into()),
1062 Kind::Ref(HRef::new("x", Some("Dis".into()))),
1063 Kind::Uri(Uri::new("http://a.com")),
1064 Kind::Symbol(Symbol::new("tag")),
1065 Kind::Date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
1066 Kind::Time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()),
1067 Kind::DateTime(HDateTime::new(dt, "New_York")),
1068 Kind::Coord(Coord::new(37.5, -77.4)),
1069 Kind::XStr(XStr::new("Color", "red")),
1070 ]);
1071 assert_eq!(roundtrip_scalar(k.clone()), k);
1072 }
1073
1074 #[test]
1075 fn nested_dict_with_typed_values() {
1076 let mut inner = HDict::new();
1077 inner.set(
1078 "temp",
1079 Kind::Number(Number::new(72.5, Some("\u{00B0}F".into()))),
1080 );
1081 inner.set("site", Kind::Ref(HRef::from_val("s1")));
1082 let k = Kind::Dict(Box::new(inner));
1083 assert_eq!(roundtrip_scalar(k.clone()), k);
1084 }
1085
1086 #[test]
1087 fn grid_nested_in_scalar() {
1088 let codec = Json3Codec;
1089 let cols = vec![HCol::new("x")];
1090 let mut row = HDict::new();
1091 row.set("x", Kind::Number(Number::unitless(42.0)));
1092 let g = HGrid::from_parts(HDict::new(), cols, vec![row]);
1093
1094 let k = Kind::Grid(Box::new(g));
1095 let encoded = codec.encode_scalar(&k).unwrap();
1096 let decoded = codec.decode_scalar(&encoded).unwrap();
1097 match decoded {
1098 Kind::Grid(g) => {
1099 assert_eq!(g.len(), 1);
1100 assert_eq!(g.num_cols(), 1);
1101 }
1102 other => panic!("expected Grid, got {other:?}"),
1103 }
1104 }
1105}