Skip to main content

pjson_rs/stream/
priority.rs

1//! Priority-based JSON streaming implementation
2//!
3//! This module implements the core Priority JSON Streaming protocol with:
4//! - Skeleton-first approach
5//! - JSON Path based patching
6//! - Priority-based field ordering
7//! - Incremental reconstruction
8
9use crate::Result;
10use crate::domain::value_objects::{JsonPath, Priority};
11use serde_json::{Map as JsonMap, Value as JsonValue};
12use std::collections::VecDeque;
13
14/// Custom serde for Priority in stream module
15mod serde_priority {
16    use crate::domain::value_objects::Priority;
17    use serde::{Serialize, Serializer};
18
19    pub fn serialize<S>(priority: &Priority, serializer: S) -> Result<S::Ok, S::Error>
20    where
21        S: Serializer,
22    {
23        priority.value().serialize(serializer)
24    }
25}
26
27/// Patch operation for updating JSON structure
28#[derive(Debug, Clone, serde::Serialize)]
29pub struct JsonPatch {
30    /// Path within the target JSON document.
31    pub path: JsonPath,
32    /// Operation to apply at `path`.
33    pub operation: PatchOperation,
34    /// Priority assigned to this patch.
35    #[serde(with = "serde_priority")]
36    pub priority: Priority,
37}
38
39/// Operation a [`JsonPatch`] performs at its target path.
40#[derive(Debug, Clone, serde::Serialize)]
41pub enum PatchOperation {
42    /// Replace the value at the path with `value`.
43    Set {
44        /// New value to set at the path.
45        value: JsonValue,
46    },
47    /// Append values to the array at the path.
48    Append {
49        /// Values appended to the target array.
50        values: Vec<JsonValue>,
51    },
52    /// Replace the value at the path with `value` (semantically distinct from `Set`).
53    Replace {
54        /// Replacement value.
55        value: JsonValue,
56    },
57    /// Remove the value at the path.
58    Remove,
59}
60
61/// Streaming frame containing skeleton or patch data
62#[derive(Debug, Clone, serde::Serialize)]
63pub enum PriorityStreamFrame {
64    /// Initial skeleton frame with placeholder values.
65    Skeleton {
66        /// Skeleton JSON value (nulls/empties for fields filled later).
67        data: JsonValue,
68        /// Priority of the skeleton frame.
69        #[serde(with = "serde_priority")]
70        priority: Priority,
71        /// Whether the skeleton is final or further skeletons may follow.
72        complete: bool,
73    },
74    /// Batch of patches sharing the same priority.
75    Patch {
76        /// Patches in this batch.
77        patches: Vec<JsonPatch>,
78        /// Priority shared by all patches in the batch.
79        #[serde(with = "serde_priority")]
80        priority: Priority,
81    },
82    /// Terminal frame indicating the stream is complete.
83    Complete {
84        /// Optional checksum of the reconstructed payload.
85        checksum: Option<u64>,
86    },
87}
88
89/// Priority-based JSON streamer
90pub struct PriorityStreamer {
91    config: StreamerConfig,
92}
93
94/// Configuration for [`PriorityStreamer`].
95#[derive(Debug, Clone)]
96pub struct StreamerConfig {
97    /// Enable name-based heuristics that infer priorities from common field names.
98    pub detect_semantics: bool,
99    /// Maximum number of patches per [`PriorityStreamFrame::Patch`] batch.
100    pub max_patch_size: usize,
101    /// Patches with priority below this threshold are dropped.
102    pub priority_threshold: Priority,
103}
104
105impl Default for StreamerConfig {
106    fn default() -> Self {
107        Self {
108            detect_semantics: true,
109            max_patch_size: 100,
110            priority_threshold: Priority::LOW,
111        }
112    }
113}
114
115impl PriorityStreamer {
116    /// Create new priority streamer
117    pub fn new() -> Self {
118        Self::with_config(StreamerConfig::default())
119    }
120
121    /// Create streamer with custom configuration
122    pub fn with_config(config: StreamerConfig) -> Self {
123        Self { config }
124    }
125
126    /// Analyze JSON and create streaming plan
127    pub fn analyze(&self, json: &JsonValue) -> Result<StreamingPlan> {
128        let mut plan = StreamingPlan::new();
129
130        // Generate skeleton
131        let skeleton = self.generate_skeleton(json);
132        plan.frames.push_back(PriorityStreamFrame::Skeleton {
133            data: skeleton,
134            priority: Priority::CRITICAL,
135            complete: false,
136        });
137
138        // Extract patches by priority
139        let mut patches = Vec::new();
140        self.extract_patches(json, &JsonPath::root(), &mut patches)?;
141
142        // Group patches by priority
143        patches.sort_by_key(|patch| std::cmp::Reverse(patch.priority));
144
145        let mut current_priority = Priority::CRITICAL;
146        let mut current_batch = Vec::new();
147
148        for patch in patches {
149            if patch.priority != current_priority && !current_batch.is_empty() {
150                plan.frames.push_back(PriorityStreamFrame::Patch {
151                    patches: current_batch,
152                    priority: current_priority,
153                });
154                current_batch = Vec::new();
155            }
156            current_priority = patch.priority;
157            current_batch.push(patch);
158
159            if current_batch.len() >= self.config.max_patch_size {
160                plan.frames.push_back(PriorityStreamFrame::Patch {
161                    patches: current_batch,
162                    priority: current_priority,
163                });
164                current_batch = Vec::new();
165            }
166        }
167
168        // Add remaining patches
169        if !current_batch.is_empty() {
170            plan.frames.push_back(PriorityStreamFrame::Patch {
171                patches: current_batch,
172                priority: current_priority,
173            });
174        }
175
176        // Add completion frame
177        plan.frames
178            .push_back(PriorityStreamFrame::Complete { checksum: None });
179
180        Ok(plan)
181    }
182
183    /// Generate skeleton structure with null/empty values
184    fn generate_skeleton(&self, json: &JsonValue) -> JsonValue {
185        match json {
186            JsonValue::Object(map) => {
187                let mut skeleton = JsonMap::new();
188                for (key, value) in map {
189                    skeleton.insert(
190                        key.clone(),
191                        match value {
192                            JsonValue::Array(_) => JsonValue::Array(vec![]),
193                            JsonValue::Object(_) => self.generate_skeleton(value),
194                            JsonValue::String(_) => JsonValue::Null,
195                            JsonValue::Number(_) => JsonValue::Number(0.into()),
196                            JsonValue::Bool(_) => JsonValue::Bool(false),
197                            JsonValue::Null => JsonValue::Null,
198                        },
199                    );
200                }
201                JsonValue::Object(skeleton)
202            }
203            JsonValue::Array(_) => JsonValue::Array(vec![]),
204            _ => JsonValue::Null,
205        }
206    }
207
208    /// Deep-clone `value`, emptying every array reachable via a JsonPath-encodable
209    /// key — mirrors `extract_patches`'s own traversal exactly. A subtree under a
210    /// key `JsonPath` cannot encode (`.`/`[`/`]`) is left fully populated, since
211    /// `extract_patches`'s recursion will never reach it to emit a compensating
212    /// `Append` (see #394 C3). Encodability depends only on the key string itself
213    /// (`JsonPath::append_key`'s sole failure mode), not on the accumulated path,
214    /// so this check does not need to thread a `JsonPath` through the recursion.
215    fn skeletonize_arrays(value: &JsonValue) -> JsonValue {
216        match value {
217            JsonValue::Object(map) => {
218                let skeleton = map
219                    .iter()
220                    .map(|(key, v)| match JsonPath::root().append_key(key) {
221                        Ok(_) => (key.clone(), Self::skeletonize_arrays(v)),
222                        Err(_) => (key.clone(), v.clone()),
223                    })
224                    .collect();
225                JsonValue::Object(skeleton)
226            }
227            JsonValue::Array(_) => JsonValue::Array(vec![]),
228            other => other.clone(),
229        }
230    }
231
232    /// Extract patches from JSON structure
233    fn extract_patches(
234        &self,
235        json: &JsonValue,
236        current_path: &JsonPath,
237        patches: &mut Vec<JsonPatch>,
238    ) -> Result<()> {
239        match json {
240            JsonValue::Object(map) => {
241                for (key, value) in map {
242                    // Keys JsonPath cannot encode (`.`, `[`, `]`) are skipped:
243                    // one weird key must not abort the whole streaming plan.
244                    let Ok(field_path) = current_path.append_key(key) else {
245                        continue;
246                    };
247                    let own_priority = self.calculate_field_priority(&field_path, key, value);
248
249                    let mut child_patches = Vec::new();
250                    self.extract_patches(value, &field_path, &mut child_patches)?;
251
252                    // Hoist the Set's priority to at least the highest Append
253                    // priority anywhere in its subtree, so a Set can never be
254                    // sorted/applied after an Append it must precede (#394 C1/C2).
255                    let append_ceiling = child_patches
256                        .iter()
257                        .filter(|p| matches!(p.operation, PatchOperation::Append { .. }))
258                        .map(|p| p.priority)
259                        .max();
260                    let priority =
261                        append_ceiling.map_or(own_priority, |ceiling| own_priority.max(ceiling));
262
263                    patches.push(JsonPatch {
264                        path: field_path.clone(),
265                        operation: PatchOperation::Set {
266                            value: Self::skeletonize_arrays(value),
267                        },
268                        priority,
269                    });
270
271                    patches.extend(child_patches);
272                }
273            }
274            JsonValue::Array(arr) => {
275                // For arrays, create append operations in chunks
276                if arr.len() > 10 {
277                    // Priority is computed once from the full array so every
278                    // chunk of the same array shares it: computing it per-chunk
279                    // let a short tail chunk outrank the bulk chunks and jump
280                    // ahead of them in the priority sort, corrupting element
281                    // order on reconstruction (#394 C2, chunked variant).
282                    let priority = self.calculate_array_priority(current_path, arr);
283                    for chunk in arr.chunks(self.config.max_patch_size) {
284                        patches.push(JsonPatch {
285                            path: current_path.clone(),
286                            operation: PatchOperation::Append {
287                                values: chunk.to_vec(),
288                            },
289                            priority,
290                        });
291                    }
292                } else if !arr.is_empty() {
293                    patches.push(JsonPatch {
294                        path: current_path.clone(),
295                        operation: PatchOperation::Append {
296                            values: arr.clone(),
297                        },
298                        priority: self.calculate_array_priority(current_path, arr),
299                    });
300                }
301            }
302            _ => {
303                // Primitive values handled by parent object/array
304            }
305        }
306
307        Ok(())
308    }
309
310    /// Calculate priority for a field based on path and content
311    fn calculate_field_priority(&self, _path: &JsonPath, key: &str, value: &JsonValue) -> Priority {
312        // Critical fields
313        if matches!(key, "id" | "uuid" | "status" | "type" | "kind") {
314            return Priority::CRITICAL;
315        }
316
317        // High priority fields
318        if matches!(key, "name" | "title" | "label" | "email" | "username") {
319            return Priority::HIGH;
320        }
321
322        // Low priority patterns
323        if key.contains("analytics") || key.contains("stats") || key.contains("meta") {
324            return Priority::LOW;
325        }
326
327        if matches!(key, "reviews" | "comments" | "logs" | "history") {
328            return Priority::BACKGROUND;
329        }
330
331        // Content-based priority
332        match value {
333            JsonValue::Array(arr) if arr.len() > 100 => Priority::BACKGROUND,
334            JsonValue::Object(obj) if obj.contains_key("timestamp") => Priority::MEDIUM,
335            JsonValue::String(s) if s.len() > 1000 => Priority::LOW,
336            _ => Priority::MEDIUM,
337        }
338    }
339
340    /// Calculate priority for array elements
341    fn calculate_array_priority(&self, path: &JsonPath, elements: &[JsonValue]) -> Priority {
342        // Large arrays get background priority
343        if elements.len() > 50 {
344            return Priority::BACKGROUND;
345        }
346
347        // Arrays in certain paths get different priorities
348        if let Some(last_key) = path.last_key() {
349            if matches!(last_key, "reviews" | "comments" | "logs") {
350                return Priority::BACKGROUND;
351            }
352            if matches!(last_key, "items" | "data" | "results") {
353                return Priority::MEDIUM;
354            }
355        }
356
357        Priority::MEDIUM
358    }
359}
360
361/// Plan for streaming JSON with priority ordering
362#[derive(Debug)]
363pub struct StreamingPlan {
364    /// Ordered queue of frames produced by analysis.
365    pub frames: VecDeque<PriorityStreamFrame>,
366}
367
368impl Default for StreamingPlan {
369    fn default() -> Self {
370        Self::new()
371    }
372}
373
374impl StreamingPlan {
375    /// Create an empty plan with no frames.
376    pub fn new() -> Self {
377        Self {
378            frames: VecDeque::new(),
379        }
380    }
381
382    /// Get next frame to send
383    pub fn next_frame(&mut self) -> Option<PriorityStreamFrame> {
384        self.frames.pop_front()
385    }
386
387    /// Check if streaming is complete
388    pub fn is_complete(&self) -> bool {
389        self.frames.is_empty()
390    }
391
392    /// Get remaining frame count
393    pub fn remaining_frames(&self) -> usize {
394        self.frames.len()
395    }
396
397    /// Get iterator over frames
398    pub fn frames(&self) -> impl Iterator<Item = &PriorityStreamFrame> {
399        self.frames.iter()
400    }
401}
402
403impl Default for PriorityStreamer {
404    fn default() -> Self {
405        Self::new()
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use crate::stream::reconstruction::JsonReconstructor;
413    use serde_json::json;
414
415    /// Runs `payload` through `PriorityStreamer::analyze()` and applies every
416    /// produced frame to a fresh `JsonReconstructor`, returning the reconstructed
417    /// document. Exercises the full `analyze()` -> `JsonReconstructor` pipeline,
418    /// not just patch inspection (see spec NFR-004).
419    fn round_trip(streamer: &PriorityStreamer, payload: &JsonValue) -> JsonValue {
420        let plan = streamer.analyze(payload).unwrap();
421        let mut reconstructor = JsonReconstructor::new();
422        for frame in plan.frames {
423            reconstructor.add_frame(frame);
424        }
425        reconstructor.process_all_frames().unwrap();
426        reconstructor.current_state().clone()
427    }
428
429    #[test]
430    fn test_json_path_creation() {
431        let path = JsonPath::root();
432        assert_eq!(path.to_json_pointer(), "/");
433
434        let path = path
435            .append_key("users")
436            .unwrap()
437            .append_index(0)
438            .append_key("name")
439            .unwrap();
440        assert_eq!(path.to_json_pointer(), "/users/0/name");
441    }
442
443    #[test]
444    fn test_priority_comparison() {
445        assert!(Priority::CRITICAL > Priority::HIGH);
446        assert!(Priority::HIGH > Priority::MEDIUM);
447        assert!(Priority::MEDIUM > Priority::LOW);
448        assert!(Priority::LOW > Priority::BACKGROUND);
449    }
450
451    #[test]
452    fn test_skeleton_generation() {
453        let streamer = PriorityStreamer::new();
454        let json = json!({
455            "name": "John",
456            "age": 30,
457            "active": true,
458            "posts": ["post1", "post2"]
459        });
460
461        let skeleton = streamer.generate_skeleton(&json);
462        let expected = json!({
463            "name": null,
464            "age": 0,
465            "active": false,
466            "posts": []
467        });
468
469        assert_eq!(skeleton, expected);
470    }
471
472    #[test]
473    fn test_field_priority_calculation() {
474        let streamer = PriorityStreamer::new();
475        let path = JsonPath::root();
476
477        assert_eq!(
478            streamer.calculate_field_priority(&path, "id", &json!(123)),
479            Priority::CRITICAL
480        );
481
482        assert_eq!(
483            streamer.calculate_field_priority(&path, "name", &json!("John")),
484            Priority::HIGH
485        );
486
487        assert_eq!(
488            streamer.calculate_field_priority(&path, "reviews", &json!([])),
489            Priority::BACKGROUND
490        );
491    }
492
493    #[test]
494    fn test_streaming_plan_creation() {
495        let streamer = PriorityStreamer::new();
496        let json = json!({
497            "id": 1,
498            "name": "John",
499            "bio": "Software developer",
500            "reviews": ["Good", "Excellent"]
501        });
502
503        let plan = streamer.analyze(&json).unwrap();
504        assert!(!plan.is_complete());
505        assert!(plan.remaining_frames() > 0);
506    }
507
508    // Regression tests for object-nested array duplication (spec 031 / issue #394):
509    // `extract_patches` used to emit a `Set` patch carrying the full array value for
510    // an object field, then recurse and emit an `Append` patch for the same array,
511    // duplicating every non-empty object-nested array after a full analyze() ->
512    // JsonReconstructor round trip. These tests wire analyze() directly into
513    // JsonReconstructor (not just patch inspection) per US-003/NFR-004.
514
515    #[test]
516    fn test_round_trip_exact_repro_case() {
517        let streamer = PriorityStreamer::new();
518        let payload = json!({"items": [1, 2, 3]});
519
520        let result = round_trip(&streamer, &payload);
521
522        assert_eq!(result, payload);
523        assert_eq!(result["items"], json!([1, 2, 3]));
524    }
525
526    #[test]
527    fn test_round_trip_multi_entity_payload() {
528        let streamer = PriorityStreamer::new();
529        let payload = json!({
530            "users": [
531                {"id": 1, "name": "Alice"},
532                {"id": 2, "name": "Bob"}
533            ],
534            "metadata": {
535                "nested": {
536                    "deep": [1, 2, 3, 4, 5]
537                }
538            }
539        });
540
541        let result = round_trip(&streamer, &payload);
542
543        assert_eq!(result, payload);
544    }
545
546    #[test]
547    fn test_round_trip_chunked_array_exceeds_max_patch_size() {
548        let config = StreamerConfig {
549            max_patch_size: 5,
550            ..StreamerConfig::default()
551        };
552        let streamer = PriorityStreamer::with_config(config);
553        let items: Vec<JsonValue> = (0..23).map(|i| json!(i)).collect();
554        let payload = json!({ "data": items });
555
556        let result = round_trip(&streamer, &payload);
557
558        assert_eq!(result, payload);
559        assert_eq!(result["data"].as_array().unwrap().len(), 23);
560    }
561
562    #[test]
563    fn test_round_trip_chunked_array_divergent_chunk_priority() {
564        // #394 M2: guards against computing `calculate_array_priority` per
565        // chunk slice instead of once for the whole array. With
566        // max_patch_size 60 over a 130-element "items" array, chunks are
567        // 60/60/10: under the old per-chunk scheme the 10-element tail (len
568        // <= 50) would fall through to the "items" last-key boost and get
569        // MEDIUM priority, while the two 60-element head chunks (len > 50)
570        // get BACKGROUND — the higher-priority tail would then sort ahead of
571        // the head chunks and be applied first, corrupting element order.
572        let config = StreamerConfig {
573            max_patch_size: 60,
574            ..StreamerConfig::default()
575        };
576        let streamer = PriorityStreamer::with_config(config);
577        let items: Vec<JsonValue> = (0..130).map(|i| json!(i)).collect();
578        let payload = json!({ "items": items });
579
580        let result = round_trip(&streamer, &payload);
581
582        assert_eq!(result, payload);
583    }
584
585    #[test]
586    fn test_round_trip_array_nested_at_depth_three() {
587        let streamer = PriorityStreamer::new();
588        let payload = json!({
589            "level1": {
590                "level2": {
591                    "level3": [1, 2, 3, 4]
592                }
593            }
594        });
595
596        let result = round_trip(&streamer, &payload);
597
598        assert_eq!(result, payload);
599    }
600
601    #[test]
602    fn test_round_trip_array_of_arrays() {
603        let streamer = PriorityStreamer::new();
604        let payload = json!({
605            "matrix": [[1, 2], [3, 4]]
606        });
607
608        let result = round_trip(&streamer, &payload);
609
610        assert_eq!(result, payload);
611    }
612
613    #[test]
614    fn test_round_trip_array_of_objects_with_nested_arrays() {
615        let streamer = PriorityStreamer::new();
616        let payload = json!({
617            "users": [
618                {"name": "Alice", "tags": ["admin", "active"]},
619                {"name": "Bob", "tags": ["guest"]}
620            ]
621        });
622
623        let result = round_trip(&streamer, &payload);
624
625        assert_eq!(result, payload);
626    }
627
628    #[test]
629    fn test_round_trip_empty_array_field() {
630        let streamer = PriorityStreamer::new();
631        let payload = json!({"items": []});
632
633        let result = round_trip(&streamer, &payload);
634
635        assert_eq!(result, payload);
636    }
637
638    #[test]
639    fn test_round_trip_top_level_bare_array() {
640        let streamer = PriorityStreamer::new();
641        let payload = json!([1, 2, 3]);
642
643        let result = round_trip(&streamer, &payload);
644
645        assert_eq!(result, payload);
646    }
647
648    #[test]
649    fn test_round_trip_mixed_payload_no_regression() {
650        let streamer = PriorityStreamer::new();
651        let payload = json!({
652            "id": 1,
653            "name": "widget",
654            "tags": ["a", "b", "c"],
655            "details": {
656                "color": "red",
657                "size": 10
658            }
659        });
660
661        let result = round_trip(&streamer, &payload);
662
663        assert_eq!(result, payload);
664        assert_eq!(result["tags"], json!(["a", "b", "c"]));
665        assert_eq!(result["details"], json!({"color": "red", "size": 10}));
666    }
667
668    // Regression cases for the Set/Append priority-inversion data-loss bug
669    // (impl-critic findings C1-C3, tracked alongside issue #394's redesign).
670    // `analyze()` sorts patches by priority *descending*, independently of
671    // path/depth, so a field's `Append` (or a descendant's `Set`/`Append`) can
672    // land in an earlier-processed, higher-priority batch than its own or an
673    // ancestor's `Set`. Pre-fix this was harmless (`Set` always carried the
674    // full pristine value); post-fix `Set` carries a skeleton, so an
675    // out-of-order `Set` destructively wipes already-applied data. These are
676    // expected to FAIL until the ordering issue is fixed (see task #9).
677
678    #[test]
679    fn test_round_trip_same_path_priority_inversion() {
680        // "history" gets BACKGROUND field priority (matches the
681        // id/uuid/.../history critical-field-name list) but its Append gets
682        // MEDIUM array priority ("history" is absent from the
683        // reviews|comments|logs array-priority boost list) -> Append (MEDIUM)
684        // applies before Set (BACKGROUND) wipes the field to `[]`.
685        let streamer = PriorityStreamer::new();
686        let payload = json!({"history": [1, 2, 3]});
687
688        let result = round_trip(&streamer, &payload);
689
690        assert_eq!(result, payload);
691    }
692
693    #[test]
694    fn test_round_trip_parent_object_priority_inversion() {
695        // Any key containing "stats"/"analytics"/"meta" gets LOW field
696        // priority, but its nested array field defaults to MEDIUM -> the
697        // nested Set/Append pair (MEDIUM) applies and populates correctly,
698        // then the ancestor's skeletonized Set (LOW) applies afterward and
699        // wipes the nested field back to `[]`.
700        let streamer = PriorityStreamer::new();
701        let payload = json!({"stats": {"values": [1, 2, 3]}});
702
703        let result = round_trip(&streamer, &payload);
704
705        assert_eq!(result, payload);
706    }
707
708    #[test]
709    fn test_round_trip_parent_object_priority_inversion_chunked() {
710        // Same class as above, but with a >100-element array so the chunking
711        // branch fires: the tail chunk (<=50 elements) gets MEDIUM while the
712        // head chunk (>50 elements) and the ancestor's Set get BACKGROUND/LOW
713        // respectively, so only some elements survive the ancestor Set wipe.
714        let streamer = PriorityStreamer::new();
715        let values: Vec<JsonValue> = (0..101).map(|i| json!(i)).collect();
716        let payload = json!({"stats": {"values": values}});
717
718        let result = round_trip(&streamer, &payload);
719
720        assert_eq!(result, payload);
721        assert_eq!(
722            result["stats"]["values"].as_array().unwrap().len(),
723            101,
724            "expected all 101 elements to survive the round trip"
725        );
726    }
727
728    #[test]
729    fn test_round_trip_unencodable_key_parent_wipe() {
730        // `extract_patches` skips recursion into keys JsonPath cannot encode
731        // (containing '.', '[', ']'), so no Set/Append is ever emitted for
732        // "weird.key" itself. Pre-fix, the parent "outer" field's Set carried
733        // the full pristine value (including "weird.key"'s array) as a safety
734        // net. Post-fix, `skeletonize_arrays` recurses into "outer" and empties
735        // "weird.key"'s array too, permanently losing the only copy of its data.
736        let streamer = PriorityStreamer::new();
737        let payload = json!({"outer": {"weird.key": [1, 2, 3]}});
738
739        let result = round_trip(&streamer, &payload);
740
741        assert_eq!(result, payload);
742    }
743}