Skip to main content

cognis_graph/
reducer.rs

1//! Channel reducers — strategies for merging per-step updates into state.
2//!
3//! `#[derive(GraphState)]` desugars `#[reducer(append|add|last|merge|...)]`
4//! into inline merge logic. This trait + impls are exposed for:
5//! - Programmatic reducer composition (advanced graph builders).
6//! - Documentation of the available strategies.
7
8use std::ops::AddAssign;
9
10/// A strategy for merging an `update: T` into a `current: &mut T`.
11pub trait Reducer<T>: Send + Sync {
12    /// Merge `update` into `current` in place.
13    fn reduce(&self, current: &mut T, update: T);
14}
15
16/// Append: `Vec<T>::extend(update)`. Matches `#[reducer(append)]`.
17pub struct Append;
18impl<T: Send + Sync> Reducer<Vec<T>> for Append {
19    fn reduce(&self, current: &mut Vec<T>, update: Vec<T>) {
20        current.extend(update);
21    }
22}
23
24/// Add: `current += update` (numeric `AddAssign`). Matches `#[reducer(add)]`.
25pub struct Add;
26impl<T> Reducer<T> for Add
27where
28    T: AddAssign + Send + Sync,
29{
30    fn reduce(&self, current: &mut T, update: T) {
31        *current += update;
32    }
33}
34
35/// LastValue: `*current = update` if update is not the type's "empty"
36/// — for `Option<U>` only assigns when `Some`. The `#[reducer(last)]`
37/// macro variant wraps the field type in `Option<T>` in the Update
38/// struct, so the user-facing semantics are "assign only if Some".
39pub struct LastValue;
40impl<T: Send + Sync> Reducer<Option<T>> for LastValue {
41    fn reduce(&self, current: &mut Option<T>, update: Option<T>) {
42        if let Some(v) = update {
43            *current = Some(v);
44        }
45    }
46}
47
48/// Merge: deep-merge for `serde_json::Value`. Matches `#[reducer(merge)]`.
49pub struct Merge;
50impl Reducer<serde_json::Value> for Merge {
51    fn reduce(&self, current: &mut serde_json::Value, update: serde_json::Value) {
52        crate::state::__merge_json(current, update);
53    }
54}
55
56/// Custom user-supplied closure. Matches `#[reducer(custom = "path::fn")]`
57/// in spirit (the macro inlines the call rather than going through this
58/// type, but `Custom` is provided for programmatic composition).
59pub struct Custom<F>(pub F);
60impl<T, F> Reducer<T> for Custom<F>
61where
62    T: Send + Sync,
63    F: Fn(&mut T, T) + Send + Sync,
64{
65    fn reduce(&self, current: &mut T, update: T) {
66        (self.0)(current, update);
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn append_extends_vec() {
76        let mut v = vec![1, 2];
77        Append.reduce(&mut v, vec![3, 4]);
78        assert_eq!(v, vec![1, 2, 3, 4]);
79    }
80
81    #[test]
82    fn add_increments() {
83        let mut n = 5u32;
84        Add.reduce(&mut n, 3);
85        assert_eq!(n, 8);
86    }
87
88    #[test]
89    fn last_value_some_overwrites() {
90        let mut o: Option<&str> = Some("old");
91        LastValue.reduce(&mut o, Some("new"));
92        assert_eq!(o, Some("new"));
93    }
94
95    #[test]
96    fn last_value_none_keeps() {
97        let mut o: Option<&str> = Some("keep");
98        LastValue.reduce(&mut o, None);
99        assert_eq!(o, Some("keep"));
100    }
101
102    #[test]
103    fn merge_deep_merges_json() {
104        let mut t = serde_json::json!({"a": 1});
105        Merge.reduce(&mut t, serde_json::json!({"b": 2}));
106        assert_eq!(t, serde_json::json!({"a": 1, "b": 2}));
107    }
108
109    #[test]
110    fn custom_closure_applies() {
111        let mut s = String::from("hello");
112        Custom(|cur: &mut String, upd: String| cur.push_str(&upd)).reduce(&mut s, " world".into());
113        assert_eq!(s, "hello world");
114    }
115}