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