1use serde::{Deserialize, Serialize};
4
5use crate::model::fields::FieldValue;
6use crate::model::node::Node;
7
8use super::{ChangeKind, ChangeSeverity};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16#[serde(untagged)]
17pub enum FieldValueSnapshot {
18 Scalar(String),
20 List(Vec<String>),
22 Block(String),
24 Complex(String),
26}
27
28impl FieldValueSnapshot {
29 pub(crate) fn from_field_value(v: &FieldValue) -> Self {
31 match v {
32 FieldValue::Scalar(s) => Self::Scalar(s.clone()),
33 FieldValue::List(l) => Self::List(l.clone()),
34 FieldValue::Block(b) => Self::Block(b.clone()),
35 }
36 }
37
38 pub(crate) fn from_str_val(s: &str) -> Self {
40 Self::Scalar(s.to_owned())
41 }
42
43 pub(crate) fn from_complex<T: Serialize>(v: &T) -> Self {
45 Self::Complex(serde_json::to_string(v).unwrap_or_default())
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct FieldChange {
52 pub field: String,
53 pub kind: ChangeKind,
54 pub severity: ChangeSeverity,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub old_value: Option<FieldValueSnapshot>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub new_value: Option<FieldValueSnapshot>,
59}
60
61#[must_use]
65pub(crate) fn classify_severity(field_name: &str, kind: ChangeKind) -> ChangeSeverity {
66 match (field_name, kind) {
67 ("node_type" | "type", _) => ChangeSeverity::Breaking,
69
70 ("summary", _) => ChangeSeverity::Minor,
72
73 ("priority" | "stability" | "confidence" | "status", ChangeKind::Added) => {
75 ChangeSeverity::Info
76 }
77 ("priority" | "stability" | "confidence" | "status", _) => ChangeSeverity::Minor,
78
79 ("depends", ChangeKind::Removed) => ChangeSeverity::Breaking,
81 ("depends", ChangeKind::Added) => ChangeSeverity::Minor,
82 ("depends", ChangeKind::Modified) => ChangeSeverity::Minor,
83
84 ("related_to" | "see_also", _) => ChangeSeverity::Info,
86
87 ("replaces" | "conflicts", _) => ChangeSeverity::Minor,
89
90 ("items", _) => ChangeSeverity::Minor,
92
93 ("steps", ChangeKind::Removed) => ChangeSeverity::Breaking,
95 ("steps", _) => ChangeSeverity::Minor,
96
97 ("fields" | "input" | "output", ChangeKind::Removed) => ChangeSeverity::Breaking,
99 ("fields" | "input" | "output", _) => ChangeSeverity::Minor,
100
101 ("detail" | "examples" | "notes", _) => ChangeSeverity::Info,
103 ("rationale" | "tradeoffs" | "resolution", _) => ChangeSeverity::Info,
104
105 ("code" | "code_blocks", ChangeKind::Removed) => ChangeSeverity::Breaking,
107 ("code" | "code_blocks", _) => ChangeSeverity::Minor,
108 ("verify", _) => ChangeSeverity::Minor,
109 ("agent_context", ChangeKind::Added) => ChangeSeverity::Info,
110 ("agent_context", _) => ChangeSeverity::Minor,
111 ("target", ChangeKind::Removed) => ChangeSeverity::Breaking,
112 ("target", _) => ChangeSeverity::Minor,
113
114 (
116 "execution_status" | "executed_by" | "executed_at" | "execution_log" | "retry_count",
117 _,
118 ) => ChangeSeverity::Info,
119
120 ("parallel_groups", ChangeKind::Removed) => ChangeSeverity::Breaking,
122 ("parallel_groups", _) => ChangeSeverity::Minor,
123
124 ("memory", ChangeKind::Added) => ChangeSeverity::Info,
126 ("memory", _) => ChangeSeverity::Minor,
127
128 ("scope", ChangeKind::Added) => ChangeSeverity::Info,
130 ("scope", _) => ChangeSeverity::Minor,
131
132 ("applies_when", ChangeKind::Added) => ChangeSeverity::Info,
134 ("applies_when", _) => ChangeSeverity::Minor,
135
136 ("valid_from" | "valid_until", _) => ChangeSeverity::Info,
138
139 ("tags" | "keywords", _) => ChangeSeverity::Info,
141
142 ("aliases", ChangeKind::Added) => ChangeSeverity::Info,
144 ("aliases", _) => ChangeSeverity::Minor,
145
146 _ => ChangeSeverity::Info,
148 }
149}
150
151#[must_use]
156pub(crate) fn diff_all_fields(left: &Node, right: &Node) -> Vec<FieldChange> {
157 let mut changes = Vec::new();
158
159 if left.node_type != right.node_type {
161 changes.push(FieldChange {
162 field: "type".to_owned(),
163 kind: ChangeKind::Modified,
164 severity: classify_severity("node_type", ChangeKind::Modified),
165 old_value: Some(FieldValueSnapshot::from_str_val(
166 &left.node_type.to_string(),
167 )),
168 new_value: Some(FieldValueSnapshot::from_str_val(
169 &right.node_type.to_string(),
170 )),
171 });
172 }
173
174 if left.summary != right.summary {
176 changes.push(FieldChange {
177 field: "summary".to_owned(),
178 kind: ChangeKind::Modified,
179 severity: classify_severity("summary", ChangeKind::Modified),
180 old_value: Some(FieldValueSnapshot::from_str_val(&left.summary)),
181 new_value: Some(FieldValueSnapshot::from_str_val(&right.summary)),
182 });
183 }
184
185 diff_opt_display(
187 &mut changes,
188 "priority",
189 left.priority.as_ref().map(|v| v.to_string()),
190 right.priority.as_ref().map(|v| v.to_string()),
191 );
192
193 diff_opt_display(
195 &mut changes,
196 "stability",
197 left.stability.as_ref().map(|v| v.to_string()),
198 right.stability.as_ref().map(|v| v.to_string()),
199 );
200
201 diff_opt_display(
203 &mut changes,
204 "confidence",
205 left.confidence.as_ref().map(|v| v.to_string()),
206 right.confidence.as_ref().map(|v| v.to_string()),
207 );
208
209 diff_opt_display(
211 &mut changes,
212 "status",
213 left.status.as_ref().map(|v| v.to_string()),
214 right.status.as_ref().map(|v| v.to_string()),
215 );
216
217 diff_opt_list_field(
219 &mut changes,
220 "depends",
221 left.depends.as_deref(),
222 right.depends.as_deref(),
223 );
224
225 diff_opt_list_field(
227 &mut changes,
228 "related_to",
229 left.related_to.as_deref(),
230 right.related_to.as_deref(),
231 );
232 diff_opt_list_field(
233 &mut changes,
234 "replaces",
235 left.replaces.as_deref(),
236 right.replaces.as_deref(),
237 );
238 diff_opt_list_field(
239 &mut changes,
240 "conflicts",
241 left.conflicts.as_deref(),
242 right.conflicts.as_deref(),
243 );
244 diff_opt_list_field(
245 &mut changes,
246 "see_also",
247 left.see_also.as_deref(),
248 right.see_also.as_deref(),
249 );
250
251 diff_opt_list_field(
253 &mut changes,
254 "items",
255 left.items.as_deref(),
256 right.items.as_deref(),
257 );
258 diff_opt_list_field(
259 &mut changes,
260 "steps",
261 left.steps.as_deref(),
262 right.steps.as_deref(),
263 );
264 diff_opt_list_field(
265 &mut changes,
266 "fields",
267 left.fields.as_deref(),
268 right.fields.as_deref(),
269 );
270 diff_opt_list_field(
271 &mut changes,
272 "input",
273 left.input.as_deref(),
274 right.input.as_deref(),
275 );
276 diff_opt_list_field(
277 &mut changes,
278 "output",
279 left.output.as_deref(),
280 right.output.as_deref(),
281 );
282
283 diff_opt_scalar(
285 &mut changes,
286 "detail",
287 left.detail.as_deref(),
288 right.detail.as_deref(),
289 );
290 diff_opt_scalar(
291 &mut changes,
292 "examples",
293 left.examples.as_deref(),
294 right.examples.as_deref(),
295 );
296 diff_opt_scalar(
297 &mut changes,
298 "notes",
299 left.notes.as_deref(),
300 right.notes.as_deref(),
301 );
302
303 diff_opt_list_field(
305 &mut changes,
306 "rationale",
307 left.rationale.as_deref(),
308 right.rationale.as_deref(),
309 );
310 diff_opt_list_field(
311 &mut changes,
312 "tradeoffs",
313 left.tradeoffs.as_deref(),
314 right.tradeoffs.as_deref(),
315 );
316 diff_opt_list_field(
317 &mut changes,
318 "resolution",
319 left.resolution.as_deref(),
320 right.resolution.as_deref(),
321 );
322
323 diff_opt_scalar(
325 &mut changes,
326 "target",
327 left.target.as_deref(),
328 right.target.as_deref(),
329 );
330
331 diff_opt_complex(
333 &mut changes,
334 "code",
335 left.code.as_ref(),
336 right.code.as_ref(),
337 );
338
339 diff_opt_complex(
341 &mut changes,
342 "code_blocks",
343 left.code_blocks.as_ref(),
344 right.code_blocks.as_ref(),
345 );
346
347 diff_opt_complex(
349 &mut changes,
350 "verify",
351 left.verify.as_ref(),
352 right.verify.as_ref(),
353 );
354
355 diff_opt_complex(
357 &mut changes,
358 "agent_context",
359 left.agent_context.as_ref(),
360 right.agent_context.as_ref(),
361 );
362
363 diff_opt_display(
365 &mut changes,
366 "execution_status",
367 left.execution_status.as_ref().map(|v| v.to_string()),
368 right.execution_status.as_ref().map(|v| v.to_string()),
369 );
370 diff_opt_scalar(
371 &mut changes,
372 "executed_by",
373 left.executed_by.as_deref(),
374 right.executed_by.as_deref(),
375 );
376 diff_opt_scalar(
377 &mut changes,
378 "executed_at",
379 left.executed_at.as_deref(),
380 right.executed_at.as_deref(),
381 );
382 diff_opt_scalar(
383 &mut changes,
384 "execution_log",
385 left.execution_log.as_deref(),
386 right.execution_log.as_deref(),
387 );
388 diff_opt_display(
389 &mut changes,
390 "retry_count",
391 left.retry_count.map(|v| v.to_string()),
392 right.retry_count.map(|v| v.to_string()),
393 );
394
395 diff_opt_complex(
397 &mut changes,
398 "parallel_groups",
399 left.parallel_groups.as_ref(),
400 right.parallel_groups.as_ref(),
401 );
402
403 diff_opt_complex(
405 &mut changes,
406 "memory",
407 left.memory.as_ref(),
408 right.memory.as_ref(),
409 );
410
411 diff_opt_list_field(
413 &mut changes,
414 "scope",
415 left.scope.as_deref(),
416 right.scope.as_deref(),
417 );
418 diff_opt_scalar(
419 &mut changes,
420 "applies_when",
421 left.applies_when.as_deref(),
422 right.applies_when.as_deref(),
423 );
424 diff_opt_scalar(
425 &mut changes,
426 "valid_from",
427 left.valid_from.as_deref(),
428 right.valid_from.as_deref(),
429 );
430 diff_opt_scalar(
431 &mut changes,
432 "valid_until",
433 left.valid_until.as_deref(),
434 right.valid_until.as_deref(),
435 );
436 diff_opt_list_field(
437 &mut changes,
438 "tags",
439 left.tags.as_deref(),
440 right.tags.as_deref(),
441 );
442 diff_opt_list_field(
443 &mut changes,
444 "aliases",
445 left.aliases.as_deref(),
446 right.aliases.as_deref(),
447 );
448 diff_opt_list_field(
449 &mut changes,
450 "keywords",
451 left.keywords.as_deref(),
452 right.keywords.as_deref(),
453 );
454
455 diff_extra_fields(&mut changes, &left.extra_fields, &right.extra_fields);
457
458 changes
459}
460
461fn diff_opt_scalar(
466 changes: &mut Vec<FieldChange>,
467 field: &str,
468 left: Option<&str>,
469 right: Option<&str>,
470) {
471 match (left, right) {
472 (None, None) => {}
473 (None, Some(r)) => changes.push(FieldChange {
474 field: field.to_owned(),
475 kind: ChangeKind::Added,
476 severity: classify_severity(field, ChangeKind::Added),
477 old_value: None,
478 new_value: Some(FieldValueSnapshot::Scalar(r.to_owned())),
479 }),
480 (Some(l), None) => changes.push(FieldChange {
481 field: field.to_owned(),
482 kind: ChangeKind::Removed,
483 severity: classify_severity(field, ChangeKind::Removed),
484 old_value: Some(FieldValueSnapshot::Scalar(l.to_owned())),
485 new_value: None,
486 }),
487 (Some(l), Some(r)) if l != r => changes.push(FieldChange {
488 field: field.to_owned(),
489 kind: ChangeKind::Modified,
490 severity: classify_severity(field, ChangeKind::Modified),
491 old_value: Some(FieldValueSnapshot::Scalar(l.to_owned())),
492 new_value: Some(FieldValueSnapshot::Scalar(r.to_owned())),
493 }),
494 _ => {}
495 }
496}
497
498fn diff_opt_display(
499 changes: &mut Vec<FieldChange>,
500 field: &str,
501 left: Option<String>,
502 right: Option<String>,
503) {
504 diff_opt_scalar(changes, field, left.as_deref(), right.as_deref());
505}
506
507fn diff_opt_list_field(
508 changes: &mut Vec<FieldChange>,
509 field: &str,
510 left: Option<&[String]>,
511 right: Option<&[String]>,
512) {
513 let left_slice = left.unwrap_or_default();
514 let right_slice = right.unwrap_or_default();
515
516 if left.is_none() && right.is_none() {
518 return;
519 }
520
521 let sub_changes = super::relation::diff_relation_field(field, left_slice, right_slice);
522 changes.extend(sub_changes);
523}
524
525fn diff_opt_complex<T: Serialize + PartialEq>(
526 changes: &mut Vec<FieldChange>,
527 field: &str,
528 left: Option<&T>,
529 right: Option<&T>,
530) {
531 match (left, right) {
532 (None, None) => {}
533 (None, Some(r)) => changes.push(FieldChange {
534 field: field.to_owned(),
535 kind: ChangeKind::Added,
536 severity: classify_severity(field, ChangeKind::Added),
537 old_value: None,
538 new_value: Some(FieldValueSnapshot::from_complex(r)),
539 }),
540 (Some(l), None) => changes.push(FieldChange {
541 field: field.to_owned(),
542 kind: ChangeKind::Removed,
543 severity: classify_severity(field, ChangeKind::Removed),
544 old_value: Some(FieldValueSnapshot::from_complex(l)),
545 new_value: None,
546 }),
547 (Some(l), Some(r)) if l != r => {
548 let old_json = serde_json::to_string(l).unwrap_or_default();
549 let new_json = serde_json::to_string(r).unwrap_or_default();
550 if old_json != new_json {
551 changes.push(FieldChange {
552 field: field.to_owned(),
553 kind: ChangeKind::Modified,
554 severity: classify_severity(field, ChangeKind::Modified),
555 old_value: Some(FieldValueSnapshot::Complex(old_json)),
556 new_value: Some(FieldValueSnapshot::Complex(new_json)),
557 });
558 }
559 }
560 _ => {}
561 }
562}
563
564fn diff_extra_fields(
565 changes: &mut Vec<FieldChange>,
566 left: &std::collections::BTreeMap<String, FieldValue>,
567 right: &std::collections::BTreeMap<String, FieldValue>,
568) {
569 for (key, val) in left {
571 if !right.contains_key(key) {
572 changes.push(FieldChange {
573 field: key.clone(),
574 kind: ChangeKind::Removed,
575 severity: ChangeSeverity::Info,
576 old_value: Some(FieldValueSnapshot::from_field_value(val)),
577 new_value: None,
578 });
579 }
580 }
581
582 for (key, val) in right {
584 if !left.contains_key(key) {
585 changes.push(FieldChange {
586 field: key.clone(),
587 kind: ChangeKind::Added,
588 severity: ChangeSeverity::Info,
589 old_value: None,
590 new_value: Some(FieldValueSnapshot::from_field_value(val)),
591 });
592 }
593 }
594
595 for (key, left_val) in left {
597 if let Some(right_val) = right.get(key) {
598 if left_val != right_val {
599 changes.push(FieldChange {
600 field: key.clone(),
601 kind: ChangeKind::Modified,
602 severity: ChangeSeverity::Info,
603 old_value: Some(FieldValueSnapshot::from_field_value(left_val)),
604 new_value: Some(FieldValueSnapshot::from_field_value(right_val)),
605 });
606 }
607 }
608 }
609}
610
611#[cfg(test)]
616mod tests {
617 use std::collections::BTreeMap;
618
619 use crate::model::fields::{FieldValue, NodeType, Priority, Span};
620 use crate::model::node::Node;
621
622 use super::*;
623
624 fn minimal_node() -> Node {
625 Node {
626 id: "test.node".to_owned(),
627 node_type: NodeType::Facts,
628 summary: "a test node".to_owned(),
629 priority: None,
630 stability: None,
631 confidence: None,
632 status: None,
633 depends: None,
634 related_to: None,
635 replaces: None,
636 conflicts: None,
637 see_also: None,
638 items: None,
639 steps: None,
640 fields: None,
641 input: None,
642 output: None,
643 detail: None,
644 rationale: None,
645 tradeoffs: None,
646 resolution: None,
647 examples: None,
648 notes: None,
649 code: None,
650 code_blocks: None,
651 verify: None,
652 agent_context: None,
653 target: None,
654 execution_status: None,
655 executed_by: None,
656 executed_at: None,
657 execution_log: None,
658 retry_count: None,
659 parallel_groups: None,
660 memory: None,
661 scope: None,
662 applies_when: None,
663 valid_from: None,
664 valid_until: None,
665 tags: None,
666 aliases: None,
667 keywords: None,
668 extra_fields: BTreeMap::new(),
669 span: Span::new(1, 1),
670 }
671 }
672
673 #[test]
674 fn test_classify_severity_type_modified_returns_breaking() {
675 assert_eq!(
676 classify_severity("node_type", ChangeKind::Modified),
677 ChangeSeverity::Breaking
678 );
679 }
680
681 #[test]
682 fn test_classify_severity_summary_modified_returns_minor() {
683 assert_eq!(
684 classify_severity("summary", ChangeKind::Modified),
685 ChangeSeverity::Minor
686 );
687 }
688
689 #[test]
690 fn test_classify_severity_detail_modified_returns_info() {
691 assert_eq!(
692 classify_severity("detail", ChangeKind::Modified),
693 ChangeSeverity::Info
694 );
695 }
696
697 #[test]
698 fn test_classify_severity_depends_removed_returns_breaking() {
699 assert_eq!(
700 classify_severity("depends", ChangeKind::Removed),
701 ChangeSeverity::Breaking
702 );
703 }
704
705 #[test]
706 fn test_classify_severity_steps_removed_returns_breaking() {
707 assert_eq!(
708 classify_severity("steps", ChangeKind::Removed),
709 ChangeSeverity::Breaking
710 );
711 }
712
713 #[test]
714 fn test_classify_severity_code_removed_returns_breaking() {
715 assert_eq!(
716 classify_severity("code", ChangeKind::Removed),
717 ChangeSeverity::Breaking
718 );
719 }
720
721 #[test]
722 fn test_classify_severity_tags_added_returns_info() {
723 assert_eq!(
724 classify_severity("tags", ChangeKind::Added),
725 ChangeSeverity::Info
726 );
727 }
728
729 #[test]
730 fn test_classify_severity_unknown_field_returns_info() {
731 assert_eq!(
732 classify_severity("some_unknown_field", ChangeKind::Modified),
733 ChangeSeverity::Info
734 );
735 }
736
737 #[test]
738 fn test_diff_all_fields_identical_returns_empty() {
739 let node = minimal_node();
740 let changes = diff_all_fields(&node, &node);
741 assert!(changes.is_empty());
742 }
743
744 #[test]
745 fn test_diff_all_fields_type_changed_returns_breaking() {
746 let left = minimal_node();
747 let mut right = left.clone();
748 right.node_type = NodeType::Rules;
749 let changes = diff_all_fields(&left, &right);
750 assert_eq!(changes.len(), 1);
751 assert_eq!(changes[0].field, "type");
752 assert_eq!(changes[0].severity, ChangeSeverity::Breaking);
753 }
754
755 #[test]
756 fn test_diff_all_fields_summary_changed_returns_minor() {
757 let left = minimal_node();
758 let mut right = left.clone();
759 right.summary = "different summary".to_owned();
760 let changes = diff_all_fields(&left, &right);
761 assert_eq!(changes.len(), 1);
762 assert_eq!(changes[0].field, "summary");
763 assert_eq!(changes[0].severity, ChangeSeverity::Minor);
764 }
765
766 #[test]
767 fn test_diff_all_fields_priority_added_returns_info() {
768 let left = minimal_node();
769 let mut right = left.clone();
770 right.priority = Some(Priority::High);
771 let changes = diff_all_fields(&left, &right);
772 assert_eq!(changes.len(), 1);
773 assert_eq!(changes[0].field, "priority");
774 assert_eq!(changes[0].kind, ChangeKind::Added);
775 assert_eq!(changes[0].severity, ChangeSeverity::Info);
776 }
777
778 #[test]
779 fn test_diff_all_fields_priority_removed_returns_minor() {
780 let mut left = minimal_node();
781 left.priority = Some(Priority::High);
782 let right = minimal_node();
783 let changes = diff_all_fields(&left, &right);
784 assert_eq!(changes.len(), 1);
785 assert_eq!(changes[0].field, "priority");
786 assert_eq!(changes[0].kind, ChangeKind::Removed);
787 assert_eq!(changes[0].severity, ChangeSeverity::Minor);
788 }
789
790 #[test]
791 fn test_diff_all_fields_extra_field_added_returns_info() {
792 let left = minimal_node();
793 let mut right = left.clone();
794 right.extra_fields.insert(
795 "custom_key".to_owned(),
796 FieldValue::Scalar("val".to_owned()),
797 );
798 let changes = diff_all_fields(&left, &right);
799 assert_eq!(changes.len(), 1);
800 assert_eq!(changes[0].field, "custom_key");
801 assert_eq!(changes[0].kind, ChangeKind::Added);
802 assert_eq!(changes[0].severity, ChangeSeverity::Info);
803 }
804
805 #[test]
806 fn test_diff_all_fields_extra_field_removed_returns_info() {
807 let mut left = minimal_node();
808 left.extra_fields.insert(
809 "custom_key".to_owned(),
810 FieldValue::Scalar("val".to_owned()),
811 );
812 let right = minimal_node();
813 let changes = diff_all_fields(&left, &right);
814 assert_eq!(changes.len(), 1);
815 assert_eq!(changes[0].field, "custom_key");
816 assert_eq!(changes[0].kind, ChangeKind::Removed);
817 assert_eq!(changes[0].severity, ChangeSeverity::Info);
818 }
819
820 #[test]
821 fn test_diff_all_fields_code_block_changed_returns_minor() {
822 use crate::model::code::{CodeAction, CodeBlock};
823 let left = minimal_node();
824 let mut right = left.clone();
825 right.code = Some(CodeBlock {
826 lang: Some("rust".to_owned()),
827 target: None,
828 action: CodeAction::Full,
829 body: "fn main() {}".to_owned(),
830 anchor: None,
831 old: None,
832 });
833 let changes = diff_all_fields(&left, &right);
834 assert_eq!(changes.len(), 1);
835 assert_eq!(changes[0].field, "code");
836 assert_eq!(changes[0].kind, ChangeKind::Added);
837 assert_eq!(changes[0].severity, ChangeSeverity::Minor);
839 }
840
841 #[test]
842 fn test_field_value_snapshot_from_field_value_scalar() {
843 let fv = FieldValue::Scalar("hello".to_owned());
844 let snap = FieldValueSnapshot::from_field_value(&fv);
845 assert_eq!(snap, FieldValueSnapshot::Scalar("hello".to_owned()));
846 }
847
848 #[test]
849 fn test_field_value_snapshot_from_field_value_list() {
850 let fv = FieldValue::List(vec!["a".to_owned(), "b".to_owned()]);
851 let snap = FieldValueSnapshot::from_field_value(&fv);
852 assert_eq!(
853 snap,
854 FieldValueSnapshot::List(vec!["a".to_owned(), "b".to_owned()])
855 );
856 }
857
858 #[test]
859 fn test_field_value_snapshot_from_complex_serializes_json() {
860 #[derive(Serialize)]
861 struct Simple {
862 key: &'static str,
863 }
864 let val = Simple { key: "value" };
865 let snap = FieldValueSnapshot::from_complex(&val);
866 assert!(matches!(snap, FieldValueSnapshot::Complex(_)));
867 if let FieldValueSnapshot::Complex(s) = snap {
868 assert!(s.contains("key"));
869 assert!(s.contains("value"));
870 }
871 }
872}