gemini_adk_fluent_rs/compose/
state.rs

1//! S — State transforms.
2//!
3//! Compose state transformations sequentially with `>>`.
4
5use std::sync::Arc;
6
7/// A state transformation step.
8#[derive(Clone)]
9pub struct StateTransform {
10    name: &'static str,
11    transform: Arc<dyn Fn(&mut serde_json::Value) + Send + Sync>,
12}
13
14impl StateTransform {
15    fn new(name: &'static str, f: impl Fn(&mut serde_json::Value) + Send + Sync + 'static) -> Self {
16        Self {
17            name,
18            transform: Arc::new(f),
19        }
20    }
21
22    /// Apply this transform to a state value.
23    pub fn apply(&self, state: &mut serde_json::Value) {
24        (self.transform)(state);
25    }
26
27    /// Name of this transform.
28    pub fn name(&self) -> &str {
29        self.name
30    }
31}
32
33impl std::fmt::Debug for StateTransform {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.debug_struct("StateTransform")
36            .field("name", &self.name)
37            .finish()
38    }
39}
40
41/// Compose two state transforms sequentially with `>>`.
42impl std::ops::Shr for StateTransform {
43    type Output = StateComposite;
44
45    fn shr(self, rhs: StateTransform) -> Self::Output {
46        StateComposite {
47            steps: vec![self, rhs],
48        }
49    }
50}
51
52/// A chain of state transforms applied sequentially (`S::a() >> S::b()`).
53#[derive(Clone)]
54#[non_exhaustive]
55pub struct StateComposite {
56    /// The ordered list of transforms applied sequentially.
57    pub steps: Vec<StateTransform>,
58}
59
60impl std::fmt::Debug for StateComposite {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_list()
63            .entries(self.steps.iter().map(StateTransform::name))
64            .finish()
65    }
66}
67
68impl From<StateTransform> for StateComposite {
69    fn from(step: StateTransform) -> Self {
70        Self { steps: vec![step] }
71    }
72}
73
74impl StateComposite {
75    /// Apply all transforms in order.
76    pub fn apply(&self, state: &mut serde_json::Value) {
77        for step in &self.steps {
78            step.apply(state);
79        }
80    }
81}
82
83/// Extend the chain with `>>`.
84impl std::ops::Shr<StateTransform> for StateComposite {
85    type Output = StateComposite;
86
87    fn shr(mut self, rhs: StateTransform) -> Self::Output {
88        self.steps.push(rhs);
89        self
90    }
91}
92
93/// The `S` namespace — static factory methods for state transforms.
94pub struct S;
95
96impl S {
97    /// Keep only the specified keys.
98    pub fn pick(keys: &[&str]) -> StateTransform {
99        let keys: Vec<String> = keys.iter().map(std::string::ToString::to_string).collect();
100        StateTransform::new("pick", move |state| {
101            if let Some(obj) = state.as_object_mut() {
102                obj.retain(|k, _| keys.contains(k));
103            }
104        })
105    }
106
107    /// Rename keys according to the mappings.
108    pub fn rename(mappings: &[(&str, &str)]) -> StateTransform {
109        let mappings: Vec<(String, String)> = mappings
110            .iter()
111            .map(|(a, b)| (a.to_string(), b.to_string()))
112            .collect();
113        StateTransform::new("rename", move |state| {
114            if let Some(obj) = state.as_object_mut() {
115                for (from, to) in &mappings {
116                    if let Some(val) = obj.remove(from) {
117                        obj.insert(to.clone(), val);
118                    }
119                }
120            }
121        })
122    }
123
124    /// Merge the specified keys into a single key as an object.
125    pub fn merge(keys: &[&str], into: &str) -> StateTransform {
126        let keys: Vec<String> = keys.iter().map(std::string::ToString::to_string).collect();
127        let into = into.to_string();
128        StateTransform::new("merge", move |state| {
129            if let Some(obj) = state.as_object_mut() {
130                let mut merged = serde_json::Map::new();
131                for key in &keys {
132                    if let Some(val) = obj.remove(key) {
133                        merged.insert(key.clone(), val);
134                    }
135                }
136                obj.insert(into.clone(), serde_json::Value::Object(merged));
137            }
138        })
139    }
140
141    /// Set default values for missing keys.
142    pub fn defaults(defaults: serde_json::Value) -> StateTransform {
143        StateTransform::new("defaults", move |state| {
144            if let (Some(obj), Some(defaults_obj)) = (state.as_object_mut(), defaults.as_object()) {
145                for (k, v) in defaults_obj {
146                    obj.entry(k.clone()).or_insert_with(|| v.clone());
147                }
148            }
149        })
150    }
151
152    /// Apply a custom transformation function.
153    pub fn map(f: impl Fn(&mut serde_json::Value) + Send + Sync + 'static) -> StateTransform {
154        StateTransform::new("map", f)
155    }
156
157    /// Flatten a nested object key into the top level.
158    pub fn flatten(key: &str) -> StateTransform {
159        let key = key.to_string();
160        StateTransform::new("flatten", move |state| {
161            if let Some(obj) = state.as_object_mut()
162                && let Some(serde_json::Value::Object(nested)) = obj.remove(&key)
163            {
164                for (k, v) in nested {
165                    obj.insert(k, v);
166                }
167            }
168        })
169    }
170
171    /// Set a key to a fixed value.
172    pub fn set(key: &str, value: serde_json::Value) -> StateTransform {
173        let key = key.to_string();
174        StateTransform::new("set", move |state| {
175            if let Some(obj) = state.as_object_mut() {
176                obj.insert(key.clone(), value.clone());
177            }
178        })
179    }
180
181    // ── State predicates ───────────────────────────────────────────────────
182    // Ergonomic helpers for transition guards and `.when()` predicates.
183
184    /// Returns `true` if the given key exists with any non-null value.
185    ///
186    /// Replaces the common pattern `|s| s.get::<String>("key").is_some()`.
187    ///
188    /// ```
189    /// # use gemini_adk_fluent_rs::prelude::*;
190    /// Live::builder().phase("greet").transition("next_phase", S::is_set("caller_name")).done();
191    /// ```
192    pub fn is_set(
193        key: &str,
194    ) -> impl Fn(&gemini_adk_rs::State) -> bool + use<> + Send + Sync + 'static {
195        let key = key.to_string();
196        move |s: &gemini_adk_rs::State| s.contains(&key)
197    }
198
199    /// Returns `true` if the given key holds a truthy boolean.
200    ///
201    /// ```
202    /// # use gemini_adk_fluent_rs::prelude::*;
203    /// Live::builder().phase("disclose").transition("next_phase", S::is_true("disclosure_given")).done();
204    /// ```
205    pub fn is_true(
206        key: &str,
207    ) -> impl Fn(&gemini_adk_rs::State) -> bool + use<> + Send + Sync + 'static {
208        let key = key.to_string();
209        move |s: &gemini_adk_rs::State| s.get::<bool>(&key).unwrap_or(false)
210    }
211
212    /// Returns `true` if the given key equals the expected string value.
213    ///
214    /// ```
215    /// # use gemini_adk_fluent_rs::prelude::*;
216    /// Live::builder().phase("triage").transition("tech:greet", S::eq("issue_type", "technical")).done();
217    /// ```
218    pub fn eq(
219        key: &str,
220        expected: &str,
221    ) -> impl Fn(&gemini_adk_rs::State) -> bool + use<> + Send + Sync + 'static {
222        let key = key.to_string();
223        let expected = expected.to_string();
224        move |s: &gemini_adk_rs::State| {
225            s.get::<String>(&key)
226                .map(|v| v == expected)
227                .unwrap_or(false)
228        }
229    }
230
231    /// Returns `true` if the given key matches any of the provided string values.
232    ///
233    /// ```
234    /// # use gemini_adk_fluent_rs::prelude::*;
235    /// Live::builder()
236    ///     .phase("negotiate")
237    ///     .transition("arrange_payment", S::one_of("negotiation_intent", &["full_pay", "partial_pay"]))
238    ///     .done();
239    /// ```
240    pub fn one_of(
241        key: &str,
242        values: &[&str],
243    ) -> impl Fn(&gemini_adk_rs::State) -> bool + use<> + Send + Sync + 'static {
244        let key = key.to_string();
245        let values: Vec<String> = values
246            .iter()
247            .map(std::string::ToString::to_string)
248            .collect();
249        move |s: &gemini_adk_rs::State| s.get::<String>(&key).is_some_and(|v| values.contains(&v))
250    }
251
252    /// Transform a single key's value with a function.
253    pub fn transform(
254        key: &str,
255        f: impl Fn(serde_json::Value) -> serde_json::Value + Send + Sync + 'static,
256    ) -> StateTransform {
257        let key = key.to_string();
258        StateTransform::new("transform", move |state| {
259            if let Some(obj) = state.as_object_mut()
260                && let Some(val) = obj.remove(&key)
261            {
262                obj.insert(key.clone(), f(val));
263            }
264        })
265    }
266
267    /// Guard — assert a condition on state, panic with message if false.
268    pub fn guard(
269        predicate: impl Fn(&serde_json::Value) -> bool + Send + Sync + 'static,
270        msg: &str,
271    ) -> StateTransform {
272        let msg = msg.to_string();
273        StateTransform::new("guard", move |state| {
274            assert!(predicate(state), "{}", msg);
275        })
276    }
277
278    /// Compute derived values from state.
279    pub fn compute(
280        key: &str,
281        f: impl Fn(&serde_json::Value) -> serde_json::Value + Send + Sync + 'static,
282    ) -> StateTransform {
283        let key = key.to_string();
284        StateTransform::new("compute", move |state| {
285            let val = f(state);
286            if let Some(obj) = state.as_object_mut() {
287                obj.insert(key.clone(), val);
288            }
289        })
290    }
291
292    /// Accumulate values into a list under a target key.
293    pub fn accumulate(source_key: &str, into: &str) -> StateTransform {
294        let source = source_key.to_string();
295        let into = into.to_string();
296        StateTransform::new("accumulate", move |state| {
297            if let Some(obj) = state.as_object_mut()
298                && let Some(val) = obj.get(&source).cloned()
299            {
300                let arr = obj
301                    .entry(into.clone())
302                    .or_insert_with(|| serde_json::Value::Array(Vec::new()));
303                if let Some(arr) = arr.as_array_mut() {
304                    arr.push(val);
305                }
306            }
307        })
308    }
309
310    /// Increment a counter key by a step.
311    pub fn counter(key: &str, step: i64) -> StateTransform {
312        let key = key.to_string();
313        StateTransform::new("counter", move |state| {
314            if let Some(obj) = state.as_object_mut() {
315                let current = obj
316                    .get(&key)
317                    .and_then(serde_json::Value::as_i64)
318                    .unwrap_or(0);
319                obj.insert(key.clone(), serde_json::json!(current + step));
320            }
321        })
322    }
323
324    /// Require that specified keys exist.
325    pub fn require(keys: &[&str]) -> StateTransform {
326        let keys: Vec<String> = keys.iter().map(std::string::ToString::to_string).collect();
327        StateTransform::new("require", move |state| {
328            if let Some(obj) = state.as_object() {
329                for key in &keys {
330                    assert!(
331                        obj.contains_key(key),
332                        "Required key '{key}' missing from state"
333                    );
334                }
335            }
336        })
337    }
338
339    /// Identity transform — no-op passthrough.
340    pub fn identity() -> StateTransform {
341        StateTransform::new("identity", |_| {})
342    }
343
344    /// Conditional transform — applies inner transform only when predicate is true.
345    pub fn when(
346        predicate: impl Fn(&serde_json::Value) -> bool + Send + Sync + 'static,
347        inner: StateTransform,
348    ) -> StateTransform {
349        StateTransform::new("when", move |state| {
350            if predicate(state) {
351                inner.apply(state);
352            }
353        })
354    }
355
356    /// Drop the specified keys.
357    pub fn drop(keys: &[&str]) -> StateTransform {
358        let keys: Vec<String> = keys.iter().map(std::string::ToString::to_string).collect();
359        StateTransform::new("drop", move |state| {
360            if let Some(obj) = state.as_object_mut() {
361                for key in &keys {
362                    obj.remove(key);
363                }
364            }
365        })
366    }
367
368    /// Log a message during state transform (side-effect marker).
369    ///
370    /// Useful for debugging transform pipelines — prints the message to stderr
371    /// each time the transform is applied.
372    ///
373    /// ```
374    /// # use gemini_adk_fluent_rs::prelude::*;
375    /// let chain = S::pick(&["a"]) >> S::log("after pick") >> S::rename(&[("a", "x")]);
376    /// # let _ = chain;
377    /// ```
378    pub fn log(message: &str) -> StateTransform {
379        let message = message.to_string();
380        StateTransform::new("log", move |_state| {
381            eprintln!("[S::log] {message}");
382        })
383    }
384
385    /// Unflatten a dotted-key object into a nested structure (inverse of `flatten`).
386    ///
387    /// Takes all top-level keys that start with `key.` and nests them under `key` as an object.
388    /// For example, `{"addr.city": "NYC", "addr.zip": "10001"}` with `unflatten("addr")`
389    /// becomes `{"addr": {"city": "NYC", "zip": "10001"}}`.
390    ///
391    /// ```
392    /// # use gemini_adk_fluent_rs::prelude::*;
393    /// let t = S::unflatten("addr");
394    /// # let _ = t;
395    /// ```
396    pub fn unflatten(key: &str) -> StateTransform {
397        let key = key.to_string();
398        StateTransform::new("unflatten", move |state| {
399            if let Some(obj) = state.as_object_mut() {
400                let prefix = format!("{key}.");
401                let dotted: Vec<(String, serde_json::Value)> = obj
402                    .keys()
403                    .filter(|k| k.starts_with(&prefix))
404                    .cloned()
405                    .collect::<Vec<_>>()
406                    .into_iter()
407                    .filter_map(|k| obj.remove(&k).map(|v| (k, v)))
408                    .collect();
409
410                if !dotted.is_empty() {
411                    let nested = obj
412                        .entry(key.clone())
413                        .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
414                    if let Some(nested_obj) = nested.as_object_mut() {
415                        for (k, v) in dotted {
416                            let sub_key = k[prefix.len()..].to_string();
417                            nested_obj.insert(sub_key, v);
418                        }
419                    }
420                }
421            }
422        })
423    }
424
425    /// Zip multiple array keys into an array of tuples (arrays).
426    ///
427    /// Takes the arrays at each of `keys` and produces an array of arrays under `into`,
428    /// where element `i` contains `[keys[0][i], keys[1][i], ...]`.
429    /// Arrays are zipped to the length of the shortest.
430    ///
431    /// ```
432    /// # use gemini_adk_fluent_rs::prelude::*;
433    /// // {"names": ["a","b"], "scores": [1,2]} -> {"zipped": [["a",1], ["b",2]]}
434    /// let t = S::zip(&["names", "scores"], "zipped");
435    /// # let _ = t;
436    /// ```
437    pub fn zip(keys: &[&str], into: &str) -> StateTransform {
438        let keys: Vec<String> = keys.iter().map(std::string::ToString::to_string).collect();
439        let into = into.to_string();
440        StateTransform::new("zip", move |state| {
441            if let Some(obj) = state.as_object_mut() {
442                let arrays: Vec<&Vec<serde_json::Value>> = keys
443                    .iter()
444                    .filter_map(|k| obj.get(k).and_then(|v| v.as_array()))
445                    .collect();
446
447                if arrays.len() == keys.len() {
448                    let min_len = arrays.iter().map(|a| a.len()).min().unwrap_or(0);
449                    let mut zipped = Vec::with_capacity(min_len);
450                    for i in 0..min_len {
451                        let tuple: Vec<serde_json::Value> =
452                            arrays.iter().map(|a| a[i].clone()).collect();
453                        zipped.push(serde_json::Value::Array(tuple));
454                    }
455                    obj.insert(into.clone(), serde_json::Value::Array(zipped));
456                }
457            }
458        })
459    }
460
461    /// Group array elements by a field value.
462    ///
463    /// Takes the array at `source`, groups its elements by the string value of `key`,
464    /// and writes the resulting object (field value -> array of elements) to `into`.
465    ///
466    /// ```
467    /// # use gemini_adk_fluent_rs::prelude::*;
468    /// // {"items": [{"type":"a","v":1}, {"type":"b","v":2}, {"type":"a","v":3}]}
469    /// // -> {"grouped": {"a": [{"type":"a","v":1}, {"type":"a","v":3}], "b": [{"type":"b","v":2}]}}
470    /// let t = S::group_by("items", "type", "grouped");
471    /// # let _ = t;
472    /// ```
473    pub fn group_by(source: &str, key: &str, into: &str) -> StateTransform {
474        let source = source.to_string();
475        let key = key.to_string();
476        let into = into.to_string();
477        StateTransform::new("group_by", move |state| {
478            if let Some(obj) = state.as_object_mut()
479                && let Some(arr) = obj.get(&source).and_then(|v| v.as_array())
480            {
481                let mut groups: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
482                for item in arr {
483                    let group_key = item
484                        .get(&key)
485                        .and_then(|v| v.as_str())
486                        .unwrap_or("_unknown")
487                        .to_string();
488                    let group = groups
489                        .entry(group_key)
490                        .or_insert_with(|| serde_json::Value::Array(Vec::new()));
491                    if let Some(arr) = group.as_array_mut() {
492                        arr.push(item.clone());
493                    }
494                }
495                obj.insert(into.clone(), serde_json::Value::Object(groups));
496            }
497        })
498    }
499
500    /// Keep history of a key's values (append to list, cap at max).
501    ///
502    /// Each time this transform runs, the current value of `key` is appended to
503    /// `{key}_history`. The history array is capped at `max` entries (oldest dropped).
504    ///
505    /// ```
506    /// # use gemini_adk_fluent_rs::prelude::*;
507    /// let t = S::history("score", 5); // keeps last 5 score values in "score_history"
508    /// # let _ = t;
509    /// ```
510    pub fn history(key: &str, max: usize) -> StateTransform {
511        let key = key.to_string();
512        StateTransform::new("history", move |state| {
513            if let Some(obj) = state.as_object_mut() {
514                let history_key = format!("{key}_history");
515                if let Some(val) = obj.get(&key).cloned() {
516                    let arr = obj
517                        .entry(history_key)
518                        .or_insert_with(|| serde_json::Value::Array(Vec::new()));
519                    if let Some(arr) = arr.as_array_mut() {
520                        arr.push(val);
521                        while arr.len() > max {
522                            arr.remove(0);
523                        }
524                    }
525                }
526            }
527        })
528    }
529
530    /// Validate state against a JSON schema value.
531    ///
532    /// Panics with a descriptive message if any required key from the schema's
533    /// `required` array is missing, or if a key's type doesn't match the schema's
534    /// `properties.{key}.type` declaration.
535    ///
536    /// ```
537    /// # use gemini_adk_fluent_rs::prelude::*;
538    /// # use serde_json::json;
539    /// let t = S::validate(json!({
540    ///     "required": ["name", "age"],
541    ///     "properties": {
542    ///         "name": {"type": "string"},
543    ///         "age": {"type": "number"}
544    ///     }
545    /// }));
546    /// ```
547    pub fn validate(schema: serde_json::Value) -> StateTransform {
548        StateTransform::new("validate", move |state| {
549            if let Some(obj) = state.as_object() {
550                // Check required keys
551                if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
552                    for req in required {
553                        if let Some(key) = req.as_str() {
554                            assert!(
555                                obj.contains_key(key),
556                                "Validation failed: required key '{key}' missing from state"
557                            );
558                        }
559                    }
560                }
561                // Check property types
562                if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
563                    for (key, prop_schema) in properties {
564                        if let Some(val) = obj.get(key)
565                            && let Some(expected_type) =
566                                prop_schema.get("type").and_then(|v| v.as_str())
567                        {
568                            let actual_ok = match expected_type {
569                                "string" => val.is_string(),
570                                "number" | "integer" => val.is_number(),
571                                "boolean" => val.is_boolean(),
572                                "array" => val.is_array(),
573                                "object" => val.is_object(),
574                                "null" => val.is_null(),
575                                _ => true,
576                            };
577                            assert!(
578                                actual_ok,
579                                "Validation failed: key '{key}' expected type '{expected_type}', got {val:?}"
580                            );
581                        }
582                    }
583                }
584            }
585        })
586    }
587
588    /// Conditional branching of state transforms.
589    ///
590    /// Applies `if_true` when the predicate returns `true`, otherwise applies `if_false`.
591    ///
592    /// ```
593    /// # use gemini_adk_fluent_rs::prelude::*;
594    /// # use serde_json::json;
595    /// let t = S::branch(
596    ///     |s| s.get("premium").and_then(serde_json::Value::as_bool).unwrap_or(false),
597    ///     S::set("tier", json!("gold")),
598    ///     S::set("tier", json!("basic")),
599    /// );
600    /// ```
601    pub fn branch(
602        predicate: impl Fn(&serde_json::Value) -> bool + Send + Sync + 'static,
603        if_true: StateTransform,
604        if_false: StateTransform,
605    ) -> StateTransform {
606        StateTransform::new("branch", move |state| {
607            if predicate(state) {
608                if_true.apply(state);
609            } else {
610                if_false.apply(state);
611            }
612        })
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619    use serde_json::json;
620
621    #[test]
622    fn pick_keeps_only_specified_keys() {
623        let mut state = json!({"a": 1, "b": 2, "c": 3});
624        S::pick(&["a", "c"]).apply(&mut state);
625        assert_eq!(state, json!({"a": 1, "c": 3}));
626    }
627
628    #[test]
629    fn rename_renames_keys() {
630        let mut state = json!({"old_name": 42});
631        S::rename(&[("old_name", "new_name")]).apply(&mut state);
632        assert_eq!(state, json!({"new_name": 42}));
633    }
634
635    #[test]
636    fn merge_combines_keys() {
637        let mut state = json!({"x": 1, "y": 2, "z": 3});
638        S::merge(&["x", "y"], "combined").apply(&mut state);
639        assert_eq!(state, json!({"z": 3, "combined": {"x": 1, "y": 2}}));
640    }
641
642    #[test]
643    fn defaults_sets_missing() {
644        let mut state = json!({"existing": "yes"});
645        S::defaults(json!({"existing": "no", "missing": "added"})).apply(&mut state);
646        assert_eq!(state["existing"], "yes");
647        assert_eq!(state["missing"], "added");
648    }
649
650    #[test]
651    fn drop_removes_keys() {
652        let mut state = json!({"keep": 1, "remove": 2});
653        S::drop(&["remove"]).apply(&mut state);
654        assert_eq!(state, json!({"keep": 1}));
655    }
656
657    #[test]
658    fn map_custom_transform() {
659        let mut state = json!({"count": 5});
660        S::map(|s| {
661            if let Some(n) = s.get("count").and_then(serde_json::Value::as_i64) {
662                s["count"] = json!(n * 2);
663            }
664        })
665        .apply(&mut state);
666        assert_eq!(state["count"], 10);
667    }
668
669    #[test]
670    fn chain_with_shr() {
671        let chain = S::pick(&["a", "b"]) >> S::rename(&[("a", "x")]);
672        let mut state = json!({"a": 1, "b": 2, "c": 3});
673        chain.apply(&mut state);
674        assert_eq!(state, json!({"x": 1, "b": 2}));
675    }
676
677    #[test]
678    fn flatten_nested_object() {
679        let mut state = json!({"nested": {"x": 1, "y": 2}, "z": 3});
680        S::flatten("nested").apply(&mut state);
681        assert_eq!(state, json!({"x": 1, "y": 2, "z": 3}));
682    }
683
684    #[test]
685    fn flatten_missing_key_is_noop() {
686        let mut state = json!({"a": 1});
687        S::flatten("nonexistent").apply(&mut state);
688        assert_eq!(state, json!({"a": 1}));
689    }
690
691    #[test]
692    fn set_inserts_value() {
693        let mut state = json!({"a": 1});
694        S::set("b", json!(42)).apply(&mut state);
695        assert_eq!(state, json!({"a": 1, "b": 42}));
696    }
697
698    #[test]
699    fn set_overwrites_existing() {
700        let mut state = json!({"a": 1});
701        S::set("a", json!("replaced")).apply(&mut state);
702        assert_eq!(state, json!({"a": "replaced"}));
703    }
704
705    #[test]
706    fn chain_extends() {
707        let chain = S::pick(&["a"]) >> S::rename(&[("a", "b")]) >> S::defaults(json!({"c": 99}));
708        let mut state = json!({"a": 1, "x": 2});
709        chain.apply(&mut state);
710        assert_eq!(state, json!({"b": 1, "c": 99}));
711    }
712
713    #[test]
714    fn log_is_noop_on_state() {
715        let mut state = json!({"a": 1});
716        S::log("debug message").apply(&mut state);
717        assert_eq!(state, json!({"a": 1}));
718    }
719
720    #[test]
721    fn unflatten_groups_dotted_keys() {
722        let mut state = json!({"addr.city": "NYC", "addr.zip": "10001", "name": "Alice"});
723        S::unflatten("addr").apply(&mut state);
724        assert_eq!(
725            state,
726            json!({"name": "Alice", "addr": {"city": "NYC", "zip": "10001"}})
727        );
728    }
729
730    #[test]
731    fn unflatten_missing_prefix_is_noop() {
732        let mut state = json!({"a": 1});
733        S::unflatten("addr").apply(&mut state);
734        assert_eq!(state, json!({"a": 1}));
735    }
736
737    #[test]
738    fn zip_combines_arrays() {
739        let mut state = json!({"names": ["a", "b", "c"], "scores": [10, 20, 30]});
740        S::zip(&["names", "scores"], "zipped").apply(&mut state);
741        assert_eq!(state["zipped"], json!([["a", 10], ["b", 20], ["c", 30]]));
742    }
743
744    #[test]
745    fn zip_truncates_to_shortest() {
746        let mut state = json!({"a": [1, 2, 3], "b": [10, 20]});
747        S::zip(&["a", "b"], "z").apply(&mut state);
748        assert_eq!(state["z"], json!([[1, 10], [2, 20]]));
749    }
750
751    #[test]
752    fn group_by_groups_elements() {
753        let mut state = json!({
754            "items": [
755                {"type": "fruit", "name": "apple"},
756                {"type": "veg", "name": "carrot"},
757                {"type": "fruit", "name": "banana"}
758            ]
759        });
760        S::group_by("items", "type", "grouped").apply(&mut state);
761        let grouped = &state["grouped"];
762        assert_eq!(grouped["fruit"].as_array().unwrap().len(), 2);
763        assert_eq!(grouped["veg"].as_array().unwrap().len(), 1);
764    }
765
766    #[test]
767    fn history_tracks_values() {
768        let mut state = json!({"score": 10});
769        let t = S::history("score", 3);
770        t.apply(&mut state);
771        state["score"] = json!(20);
772        t.apply(&mut state);
773        state["score"] = json!(30);
774        t.apply(&mut state);
775        state["score"] = json!(40);
776        t.apply(&mut state);
777        // Should only keep last 3
778        assert_eq!(state["score_history"], json!([20, 30, 40]));
779    }
780
781    #[test]
782    fn validate_passes_valid_state() {
783        let mut state = json!({"name": "Alice", "age": 30});
784        S::validate(json!({
785            "required": ["name", "age"],
786            "properties": {
787                "name": {"type": "string"},
788                "age": {"type": "number"}
789            }
790        }))
791        .apply(&mut state);
792        // Should not panic
793    }
794
795    #[test]
796    #[should_panic(expected = "required key 'missing' missing from state")]
797    fn validate_fails_missing_required() {
798        let mut state = json!({"name": "Alice"});
799        S::validate(json!({"required": ["name", "missing"]})).apply(&mut state);
800    }
801
802    #[test]
803    #[should_panic(expected = "expected type 'string'")]
804    fn validate_fails_wrong_type() {
805        let mut state = json!({"name": 42});
806        S::validate(json!({
807            "properties": {"name": {"type": "string"}}
808        }))
809        .apply(&mut state);
810    }
811
812    #[test]
813    fn branch_takes_true_path() {
814        let mut state = json!({"premium": true});
815        S::branch(
816            |s| {
817                s.get("premium")
818                    .and_then(serde_json::Value::as_bool)
819                    .unwrap_or(false)
820            },
821            S::set("tier", json!("gold")),
822            S::set("tier", json!("basic")),
823        )
824        .apply(&mut state);
825        assert_eq!(state["tier"], "gold");
826    }
827
828    #[test]
829    fn branch_takes_false_path() {
830        let mut state = json!({"premium": false});
831        S::branch(
832            |s| {
833                s.get("premium")
834                    .and_then(serde_json::Value::as_bool)
835                    .unwrap_or(false)
836            },
837            S::set("tier", json!("gold")),
838            S::set("tier", json!("basic")),
839        )
840        .apply(&mut state);
841        assert_eq!(state["tier"], "basic");
842    }
843}