Skip to main content

adk_graph/
deferred.rs

1//! Deferred node (fan-in barrier) support for graph workflows.
2//!
3//! Provides fan-in barrier semantics for nodes that wait on multiple upstream
4//! parallel paths before executing. This enables scatter-gather patterns where
5//! work is distributed across parallel branches and then collected at a single
6//! synchronization point.
7//!
8//! # Overview
9//!
10//! A deferred node is declared with a [`DeferredNodeConfig`] that specifies:
11//! - [`MergeStrategy`]: How upstream outputs are combined (collect, merge maps, first, or custom).
12//! - `fan_in_timeout`: Optional maximum wait duration for all upstream paths.
13//!
14//! The [`FanInTracker`] tracks which upstream paths have completed and merges
15//! their outputs according to the configured strategy.
16//!
17//! # Example
18//!
19//! ```rust
20//! use std::time::Duration;
21//! use adk_graph::deferred::{DeferredNodeConfig, FanInTracker, MergeStrategy};
22//! use serde_json::json;
23//!
24//! // Configure a deferred node that collects all upstream outputs
25//! let config = DeferredNodeConfig {
26//!     merge_strategy: MergeStrategy::Collect,
27//!     fan_in_timeout: Some(Duration::from_secs(30)),
28//!     ..Default::default()
29//! };
30//!
31//! // Track upstream completions
32//! let mut tracker = FanInTracker::new(vec!["branch_a", "branch_b", "branch_c"]);
33//!
34//! tracker.record("branch_a", json!({"result": 1}));
35//! tracker.record("branch_b", json!({"result": 2}));
36//! assert!(!tracker.is_ready());
37//!
38//! tracker.record("branch_c", json!({"result": 3}));
39//! assert!(tracker.is_ready());
40//!
41//! // Merge outputs using the configured strategy
42//! let merged = tracker.merge(&config.merge_strategy);
43//! assert_eq!(merged, json!([{"result": 1}, {"result": 2}, {"result": 3}]));
44//! ```
45
46use std::collections::{BTreeMap, HashMap, HashSet};
47use std::fmt;
48use std::sync::Arc;
49use std::time::{Duration, Instant};
50
51use serde::{Deserialize, Serialize};
52use serde_json::Value;
53
54use crate::error::{GraphError, Result};
55
56/// How to combine outputs from multiple upstream parallel paths.
57///
58/// The merge strategy determines how the collected outputs from all upstream
59/// branches are combined into a single value for the deferred node's input.
60///
61/// # Example
62///
63/// ```rust
64/// use adk_graph::deferred::MergeStrategy;
65///
66/// // Default strategy collects all outputs into a Vec
67/// let strategy = MergeStrategy::default();
68/// assert!(matches!(strategy, MergeStrategy::Collect));
69///
70/// // MergeMap combines all output maps with last-write-wins
71/// let strategy = MergeStrategy::MergeMap;
72/// ```
73#[derive(Clone, Default)]
74pub enum MergeStrategy {
75    /// Collect all outputs into a `Vec<Value>`.
76    ///
77    /// Outputs are ordered by the insertion order of source nodes
78    /// (the order in which they were recorded).
79    #[default]
80    Collect,
81
82    /// Merge all output maps into a single map (last-write-wins on key conflict).
83    ///
84    /// Each upstream output is expected to be a JSON object. Non-object outputs
85    /// are skipped. When multiple outputs contain the same key, the value from
86    /// the later-recorded source wins.
87    MergeMap,
88
89    /// Use only the first completed output.
90    ///
91    /// Returns the output from whichever upstream path completed first
92    /// (i.e., was recorded first).
93    First,
94
95    /// Custom merge function.
96    ///
97    /// Accepts a closure that takes all collected outputs and produces a
98    /// single merged value.
99    ///
100    /// # Example
101    ///
102    /// ```rust
103    /// use std::sync::Arc;
104    /// use adk_graph::deferred::MergeStrategy;
105    /// use serde_json::{json, Value};
106    ///
107    /// let strategy = MergeStrategy::Custom(Arc::new(|outputs: Vec<Value>| {
108    ///     json!({ "count": outputs.len() })
109    /// }));
110    /// ```
111    Custom(Arc<dyn Fn(Vec<Value>) -> Value + Send + Sync>),
112}
113
114impl fmt::Debug for MergeStrategy {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            Self::Collect => write!(f, "Collect"),
118            Self::MergeMap => write!(f, "MergeMap"),
119            Self::First => write!(f, "First"),
120            Self::Custom(_) => write!(f, "Custom(<fn>)"),
121        }
122    }
123}
124
125/// Configuration for a deferred (fan-in) node.
126///
127/// A deferred node waits for all upstream parallel paths to complete before
128/// executing. The configuration controls how outputs are merged and how long
129/// the node waits.
130///
131/// # Example
132///
133/// ```rust
134/// use std::time::Duration;
135/// use adk_graph::deferred::{DeferredNodeConfig, MergeStrategy};
136///
137/// let config = DeferredNodeConfig {
138///     merge_strategy: MergeStrategy::MergeMap,
139///     fan_in_timeout: Some(Duration::from_secs(60)),
140///     ..Default::default()
141/// };
142/// ```
143#[derive(Debug, Clone, Default)]
144pub struct DeferredNodeConfig {
145    /// Strategy for combining upstream outputs.
146    pub merge_strategy: MergeStrategy,
147
148    /// Maximum time to wait for all upstream paths to complete.
149    ///
150    /// - `None`: Wait indefinitely for all upstream paths.
151    /// - `Some(duration)`: If the timeout expires and some paths have completed,
152    ///   proceed with partial results. If zero paths have completed, return
153    ///   `GraphError::FanInTimedOut`.
154    pub fan_in_timeout: Option<Duration>,
155    /// How many predecessors must have arrived for `fan_in_timeout` to release
156    /// the node with partial results.
157    ///
158    /// `None` means one, which is the behaviour this field replaced. Set it to
159    /// the full predecessor count to require all of them even after a timeout, or
160    /// to any value between for an n-of-m join: release once that many branches
161    /// have answered and abandon the rest.
162    pub min_predecessors: Option<usize>,
163}
164
165/// Tracks which upstream paths have completed for a deferred node.
166///
167/// The tracker maintains a set of expected source nodes and records their
168/// outputs as they arrive. Once all expected sources have reported, the
169/// tracker is ready and outputs can be merged.
170///
171/// # Example
172///
173/// ```rust
174/// use adk_graph::deferred::{FanInTracker, MergeStrategy};
175/// use serde_json::json;
176///
177/// let mut tracker = FanInTracker::new(vec!["node_a", "node_b"]);
178///
179/// assert!(!tracker.is_ready());
180/// assert_eq!(tracker.received_count(), 0);
181/// assert_eq!(tracker.expected_count(), 2);
182///
183/// tracker.record("node_a", json!("output_a"));
184/// assert!(!tracker.is_ready());
185///
186/// tracker.record("node_b", json!("output_b"));
187/// assert!(tracker.is_ready());
188///
189/// let merged = tracker.merge(&MergeStrategy::Collect);
190/// assert_eq!(merged, json!(["output_a", "output_b"]));
191/// ```
192pub struct FanInTracker {
193    /// The set of source node names we expect to receive output from.
194    expected: HashSet<String>,
195    /// Outputs received so far, keyed by source node name.
196    received: HashMap<String, Value>,
197    /// Insertion order of received outputs (for deterministic merge ordering).
198    insertion_order: Vec<String>,
199}
200
201impl FanInTracker {
202    /// Create a new tracker expecting outputs from the given source nodes.
203    ///
204    /// # Arguments
205    ///
206    /// * `expected_sources` - Names of upstream nodes that must complete
207    ///   before this deferred node can execute.
208    ///
209    /// # Example
210    ///
211    /// ```rust
212    /// use adk_graph::deferred::FanInTracker;
213    ///
214    /// let tracker = FanInTracker::new(vec!["branch_1", "branch_2", "branch_3"]);
215    /// assert_eq!(tracker.expected_count(), 3);
216    /// assert!(!tracker.is_ready());
217    /// ```
218    pub fn new(expected_sources: Vec<&str>) -> Self {
219        Self {
220            expected: expected_sources.iter().map(|s| (*s).to_string()).collect(),
221            received: HashMap::new(),
222            insertion_order: Vec::new(),
223        }
224    }
225
226    /// Returns `true` when all expected sources have reported their output.
227    ///
228    /// # Example
229    ///
230    /// ```rust
231    /// use adk_graph::deferred::FanInTracker;
232    /// use serde_json::json;
233    ///
234    /// let mut tracker = FanInTracker::new(vec!["a"]);
235    /// assert!(!tracker.is_ready());
236    ///
237    /// tracker.record("a", json!(42));
238    /// assert!(tracker.is_ready());
239    /// ```
240    pub fn is_ready(&self) -> bool {
241        self.expected.iter().all(|s| self.received.contains_key(s))
242    }
243
244    /// Record the output from a source node.
245    ///
246    /// If the source has already been recorded, the previous value is
247    /// overwritten (last-write-wins). Recording a source that is not in
248    /// the expected set is a no-op for readiness but the value is still stored.
249    ///
250    /// # Arguments
251    ///
252    /// * `source_node` - The name of the upstream node that produced the output.
253    /// * `output` - The output value from the source node.
254    ///
255    /// # Example
256    ///
257    /// ```rust
258    /// use adk_graph::deferred::FanInTracker;
259    /// use serde_json::json;
260    ///
261    /// let mut tracker = FanInTracker::new(vec!["worker_1", "worker_2"]);
262    /// tracker.record("worker_1", json!({"status": "done"}));
263    /// assert_eq!(tracker.received_count(), 1);
264    /// ```
265    pub fn record(&mut self, source_node: &str, output: Value) {
266        let key = source_node.to_string();
267        if !self.received.contains_key(&key) {
268            self.insertion_order.push(key.clone());
269        }
270        self.received.insert(key, output);
271    }
272
273    /// Merge all received outputs according to the given strategy.
274    ///
275    /// The merge operation combines all recorded outputs into a single
276    /// [`Value`] based on the [`MergeStrategy`]:
277    ///
278    /// - [`MergeStrategy::Collect`]: Returns a JSON array of all outputs in
279    ///   insertion order.
280    /// - [`MergeStrategy::MergeMap`]: Merges all JSON object outputs into a
281    ///   single object (last-write-wins). Non-object outputs are skipped.
282    /// - [`MergeStrategy::First`]: Returns the first recorded output.
283    /// - [`MergeStrategy::Custom`]: Invokes the custom function with all outputs.
284    ///
285    /// # Arguments
286    ///
287    /// * `strategy` - The merge strategy to apply.
288    ///
289    /// # Example
290    ///
291    /// ```rust
292    /// use adk_graph::deferred::{FanInTracker, MergeStrategy};
293    /// use serde_json::json;
294    ///
295    /// let mut tracker = FanInTracker::new(vec!["a", "b"]);
296    /// tracker.record("a", json!({"x": 1}));
297    /// tracker.record("b", json!({"y": 2}));
298    ///
299    /// // Collect strategy
300    /// let result = tracker.merge(&MergeStrategy::Collect);
301    /// assert_eq!(result, json!([{"x": 1}, {"y": 2}]));
302    ///
303    /// // MergeMap strategy
304    /// let result = tracker.merge(&MergeStrategy::MergeMap);
305    /// assert_eq!(result, json!({"x": 1, "y": 2}));
306    /// ```
307    pub fn merge(&self, strategy: &MergeStrategy) -> Value {
308        match strategy {
309            MergeStrategy::Collect => {
310                let outputs: Vec<Value> = self
311                    .insertion_order
312                    .iter()
313                    .filter_map(|key| self.received.get(key).cloned())
314                    .collect();
315                Value::Array(outputs)
316            }
317            MergeStrategy::MergeMap => {
318                let mut merged = serde_json::Map::new();
319                for key in &self.insertion_order {
320                    if let Some(Value::Object(map)) = self.received.get(key) {
321                        for (k, v) in map {
322                            merged.insert(k.clone(), v.clone());
323                        }
324                    }
325                }
326                Value::Object(merged)
327            }
328            MergeStrategy::First => self
329                .insertion_order
330                .first()
331                .and_then(|key| self.received.get(key).cloned())
332                .unwrap_or(Value::Null),
333            MergeStrategy::Custom(f) => {
334                let outputs: Vec<Value> = self
335                    .insertion_order
336                    .iter()
337                    .filter_map(|key| self.received.get(key).cloned())
338                    .collect();
339                f(outputs)
340            }
341        }
342    }
343
344    /// Returns the number of outputs received so far.
345    pub fn received_count(&self) -> usize {
346        self.received.len()
347    }
348
349    /// Returns the number of expected source nodes.
350    pub fn expected_count(&self) -> usize {
351        self.expected.len()
352    }
353
354    /// Returns the names of sources that have not yet reported.
355    pub fn pending_sources(&self) -> Vec<&str> {
356        self.expected
357            .iter()
358            .filter(|s| !self.received.contains_key(*s))
359            .map(|s| s.as_str())
360            .collect()
361    }
362
363    /// Returns the names of sources that have reported.
364    pub fn completed_sources(&self) -> Vec<&str> {
365        self.insertion_order.iter().map(|s| s.as_str()).collect()
366    }
367}
368
369/// Checkpoint metadata key holding the arrivals of fan-in nodes still waiting.
370pub(crate) const FAN_IN_METADATA_KEY: &str = "adk.graph.fanIn";
371
372/// Layout version of [`FAN_IN_METADATA_KEY`].
373///
374/// A checkpoint without the key predates it and restores with no arrivals.
375const FAN_IN_SNAPSHOT_VERSION: u32 = 1;
376
377/// The fan-in arrivals a checkpoint carries, so a resumed run still releases a
378/// join whose earlier predecessors finished before the pause or crash.
379#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
380#[serde(rename_all = "camelCase")]
381pub(crate) struct FanInSnapshot {
382    version: u32,
383    nodes: BTreeMap<String, PendingFanIn>,
384}
385
386/// One deferred node's recorded arrivals.
387#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389struct PendingFanIn {
390    /// Arrivals in recording order, which is the merge order.
391    arrivals: Vec<FanInArrival>,
392    /// When the node started waiting, so `fan_in_timeout` spans the pause.
393    waiting_since: chrono::DateTime<chrono::Utc>,
394}
395
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397#[serde(rename_all = "camelCase")]
398struct FanInArrival {
399    source: String,
400    output: Value,
401}
402
403impl FanInSnapshot {
404    /// Captures every waiting tracker, or `None` when no join is waiting.
405    pub(crate) fn capture(
406        trackers: &HashMap<String, FanInTracker>,
407        started: &HashMap<String, Instant>,
408    ) -> Option<Self> {
409        if trackers.is_empty() {
410            return None;
411        }
412        let now = chrono::Utc::now();
413        let nodes = trackers
414            .iter()
415            .map(|(node, tracker)| {
416                let waited = started
417                    .get(node)
418                    .and_then(|start| chrono::Duration::from_std(start.elapsed()).ok())
419                    .unwrap_or_default();
420                let arrivals = tracker
421                    .insertion_order
422                    .iter()
423                    .filter_map(|source| {
424                        tracker.received.get(source).map(|output| FanInArrival {
425                            source: source.clone(),
426                            output: output.clone(),
427                        })
428                    })
429                    .collect();
430                (node.clone(), PendingFanIn { arrivals, waiting_since: now - waited })
431            })
432            .collect();
433        Some(Self { version: FAN_IN_SNAPSHOT_VERSION, nodes })
434    }
435
436    /// Reads the snapshot from checkpoint metadata; `None` when the key is absent.
437    ///
438    /// # Errors
439    ///
440    /// Returns [`GraphError::CheckpointError`] when the value is malformed or was
441    /// written in a newer layout than this release reads.
442    pub(crate) fn from_metadata(metadata: &HashMap<String, Value>) -> Result<Option<Self>> {
443        let Some(value) = metadata.get(FAN_IN_METADATA_KEY) else { return Ok(None) };
444        let snapshot: Self = serde_json::from_value(value.clone()).map_err(|e| {
445            GraphError::CheckpointError(format!(
446                "checkpoint metadata '{FAN_IN_METADATA_KEY}' is malformed: {e}"
447            ))
448        })?;
449        if snapshot.version != FAN_IN_SNAPSHOT_VERSION {
450            return Err(GraphError::CheckpointError(format!(
451                "checkpoint metadata '{FAN_IN_METADATA_KEY}' has layout version {}, but this \
452                 adk-graph release reads version {FAN_IN_SNAPSHOT_VERSION}; resume the thread \
453                 with the release that wrote it",
454                snapshot.version
455            )));
456        }
457        Ok(Some(snapshot))
458    }
459
460    /// Rebuilds the trackers and their wait start times.
461    ///
462    /// `upstream` names each node's predecessors from the graph being resumed, so
463    /// the expected set is never read from the checkpoint.
464    pub(crate) fn restore(
465        self,
466        upstream: impl Fn(&str) -> Vec<String>,
467    ) -> (HashMap<String, FanInTracker>, HashMap<String, Instant>) {
468        let now = chrono::Utc::now();
469        let mut trackers = HashMap::new();
470        let mut started = HashMap::new();
471        for (node, pending) in self.nodes {
472            let sources = upstream(&node);
473            let mut tracker = FanInTracker::new(sources.iter().map(String::as_str).collect());
474            for arrival in pending.arrivals {
475                tracker.record(&arrival.source, arrival.output);
476            }
477            let waited = (now - pending.waiting_since).to_std().unwrap_or_default();
478            started.insert(
479                node.clone(),
480                Instant::now().checked_sub(waited).unwrap_or_else(Instant::now),
481            );
482            trackers.insert(node, tracker);
483        }
484        (trackers, started)
485    }
486
487    /// Serializes the snapshot for checkpoint metadata.
488    pub(crate) fn to_value(&self) -> Value {
489        serde_json::to_value(self).unwrap_or(Value::Null)
490    }
491}
492
493impl fmt::Debug for FanInTracker {
494    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495        f.debug_struct("FanInTracker")
496            .field("expected", &self.expected)
497            .field("received_keys", &self.insertion_order)
498            .field("is_ready", &self.is_ready())
499            .finish()
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506    use serde_json::json;
507
508    #[test]
509    fn test_tracker_new_empty_not_ready() {
510        let tracker = FanInTracker::new(vec!["a", "b", "c"]);
511        assert!(!tracker.is_ready());
512        assert_eq!(tracker.expected_count(), 3);
513        assert_eq!(tracker.received_count(), 0);
514    }
515
516    #[test]
517    fn test_tracker_ready_when_all_received() {
518        let mut tracker = FanInTracker::new(vec!["a", "b"]);
519        tracker.record("a", json!(1));
520        assert!(!tracker.is_ready());
521        tracker.record("b", json!(2));
522        assert!(tracker.is_ready());
523    }
524
525    #[test]
526    fn test_merge_collect() {
527        let mut tracker = FanInTracker::new(vec!["x", "y", "z"]);
528        tracker.record("x", json!("first"));
529        tracker.record("y", json!("second"));
530        tracker.record("z", json!("third"));
531
532        let result = tracker.merge(&MergeStrategy::Collect);
533        assert_eq!(result, json!(["first", "second", "third"]));
534    }
535
536    #[test]
537    fn test_merge_map_combines_objects() {
538        let mut tracker = FanInTracker::new(vec!["a", "b"]);
539        tracker.record("a", json!({"key1": "val1", "shared": "from_a"}));
540        tracker.record("b", json!({"key2": "val2", "shared": "from_b"}));
541
542        let result = tracker.merge(&MergeStrategy::MergeMap);
543        assert_eq!(result, json!({"key1": "val1", "key2": "val2", "shared": "from_b"}));
544    }
545
546    #[test]
547    fn test_merge_map_skips_non_objects() {
548        let mut tracker = FanInTracker::new(vec!["a", "b"]);
549        tracker.record("a", json!(42)); // Not an object, skipped
550        tracker.record("b", json!({"key": "value"}));
551
552        let result = tracker.merge(&MergeStrategy::MergeMap);
553        assert_eq!(result, json!({"key": "value"}));
554    }
555
556    #[test]
557    fn test_merge_first() {
558        let mut tracker = FanInTracker::new(vec!["a", "b", "c"]);
559        tracker.record("b", json!("first_to_arrive"));
560        tracker.record("a", json!("second_to_arrive"));
561        tracker.record("c", json!("third_to_arrive"));
562
563        let result = tracker.merge(&MergeStrategy::First);
564        assert_eq!(result, json!("first_to_arrive"));
565    }
566
567    #[test]
568    fn test_merge_first_empty() {
569        let tracker = FanInTracker::new(vec!["a"]);
570        let result = tracker.merge(&MergeStrategy::First);
571        assert_eq!(result, Value::Null);
572    }
573
574    #[test]
575    fn test_merge_custom() {
576        let mut tracker = FanInTracker::new(vec!["a", "b"]);
577        tracker.record("a", json!(10));
578        tracker.record("b", json!(20));
579
580        let strategy = MergeStrategy::Custom(Arc::new(|outputs| {
581            let sum: i64 = outputs.iter().filter_map(|v| v.as_i64()).sum();
582            json!(sum)
583        }));
584
585        let result = tracker.merge(&strategy);
586        assert_eq!(result, json!(30));
587    }
588
589    #[test]
590    fn test_record_overwrites_previous() {
591        let mut tracker = FanInTracker::new(vec!["a"]);
592        tracker.record("a", json!("first"));
593        tracker.record("a", json!("second"));
594
595        assert!(tracker.is_ready());
596        assert_eq!(tracker.received_count(), 1);
597
598        let result = tracker.merge(&MergeStrategy::First);
599        assert_eq!(result, json!("second"));
600    }
601
602    #[test]
603    fn test_pending_and_completed_sources() {
604        let mut tracker = FanInTracker::new(vec!["a", "b", "c"]);
605        tracker.record("b", json!(1));
606
607        let mut pending = tracker.pending_sources();
608        pending.sort();
609        assert_eq!(pending, vec!["a", "c"]);
610        assert_eq!(tracker.completed_sources(), vec!["b"]);
611    }
612
613    #[test]
614    fn test_default_config() {
615        let config = DeferredNodeConfig::default();
616        assert!(matches!(config.merge_strategy, MergeStrategy::Collect));
617        assert!(config.fan_in_timeout.is_none());
618    }
619
620    #[test]
621    fn fan_in_snapshot_round_trips_arrivals_in_order() {
622        let mut tracker = FanInTracker::new(vec!["a", "b", "c"]);
623        tracker.record("b", json!("from b"));
624        tracker.record("a", json!({"x": 1}));
625        let trackers = HashMap::from([("join".to_string(), tracker)]);
626        let started = HashMap::from([("join".to_string(), Instant::now())]);
627
628        let snapshot = FanInSnapshot::capture(&trackers, &started).expect("a join is waiting");
629        let metadata = HashMap::from([(FAN_IN_METADATA_KEY.to_string(), snapshot.to_value())]);
630        let restored = FanInSnapshot::from_metadata(&metadata).unwrap().expect("key present");
631        let (trackers, started) =
632            restored.restore(|_| vec!["a".to_string(), "b".to_string(), "c".to_string()]);
633
634        let tracker = &trackers["join"];
635        assert_eq!(tracker.completed_sources(), vec!["b", "a"]);
636        assert_eq!(tracker.merge(&MergeStrategy::Collect), json!(["from b", {"x": 1}]));
637        assert!(!tracker.is_ready());
638        assert!(started.contains_key("join"));
639    }
640
641    #[test]
642    fn a_checkpoint_without_fan_in_metadata_restores_nothing() {
643        assert_eq!(FanInSnapshot::from_metadata(&HashMap::new()).unwrap(), None);
644        assert_eq!(FanInSnapshot::capture(&HashMap::new(), &HashMap::new()), None);
645    }
646
647    #[test]
648    fn a_newer_fan_in_layout_is_rejected() {
649        let metadata =
650            HashMap::from([(FAN_IN_METADATA_KEY.to_string(), json!({"version": 99, "nodes": {}}))]);
651        assert!(matches!(
652            FanInSnapshot::from_metadata(&metadata),
653            Err(GraphError::CheckpointError(_))
654        ));
655    }
656
657    #[test]
658    fn test_merge_strategy_debug() {
659        assert_eq!(format!("{:?}", MergeStrategy::Collect), "Collect");
660        assert_eq!(format!("{:?}", MergeStrategy::MergeMap), "MergeMap");
661        assert_eq!(format!("{:?}", MergeStrategy::First), "First");
662        let custom = MergeStrategy::Custom(Arc::new(Value::Array));
663        assert_eq!(format!("{:?}", custom), "Custom(<fn>)");
664    }
665}