1use super::{SnapshotError, SnapshotResult};
8
9#[derive(Debug, Clone)]
24#[must_use = "JsonField is a borrowed view; call as_u64 / as_i64 / etc. to extract"]
25#[non_exhaustive]
26pub enum JsonField<'a> {
27 Value(&'a serde_json::Value),
29 Missing(SnapshotError),
31}
32
33impl<'a> JsonField<'a> {
34 pub fn is_present(&self) -> bool {
36 !matches!(self, JsonField::Missing(_))
37 }
38
39 pub fn raw(&self) -> Option<&'a serde_json::Value> {
41 match self {
42 JsonField::Value(v) => Some(*v),
43 JsonField::Missing(_) => None,
44 }
45 }
46
47 pub fn error(&self) -> Option<&SnapshotError> {
49 match self {
50 JsonField::Missing(err) => Some(err),
51 _ => None,
52 }
53 }
54
55 pub fn get(&self, path: &str) -> JsonField<'a> {
62 match self {
63 JsonField::Value(v) => walk_json_path(v, path),
64 JsonField::Missing(err) => JsonField::Missing(err.clone()),
65 }
66 }
67
68 pub fn as_u64(&self) -> SnapshotResult<u64> {
76 match self {
77 JsonField::Value(v) => json_to_u64(v),
78 JsonField::Missing(err) => Err(err.clone()),
79 }
80 }
81
82 pub fn as_i64(&self) -> SnapshotResult<i64> {
87 match self {
88 JsonField::Value(v) => json_to_i64(v),
89 JsonField::Missing(err) => Err(err.clone()),
90 }
91 }
92
93 pub fn as_f64(&self) -> SnapshotResult<f64> {
97 match self {
98 JsonField::Value(v) => json_to_f64(v),
99 JsonField::Missing(err) => Err(err.clone()),
100 }
101 }
102
103 pub fn as_bool(&self) -> SnapshotResult<bool> {
108 match self {
109 JsonField::Value(serde_json::Value::Bool(b)) => Ok(*b),
110 JsonField::Value(other) => Err(SnapshotError::TypeMismatch {
111 expected: "bool".to_string(),
112 actual: describe_json_kind(other),
113 requested: String::new(),
114 }),
115 JsonField::Missing(err) => Err(err.clone()),
116 }
117 }
118
119 pub fn as_str(&self) -> SnapshotResult<&'a str> {
121 match self {
122 JsonField::Value(serde_json::Value::String(s)) => Ok(s.as_str()),
123 JsonField::Value(other) => Err(SnapshotError::TypeMismatch {
124 expected: "str".to_string(),
125 actual: describe_json_kind(other),
126 requested: String::new(),
127 }),
128 JsonField::Missing(err) => Err(err.clone()),
129 }
130 }
131
132 pub fn as_u64_array(&self) -> SnapshotResult<Vec<u64>> {
137 json_to_typed_array(self, json_to_u64, "u64")
138 }
139
140 pub fn as_u32_array(&self) -> SnapshotResult<Vec<u32>> {
144 json_to_typed_array(
145 self,
146 |v| {
147 json_to_u64(v).and_then(|x| {
148 u32::try_from(x).map_err(|_| SnapshotError::TypeMismatch {
149 expected: "u32".to_string(),
150 actual: "Uint(>u32::MAX)".to_string(),
151 requested: String::new(),
152 })
153 })
154 },
155 "u32",
156 )
157 }
158
159 pub fn as_i64_array(&self) -> SnapshotResult<Vec<i64>> {
162 json_to_typed_array(self, json_to_i64, "i64")
163 }
164
165 pub fn as_f64_array(&self) -> SnapshotResult<Vec<f64>> {
168 json_to_typed_array(self, json_to_f64, "f64")
169 }
170
171 pub fn as_bool_array(&self) -> SnapshotResult<Vec<bool>> {
176 json_to_typed_array(
177 self,
178 |v| match v {
179 serde_json::Value::Bool(b) => Ok(*b),
180 other => Err(SnapshotError::TypeMismatch {
181 expected: "bool".to_string(),
182 actual: describe_json_kind(other),
183 requested: String::new(),
184 }),
185 },
186 "bool",
187 )
188 }
189
190 pub fn iter_members(&self) -> impl Iterator<Item = JsonField<'a>> + '_ {
201 let elements: &[serde_json::Value] = match self {
202 JsonField::Value(serde_json::Value::Array(a)) => a.as_slice(),
203 _ => &[],
204 };
205 elements.iter().map(JsonField::Value)
206 }
207}
208
209fn json_to_typed_array<T, F>(
213 field: &JsonField<'_>,
214 coerce: F,
215 type_name: &'static str,
216) -> SnapshotResult<Vec<T>>
217where
218 F: Fn(&serde_json::Value) -> SnapshotResult<T>,
219{
220 let value = match field {
221 JsonField::Value(v) => *v,
222 JsonField::Missing(err) => return Err(err.clone()),
223 };
224 let elements = match value {
225 serde_json::Value::Array(a) => a.as_slice(),
226 other => {
227 return Err(SnapshotError::TypeMismatch {
228 expected: format!("[{type_name}]"),
229 actual: describe_json_kind(other),
230 requested: String::new(),
231 });
232 }
233 };
234 let mut out = Vec::with_capacity(elements.len());
235 for element in elements {
236 out.push(coerce(element)?);
237 }
238 Ok(out)
239}
240
241pub fn stats_path<'a>(value: &'a serde_json::Value, path: &str) -> JsonField<'a> {
256 walk_json_path(value, path)
257}
258
259fn walk_json_path<'a>(root: &'a serde_json::Value, path: &str) -> JsonField<'a> {
260 if path.is_empty() {
261 return JsonField::Value(root);
262 }
263 let mut cursor: &serde_json::Value = root;
264 let mut walked = String::new();
265 for component in path.split('.') {
266 if component.is_empty() {
267 return JsonField::Missing(SnapshotError::EmptyPathComponent {
268 requested: path.to_string(),
269 });
270 }
271 match cursor {
272 serde_json::Value::Object(map) => {
273 let Some(next) = map.get(component) else {
274 let mut available: Vec<String> = map.keys().cloned().collect();
275 available.sort();
276 return JsonField::Missing(SnapshotError::FieldNotFound {
277 requested: path.to_string(),
278 walked: walked.clone(),
279 component: component.to_string(),
280 available,
281 });
282 };
283 cursor = next;
284 }
285 other => {
286 return JsonField::Missing(SnapshotError::NotAStruct {
287 requested: path.to_string(),
288 walked: walked.clone(),
289 component: component.to_string(),
290 kind: describe_json_kind(other),
291 });
292 }
293 }
294 if !walked.is_empty() {
295 walked.push('.');
296 }
297 walked.push_str(component);
298 }
299 JsonField::Value(cursor)
300}
301
302fn describe_json_kind(v: &serde_json::Value) -> String {
303 match v {
304 serde_json::Value::Null => "Null",
305 serde_json::Value::Bool(_) => "Bool",
306 serde_json::Value::Number(_) => "Number",
307 serde_json::Value::String(_) => "String",
308 serde_json::Value::Array(_) => "Array",
309 serde_json::Value::Object(_) => "Object",
310 }
311 .to_string()
312}
313
314fn json_to_u64(v: &serde_json::Value) -> SnapshotResult<u64> {
315 match v {
316 serde_json::Value::Number(n) => {
317 if let Some(u) = n.as_u64() {
318 Ok(u)
319 } else if let Some(i) = n.as_i64() {
320 if i < 0 {
321 Err(SnapshotError::TypeMismatch {
322 expected: "u64".to_string(),
323 actual: "Int(negative)".to_string(),
324 requested: String::new(),
325 })
326 } else {
327 Ok(i as u64)
328 }
329 } else if let Some(f) = n.as_f64() {
330 if !f.is_finite() || f < 0.0 {
331 Err(SnapshotError::TypeMismatch {
332 expected: "u64".to_string(),
333 actual: "Float(non-coercible)".to_string(),
334 requested: String::new(),
335 })
336 } else if f.fract() != 0.0 {
337 Err(SnapshotError::TypeMismatch {
338 expected: "integer".to_string(),
339 actual: "non-integer float".to_string(),
340 requested: String::new(),
341 })
342 } else {
343 Ok(f as u64)
344 }
345 } else {
346 Err(SnapshotError::TypeMismatch {
347 expected: "u64".to_string(),
348 actual: "Number(unrepresentable)".to_string(),
349 requested: String::new(),
350 })
351 }
352 }
353 serde_json::Value::Bool(b) => Ok(u64::from(*b)),
354 serde_json::Value::String(s) => s.parse::<u64>().map_err(|_| SnapshotError::TypeMismatch {
355 expected: "u64".to_string(),
356 actual: "String(non-numeric)".to_string(),
357 requested: String::new(),
358 }),
359 other => Err(SnapshotError::TypeMismatch {
360 expected: "u64".to_string(),
361 actual: describe_json_kind(other),
362 requested: String::new(),
363 }),
364 }
365}
366
367fn json_to_i64(v: &serde_json::Value) -> SnapshotResult<i64> {
368 match v {
369 serde_json::Value::Number(n) => {
370 if let Some(i) = n.as_i64() {
371 Ok(i)
372 } else if let Some(u) = n.as_u64() {
373 if u > i64::MAX as u64 {
374 Err(SnapshotError::TypeMismatch {
375 expected: "i64".to_string(),
376 actual: "Uint(>i64::MAX)".to_string(),
377 requested: String::new(),
378 })
379 } else {
380 Ok(u as i64)
381 }
382 } else if let Some(f) = n.as_f64() {
383 if !f.is_finite() {
384 Err(SnapshotError::TypeMismatch {
385 expected: "i64".to_string(),
386 actual: "Float(non-finite)".to_string(),
387 requested: String::new(),
388 })
389 } else if f.fract() != 0.0 {
390 Err(SnapshotError::TypeMismatch {
391 expected: "integer".to_string(),
392 actual: "non-integer float".to_string(),
393 requested: String::new(),
394 })
395 } else {
396 Ok(f as i64)
397 }
398 } else {
399 Err(SnapshotError::TypeMismatch {
400 expected: "i64".to_string(),
401 actual: "Number(unrepresentable)".to_string(),
402 requested: String::new(),
403 })
404 }
405 }
406 serde_json::Value::Bool(b) => Ok(i64::from(*b)),
407 serde_json::Value::String(s) => s.parse::<i64>().map_err(|_| SnapshotError::TypeMismatch {
408 expected: "i64".to_string(),
409 actual: "String(non-numeric)".to_string(),
410 requested: String::new(),
411 }),
412 other => Err(SnapshotError::TypeMismatch {
413 expected: "i64".to_string(),
414 actual: describe_json_kind(other),
415 requested: String::new(),
416 }),
417 }
418}
419
420fn json_to_f64(v: &serde_json::Value) -> SnapshotResult<f64> {
421 match v {
422 serde_json::Value::Number(n) => n.as_f64().ok_or(SnapshotError::TypeMismatch {
423 expected: "f64".to_string(),
424 actual: "Number(unrepresentable)".to_string(),
425 requested: String::new(),
426 }),
427 serde_json::Value::String(s) => s.parse::<f64>().map_err(|_| SnapshotError::TypeMismatch {
428 expected: "f64".to_string(),
429 actual: "String(non-numeric)".to_string(),
430 requested: String::new(),
431 }),
432 other => Err(SnapshotError::TypeMismatch {
433 expected: "f64".to_string(),
434 actual: describe_json_kind(other),
435 requested: String::new(),
436 }),
437 }
438}
439
440#[cfg(test)]
441mod tests_coercion {
442 use super::*;
451 use serde_json::json;
452
453 #[test]
456 fn describe_json_kind_names_each_value_shape() {
457 assert_eq!(describe_json_kind(&json!(null)), "Null");
458 assert_eq!(describe_json_kind(&json!(true)), "Bool");
459 assert_eq!(describe_json_kind(&json!(1)), "Number");
460 assert_eq!(describe_json_kind(&json!("s")), "String");
461 assert_eq!(describe_json_kind(&json!([])), "Array");
462 assert_eq!(describe_json_kind(&json!({})), "Object");
463 }
464
465 #[test]
468 fn json_to_u64_accepts_uint_bool_and_numeric_string() {
469 assert_eq!(json_to_u64(&json!(42u64)).unwrap(), 42);
470 assert_eq!(json_to_u64(&json!(true)).unwrap(), 1);
471 assert_eq!(json_to_u64(&json!(false)).unwrap(), 0);
472 assert_eq!(
474 json_to_u64(&json!("18446744073709551615")).unwrap(),
475 u64::MAX
476 );
477 }
478
479 #[test]
480 fn json_to_u64_accepts_integral_float_rejects_fractional_and_negative() {
481 assert_eq!(json_to_u64(&json!(5.0)).unwrap(), 5);
483 match json_to_u64(&json!(2.5)) {
485 Err(SnapshotError::TypeMismatch {
486 expected, actual, ..
487 }) => {
488 assert_eq!(expected, "integer");
489 assert_eq!(actual, "non-integer float");
490 }
491 other => panic!("expected non-integer-float TypeMismatch, got {other:?}"),
492 }
493 match json_to_u64(&json!(-2.5)) {
495 Err(SnapshotError::TypeMismatch {
496 expected, actual, ..
497 }) => {
498 assert_eq!(expected, "u64");
499 assert_eq!(actual, "Float(non-coercible)");
500 }
501 other => panic!("expected negative-float TypeMismatch, got {other:?}"),
502 }
503 }
504
505 #[test]
506 fn json_to_u64_rejects_negative_int_nonnumeric_string_and_other_shapes() {
507 match json_to_u64(&json!(-5)) {
508 Err(SnapshotError::TypeMismatch { actual, .. }) => {
509 assert_eq!(actual, "Int(negative)");
510 }
511 other => panic!("expected negative-int TypeMismatch, got {other:?}"),
512 }
513 match json_to_u64(&json!("abc")) {
514 Err(SnapshotError::TypeMismatch { actual, .. }) => {
515 assert_eq!(actual, "String(non-numeric)");
516 }
517 other => panic!("expected non-numeric-string TypeMismatch, got {other:?}"),
518 }
519 assert!(
521 json_to_u64(&json!(null))
522 .unwrap_err()
523 .to_string()
524 .contains("Null")
525 );
526 assert!(matches!(
527 json_to_u64(&json!({"k": 1})),
528 Err(SnapshotError::TypeMismatch { .. })
529 ));
530 }
531
532 #[test]
535 fn json_to_i64_accepts_signed_bool_string_and_integral_float() {
536 assert_eq!(json_to_i64(&json!(-7)).unwrap(), -7);
537 assert_eq!(json_to_i64(&json!(true)).unwrap(), 1);
538 assert_eq!(json_to_i64(&json!("-123")).unwrap(), -123);
539 assert_eq!(json_to_i64(&json!(9.0)).unwrap(), 9);
540 }
541
542 #[test]
543 fn json_to_i64_rejects_uint_over_i64_max_and_fractional_float() {
544 match json_to_i64(&json!(u64::MAX)) {
546 Err(SnapshotError::TypeMismatch { actual, .. }) => {
547 assert_eq!(actual, "Uint(>i64::MAX)");
548 }
549 other => panic!("expected over-i64::MAX TypeMismatch, got {other:?}"),
550 }
551 match json_to_i64(&json!(2.5)) {
552 Err(SnapshotError::TypeMismatch {
553 expected, actual, ..
554 }) => {
555 assert_eq!(expected, "integer");
556 assert_eq!(actual, "non-integer float");
557 }
558 other => panic!("expected fractional-float TypeMismatch, got {other:?}"),
559 }
560 }
561
562 #[test]
563 fn json_to_i64_rejects_nonnumeric_string_and_other_shapes() {
564 match json_to_i64(&json!("nope")) {
565 Err(SnapshotError::TypeMismatch { actual, .. }) => {
566 assert_eq!(actual, "String(non-numeric)");
567 }
568 other => panic!("expected non-numeric-string TypeMismatch, got {other:?}"),
569 }
570 assert!(matches!(
571 json_to_i64(&json!([1, 2])),
572 Err(SnapshotError::TypeMismatch { .. })
573 ));
574 }
575
576 #[test]
579 fn json_to_f64_accepts_number_and_numeric_string_rejects_others() {
580 assert_eq!(json_to_f64(&json!(1.5)).unwrap(), 1.5);
581 assert_eq!(json_to_f64(&json!(3)).unwrap(), 3.0);
583 assert_eq!(json_to_f64(&json!("2.25")).unwrap(), 2.25);
584 match json_to_f64(&json!("x")) {
585 Err(SnapshotError::TypeMismatch { actual, .. }) => {
586 assert_eq!(actual, "String(non-numeric)");
587 }
588 other => panic!("expected non-numeric-string TypeMismatch, got {other:?}"),
589 }
590 assert!(matches!(
591 json_to_f64(&json!(null)),
592 Err(SnapshotError::TypeMismatch { .. })
593 ));
594 }
595
596 #[test]
599 fn json_field_is_present_raw_and_error_reflect_variant() {
600 let v = json!({"a": 1});
601 let present = stats_path(&v, "a");
602 assert!(present.is_present());
603 assert!(present.raw().is_some());
604 assert!(present.error().is_none());
605
606 let missing = stats_path(&v, "nope");
607 assert!(!missing.is_present());
608 assert!(missing.raw().is_none());
609 assert!(missing.error().is_some());
610 }
611
612 #[test]
613 fn json_field_as_bool_accepts_bool_rejects_other_and_propagates_missing() {
614 let t = json!({"on": true});
615 assert!(stats_path(&t, "on").as_bool().unwrap());
616 let n = json!({"on": "1"});
618 assert!(matches!(
619 stats_path(&n, "on").as_bool(),
620 Err(SnapshotError::TypeMismatch { .. })
621 ));
622 assert!(matches!(
624 stats_path(&n, "absent").as_bool(),
625 Err(SnapshotError::FieldNotFound { .. })
626 ));
627 }
628
629 #[test]
630 fn json_field_as_str_accepts_string_rejects_other_and_propagates_missing() {
631 let v = json!({"name": "batch"});
632 assert_eq!(stats_path(&v, "name").as_str().unwrap(), "batch");
633 let num = json!({"name": 7});
634 assert!(matches!(
635 stats_path(&num, "name").as_str(),
636 Err(SnapshotError::TypeMismatch { .. })
637 ));
638 assert!(matches!(
639 stats_path(&v, "absent").as_str(),
640 Err(SnapshotError::FieldNotFound { .. })
641 ));
642 }
643
644 #[test]
645 fn json_field_scalar_accessors_propagate_missing_error() {
646 let v = json!({"x": 1});
647 let missing = stats_path(&v, "absent");
648 assert!(matches!(
650 missing.as_u64(),
651 Err(SnapshotError::FieldNotFound { .. })
652 ));
653 assert!(matches!(
654 missing.as_i64(),
655 Err(SnapshotError::FieldNotFound { .. })
656 ));
657 assert!(matches!(
658 missing.as_f64(),
659 Err(SnapshotError::FieldNotFound { .. })
660 ));
661 }
662
663 #[test]
664 fn json_field_get_on_missing_stays_missing() {
665 let v = json!({"x": 1});
666 let chained = stats_path(&v, "absent").get("deeper");
668 assert!(matches!(
669 chained.error(),
670 Some(SnapshotError::FieldNotFound { .. })
671 ));
672 }
673
674 #[test]
675 fn json_field_iter_members_yields_elements_and_empty_for_nonarray() {
676 let arr = json!([10, 20, 30]);
677 let got: Vec<u64> = stats_path(&arr, "")
678 .iter_members()
679 .map(|el| el.as_u64().unwrap())
680 .collect();
681 assert_eq!(got, vec![10, 20, 30]);
682 let scalar = json!(5);
684 assert_eq!(stats_path(&scalar, "").iter_members().count(), 0);
685 let obj = json!({"a": 1});
687 assert_eq!(stats_path(&obj, "absent").iter_members().count(), 0);
688 }
689
690 #[test]
693 fn json_field_typed_arrays_extract_each_element_type() {
694 let v = json!({
695 "u": [1, 2, 3],
696 "i": [-1, 0, 5],
697 "f": [1.5, 2.0],
698 "b": [true, false, true],
699 });
700 assert_eq!(
701 stats_path(&v, "u").as_u64_array().unwrap(),
702 vec![1u64, 2, 3]
703 );
704 assert_eq!(
705 stats_path(&v, "i").as_i64_array().unwrap(),
706 vec![-1i64, 0, 5]
707 );
708 assert_eq!(stats_path(&v, "f").as_f64_array().unwrap(), vec![1.5, 2.0]);
709 assert_eq!(
710 stats_path(&v, "b").as_bool_array().unwrap(),
711 vec![true, false, true]
712 );
713 }
714
715 #[test]
716 fn json_field_as_u32_array_rejects_out_of_range_element() {
717 let v = json!({"c": [1, 4294967296u64]});
719 match stats_path(&v, "c").as_u32_array() {
720 Err(SnapshotError::TypeMismatch {
721 expected, actual, ..
722 }) => {
723 assert_eq!(expected, "u32");
724 assert_eq!(actual, "Uint(>u32::MAX)");
725 }
726 other => panic!("expected u32 out-of-range TypeMismatch, got {other:?}"),
727 }
728 }
729
730 #[test]
731 fn json_field_typed_array_on_nonarray_value_errors_with_element_type() {
732 let v = json!({"scalar": 7});
734 match stats_path(&v, "scalar").as_u64_array() {
735 Err(SnapshotError::TypeMismatch {
736 expected, actual, ..
737 }) => {
738 assert_eq!(expected, "[u64]");
739 assert_eq!(actual, "Number");
740 }
741 other => panic!("expected non-array TypeMismatch, got {other:?}"),
742 }
743 }
744
745 #[test]
746 fn json_field_typed_array_propagates_missing_error() {
747 let v = json!({"x": 1});
748 assert!(matches!(
749 stats_path(&v, "absent").as_u64_array(),
750 Err(SnapshotError::FieldNotFound { .. })
751 ));
752 }
753}