Skip to main content

hypen_engine/lifecycle/
module.rs

1use serde::{Deserialize, Serialize};
2use std::sync::Arc;
3
4/// Module metadata and manifest
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Module {
7    /// Module name
8    pub name: String,
9
10    /// List of action names this module handles
11    pub actions: Vec<String>,
12
13    /// List of state keys this module exposes
14    pub state_keys: Vec<String>,
15
16    /// Whether state should be persisted
17    pub persist: bool,
18
19    /// Optional version number for migration
20    pub version: Option<u32>,
21}
22
23impl Module {
24    pub fn new(name: impl Into<String>) -> Self {
25        Self {
26            name: name.into(),
27            actions: Vec::new(),
28            state_keys: Vec::new(),
29            persist: false,
30            version: None,
31        }
32    }
33
34    pub fn with_actions(mut self, actions: Vec<String>) -> Self {
35        self.actions = actions;
36        self
37    }
38
39    pub fn with_state_keys(mut self, state_keys: Vec<String>) -> Self {
40        self.state_keys = state_keys;
41        self
42    }
43
44    pub fn with_persist(mut self, persist: bool) -> Self {
45        self.persist = persist;
46        self
47    }
48
49    pub fn with_version(mut self, version: u32) -> Self {
50        self.version = Some(version);
51        self
52    }
53}
54
55/// Lifecycle callbacks for a module
56pub trait ModuleLifecycle {
57    /// Called when module is first created
58    fn on_created(&mut self);
59
60    /// Called when module is destroyed
61    fn on_destroyed(&mut self);
62
63    /// Called when state is updated from the host
64    fn on_state_changed(&mut self, state: serde_json::Value);
65}
66
67/// Callback type for lifecycle events
68pub type LifecycleCallback = Box<dyn Fn() + Send + Sync>;
69
70/// Module instance that holds the current state
71///
72/// State is wrapped in Arc for O(1) clone performance. This is critical
73/// because state is cloned on every render cycle. With Arc, cloning is
74/// just a reference count increment instead of a deep copy.
75pub struct ModuleInstance {
76    /// Module metadata
77    pub module: Module,
78
79    /// Current state snapshot (Arc-wrapped for O(1) clone)
80    state: Arc<serde_json::Value>,
81
82    /// Whether the module is currently mounted
83    pub mounted: bool,
84
85    /// Callback for when module is created
86    on_created: Option<LifecycleCallback>,
87
88    /// Callback for when module is destroyed
89    on_destroyed: Option<LifecycleCallback>,
90}
91
92impl ModuleInstance {
93    pub fn new(module: Module, initial_state: serde_json::Value) -> Self {
94        Self {
95            module,
96            state: Arc::new(initial_state),
97            mounted: false,
98            on_created: None,
99            on_destroyed: None,
100        }
101    }
102
103    /// Set the on_created lifecycle callback
104    pub fn set_on_created<F>(&mut self, callback: F)
105    where
106        F: Fn() + Send + Sync + 'static,
107    {
108        self.on_created = Some(Box::new(callback));
109    }
110
111    /// Set the on_destroyed lifecycle callback
112    pub fn set_on_destroyed<F>(&mut self, callback: F)
113    where
114        F: Fn() + Send + Sync + 'static,
115    {
116        self.on_destroyed = Some(Box::new(callback));
117    }
118
119    /// Mount the module (call on_created)
120    pub fn mount(&mut self) {
121        if !self.mounted {
122            self.mounted = true;
123            // Call on_created lifecycle hook if registered
124            if let Some(ref callback) = self.on_created {
125                callback();
126            }
127        }
128    }
129
130    /// Unmount the module (call on_destroyed)
131    pub fn unmount(&mut self) {
132        if self.mounted {
133            // Call on_destroyed lifecycle hook if registered
134            if let Some(ref callback) = self.on_destroyed {
135                callback();
136            }
137            self.mounted = false;
138        }
139    }
140
141    /// Update state from a patch
142    ///
143    /// Uses Arc::make_mut for copy-on-write semantics: if this is the only
144    /// reference, mutates in place; otherwise clones first.
145    pub fn update_state(&mut self, patch: serde_json::Value) {
146        // Arc::make_mut provides copy-on-write: clones only if shared
147        let state = Arc::make_mut(&mut self.state);
148        merge_json(state, patch);
149    }
150
151    /// Update state from sparse path-value pairs
152    /// This is more efficient than sending the full state when only a few paths changed
153    ///
154    /// Uses Arc::make_mut for copy-on-write semantics.
155    pub fn update_state_sparse(&mut self, paths: &[String], values: &serde_json::Value) {
156        // values is expected to be an object mapping paths to their new values
157        if let serde_json::Value::Object(map) = values {
158            // Only get mutable access if we have paths to update
159            if paths.iter().any(|p| map.contains_key(p)) {
160                let state = Arc::make_mut(&mut self.state);
161                for path in paths {
162                    if let Some(new_value) = map.get(path) {
163                        set_value_at_path(state, path, new_value.clone());
164                    }
165                }
166            }
167        }
168    }
169
170    /// Get a reference to the current state
171    ///
172    /// For read-only access without cloning. Prefer this over `get_state_shared()`
173    /// when you don't need to store the state beyond the current scope.
174    pub fn get_state(&self) -> &serde_json::Value {
175        &self.state
176    }
177
178    /// Get a shared reference to the current state (O(1) clone)
179    ///
180    /// Use this for the render hot path where state needs to be passed around
181    /// without deep copying. Cloning the Arc is just a reference count increment.
182    pub fn get_state_shared(&self) -> Arc<serde_json::Value> {
183        Arc::clone(&self.state)
184    }
185}
186
187/// Deep merge two JSON values
188fn merge_json(target: &mut serde_json::Value, source: serde_json::Value) {
189    use serde_json::Value;
190
191    match (target, source) {
192        (Value::Object(target_map), Value::Object(source_map)) => {
193            for (key, value) in source_map {
194                if let Some(target_value) = target_map.get_mut(&key) {
195                    merge_json(target_value, value);
196                } else {
197                    target_map.insert(key, value);
198                }
199            }
200        }
201        (target, source) => {
202            *target = source;
203        }
204    }
205}
206
207/// Set a value at a dot-separated path (e.g., "user.profile.name")
208/// Creates intermediate objects if they don't exist
209fn set_value_at_path(target: &mut serde_json::Value, path: &str, value: serde_json::Value) {
210    use serde_json::Value;
211
212    let parts: Vec<&str> = path.split('.').collect();
213    if parts.is_empty() {
214        return;
215    }
216
217    let mut current = target;
218
219    // Navigate to the parent of the final key
220    for part in &parts[..parts.len() - 1] {
221        // Try to parse as array index first
222        if let Ok(index) = part.parse::<usize>() {
223            if let Value::Array(arr) = current {
224                // Extend array if needed
225                while arr.len() <= index {
226                    arr.push(Value::Null);
227                }
228                current = &mut arr[index];
229                continue;
230            }
231        }
232
233        // Otherwise treat as object key
234        if !current.is_object() {
235            *current = Value::Object(serde_json::Map::new());
236        }
237
238        if let Value::Object(map) = current {
239            if !map.contains_key(*part) {
240                map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
241            }
242            current = map.get_mut(*part).unwrap();
243        }
244    }
245
246    // Set the final value
247    let final_key = parts[parts.len() - 1];
248
249    // Try to parse as array index
250    if let Ok(index) = final_key.parse::<usize>() {
251        if let Value::Array(arr) = current {
252            while arr.len() <= index {
253                arr.push(Value::Null);
254            }
255            arr[index] = value;
256            return;
257        }
258    }
259
260    // Set as object property
261    if !current.is_object() {
262        *current = Value::Object(serde_json::Map::new());
263    }
264
265    if let Value::Object(map) = current {
266        map.insert(final_key.to_string(), value);
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use serde_json::json;
274
275    #[test]
276    fn test_merge_json() {
277        let mut target = json!({
278            "user": {
279                "name": "Alice",
280                "age": 30
281            }
282        });
283
284        let patch = json!({
285            "user": {
286                "age": 31
287            }
288        });
289
290        merge_json(&mut target, patch);
291
292        assert_eq!(target["user"]["name"], "Alice");
293        assert_eq!(target["user"]["age"], 31);
294    }
295
296    #[test]
297    fn test_lifecycle_on_created_callback() {
298        use std::sync::{Arc, Mutex};
299
300        let module = Module::new("TestModule");
301        let initial_state = json!({});
302        let mut instance = ModuleInstance::new(module, initial_state);
303
304        // Track if callback was called
305        let called = Arc::new(Mutex::new(false));
306        let called_clone = called.clone();
307
308        instance.set_on_created(move || {
309            *called_clone.lock().unwrap() = true;
310        });
311
312        // Mount should call on_created
313        assert!(!*called.lock().unwrap());
314        instance.mount();
315        assert!(*called.lock().unwrap());
316        assert!(instance.mounted);
317    }
318
319    #[test]
320    fn test_lifecycle_on_destroyed_callback() {
321        use std::sync::{Arc, Mutex};
322
323        let module = Module::new("TestModule");
324        let initial_state = json!({});
325        let mut instance = ModuleInstance::new(module, initial_state);
326
327        let called = Arc::new(Mutex::new(false));
328        let called_clone = called.clone();
329
330        instance.set_on_destroyed(move || {
331            *called_clone.lock().unwrap() = true;
332        });
333
334        // First mount the module
335        instance.mount();
336        assert!(instance.mounted);
337
338        // Unmount should call on_destroyed
339        assert!(!*called.lock().unwrap());
340        instance.unmount();
341        assert!(*called.lock().unwrap());
342        assert!(!instance.mounted);
343    }
344
345    #[test]
346    fn test_lifecycle_callbacks_not_called_when_not_set() {
347        let module = Module::new("TestModule");
348        let initial_state = json!({});
349        let mut instance = ModuleInstance::new(module, initial_state);
350
351        // Should not panic when callbacks are not set
352        instance.mount();
353        assert!(instance.mounted);
354
355        instance.unmount();
356        assert!(!instance.mounted);
357    }
358
359    #[test]
360    fn test_lifecycle_mount_idempotent() {
361        use std::sync::{Arc, Mutex};
362
363        let module = Module::new("TestModule");
364        let initial_state = json!({});
365        let mut instance = ModuleInstance::new(module, initial_state);
366
367        let call_count = Arc::new(Mutex::new(0));
368        let call_count_clone = call_count.clone();
369
370        instance.set_on_created(move || {
371            *call_count_clone.lock().unwrap() += 1;
372        });
373
374        // First mount
375        instance.mount();
376        assert_eq!(*call_count.lock().unwrap(), 1);
377
378        // Second mount should not call callback again
379        instance.mount();
380        assert_eq!(*call_count.lock().unwrap(), 1);
381    }
382
383    #[test]
384    fn test_lifecycle_unmount_idempotent() {
385        use std::sync::{Arc, Mutex};
386
387        let module = Module::new("TestModule");
388        let initial_state = json!({});
389        let mut instance = ModuleInstance::new(module, initial_state);
390
391        let call_count = Arc::new(Mutex::new(0));
392        let call_count_clone = call_count.clone();
393
394        instance.set_on_destroyed(move || {
395            *call_count_clone.lock().unwrap() += 1;
396        });
397
398        // Mount first
399        instance.mount();
400
401        // First unmount
402        instance.unmount();
403        assert_eq!(*call_count.lock().unwrap(), 1);
404
405        // Second unmount should not call callback again
406        instance.unmount();
407        assert_eq!(*call_count.lock().unwrap(), 1);
408    }
409
410    #[test]
411    fn test_lifecycle_full_cycle() {
412        use std::sync::{Arc, Mutex};
413
414        let module = Module::new("TestModule");
415        let initial_state = json!({});
416        let mut instance = ModuleInstance::new(module, initial_state);
417
418        let events = Arc::new(Mutex::new(Vec::new()));
419        let events_created = events.clone();
420        let events_destroyed = events.clone();
421
422        instance.set_on_created(move || {
423            events_created.lock().unwrap().push("created");
424        });
425
426        instance.set_on_destroyed(move || {
427            events_destroyed.lock().unwrap().push("destroyed");
428        });
429
430        // Full lifecycle: mount -> unmount -> mount -> unmount
431        instance.mount();
432        instance.unmount();
433        instance.mount();
434        instance.unmount();
435
436        let events = events.lock().unwrap();
437        assert_eq!(events.len(), 4);
438        assert_eq!(events[0], "created");
439        assert_eq!(events[1], "destroyed");
440        assert_eq!(events[2], "created");
441        assert_eq!(events[3], "destroyed");
442    }
443
444    #[test]
445    fn test_set_value_at_path_simple() {
446        let mut state = json!({
447            "count": 0
448        });
449
450        set_value_at_path(&mut state, "count", json!(42));
451        assert_eq!(state["count"], 42);
452    }
453
454    #[test]
455    fn test_set_value_at_path_nested() {
456        let mut state = json!({
457            "user": {
458                "name": "Alice",
459                "profile": {
460                    "bio": "Developer"
461                }
462            }
463        });
464
465        set_value_at_path(&mut state, "user.profile.bio", json!("Engineer"));
466        assert_eq!(state["user"]["profile"]["bio"], "Engineer");
467        // Other values should be unchanged
468        assert_eq!(state["user"]["name"], "Alice");
469    }
470
471    #[test]
472    fn test_set_value_at_path_creates_intermediate() {
473        let mut state = json!({});
474
475        set_value_at_path(&mut state, "user.profile.name", json!("Bob"));
476        assert_eq!(state["user"]["profile"]["name"], "Bob");
477    }
478
479    #[test]
480    fn test_set_value_at_path_array_index() {
481        let mut state = json!({
482            "items": ["a", "b", "c"]
483        });
484
485        set_value_at_path(&mut state, "items.1", json!("modified"));
486        assert_eq!(state["items"][1], "modified");
487        assert_eq!(state["items"][0], "a");
488        assert_eq!(state["items"][2], "c");
489    }
490
491    #[test]
492    fn test_update_state_sparse_single_path() {
493        let module = Module::new("TestModule");
494        let initial_state = json!({
495            "count": 0,
496            "name": "Alice"
497        });
498        let mut instance = ModuleInstance::new(module, initial_state);
499
500        let paths = vec!["count".to_string()];
501        let values = json!({
502            "count": 42
503        });
504
505        instance.update_state_sparse(&paths, &values);
506
507        assert_eq!(instance.get_state()["count"], 42);
508        assert_eq!(instance.get_state()["name"], "Alice"); // Unchanged
509    }
510
511    #[test]
512    fn test_update_state_sparse_nested_path() {
513        let module = Module::new("TestModule");
514        let initial_state = json!({
515            "user": {
516                "name": "Alice",
517                "age": 30
518            },
519            "settings": {
520                "theme": "dark"
521            }
522        });
523        let mut instance = ModuleInstance::new(module, initial_state);
524
525        let paths = vec!["user.age".to_string()];
526        let values = json!({
527            "user.age": 31
528        });
529
530        instance.update_state_sparse(&paths, &values);
531
532        assert_eq!(instance.get_state()["user"]["age"], 31);
533        assert_eq!(instance.get_state()["user"]["name"], "Alice"); // Unchanged
534        assert_eq!(instance.get_state()["settings"]["theme"], "dark"); // Unchanged
535    }
536
537    #[test]
538    fn test_update_state_sparse_multiple_paths() {
539        let module = Module::new("TestModule");
540        let initial_state = json!({
541            "count": 0,
542            "user": {
543                "name": "Alice"
544            }
545        });
546        let mut instance = ModuleInstance::new(module, initial_state);
547
548        let paths = vec!["count".to_string(), "user.name".to_string()];
549        let values = json!({
550            "count": 100,
551            "user.name": "Bob"
552        });
553
554        instance.update_state_sparse(&paths, &values);
555
556        assert_eq!(instance.get_state()["count"], 100);
557        assert_eq!(instance.get_state()["user"]["name"], "Bob");
558    }
559
560    // ============ Edge Case Tests ============
561
562    #[test]
563    fn test_set_value_at_path_empty_path() {
564        let mut state = json!({"count": 0});
565        // Empty path should do nothing
566        set_value_at_path(&mut state, "", json!(42));
567        assert_eq!(state["count"], 0);
568    }
569
570    #[test]
571    fn test_set_value_at_path_null_value() {
572        let mut state = json!({
573            "user": {
574                "name": "Alice",
575                "email": "alice@example.com"
576            }
577        });
578
579        set_value_at_path(&mut state, "user.email", json!(null));
580        assert_eq!(state["user"]["email"], serde_json::Value::Null);
581        assert_eq!(state["user"]["name"], "Alice"); // Unchanged
582    }
583
584    #[test]
585    fn test_set_value_at_path_type_change() {
586        let mut state = json!({
587            "data": "string value"
588        });
589
590        // Change string to object
591        set_value_at_path(&mut state, "data", json!({"nested": true}));
592        assert_eq!(state["data"]["nested"], true);
593
594        // Change object to array
595        set_value_at_path(&mut state, "data", json!([1, 2, 3]));
596        assert_eq!(state["data"][0], 1);
597
598        // Change array to number
599        set_value_at_path(&mut state, "data", json!(42));
600        assert_eq!(state["data"], 42);
601    }
602
603    #[test]
604    fn test_set_value_at_path_deeply_nested() {
605        let mut state = json!({});
606
607        // Create deeply nested path (6 levels deep)
608        set_value_at_path(&mut state, "a.b.c.d.e.f", json!("deep value"));
609        assert_eq!(state["a"]["b"]["c"]["d"]["e"]["f"], "deep value");
610    }
611
612    #[test]
613    fn test_set_value_at_path_nested_array_object() {
614        let mut state = json!({
615            "users": [
616                {"name": "Alice", "tags": ["admin"]},
617                {"name": "Bob", "tags": ["user"]}
618            ]
619        });
620
621        // Update nested object within array
622        set_value_at_path(&mut state, "users.1.name", json!("Robert"));
623        assert_eq!(state["users"][1]["name"], "Robert");
624        assert_eq!(state["users"][0]["name"], "Alice"); // Unchanged
625
626        // Update nested array within array element
627        set_value_at_path(&mut state, "users.0.tags.0", json!("superadmin"));
628        assert_eq!(state["users"][0]["tags"][0], "superadmin");
629    }
630
631    #[test]
632    fn test_set_value_at_path_extend_array() {
633        let mut state = json!({
634            "items": ["a", "b"]
635        });
636
637        // Setting index 5 should extend array with nulls
638        set_value_at_path(&mut state, "items.5", json!("extended"));
639        assert_eq!(state["items"].as_array().unwrap().len(), 6);
640        assert_eq!(state["items"][5], "extended");
641        assert_eq!(state["items"][2], serde_json::Value::Null);
642        assert_eq!(state["items"][3], serde_json::Value::Null);
643        assert_eq!(state["items"][4], serde_json::Value::Null);
644    }
645
646    #[test]
647    fn test_set_value_at_path_overwrite_primitive_with_nested() {
648        let mut state = json!({
649            "config": 42
650        });
651
652        // Trying to set a nested path where parent is a primitive
653        // Should convert primitive to object
654        set_value_at_path(&mut state, "config.nested.value", json!("test"));
655        assert_eq!(state["config"]["nested"]["value"], "test");
656    }
657
658    #[test]
659    fn test_set_value_at_path_boolean_values() {
660        let mut state = json!({
661            "flags": {
662                "enabled": true,
663                "visible": false
664            }
665        });
666
667        set_value_at_path(&mut state, "flags.enabled", json!(false));
668        set_value_at_path(&mut state, "flags.visible", json!(true));
669        assert_eq!(state["flags"]["enabled"], false);
670        assert_eq!(state["flags"]["visible"], true);
671    }
672
673    #[test]
674    fn test_set_value_at_path_float_values() {
675        let mut state = json!({
676            "coordinates": {
677                "lat": 0.0,
678                "lng": 0.0
679            }
680        });
681
682        set_value_at_path(&mut state, "coordinates.lat", json!(37.7749));
683        set_value_at_path(&mut state, "coordinates.lng", json!(-122.4194));
684
685        // Use approximate comparison for floats
686        let lat = state["coordinates"]["lat"].as_f64().unwrap();
687        let lng = state["coordinates"]["lng"].as_f64().unwrap();
688        assert!((lat - 37.7749).abs() < 0.0001);
689        assert!((lng - (-122.4194)).abs() < 0.0001);
690    }
691
692    #[test]
693    fn test_update_state_sparse_empty_paths() {
694        let module = Module::new("TestModule");
695        let initial_state = json!({
696            "count": 0
697        });
698        let mut instance = ModuleInstance::new(module, initial_state);
699
700        let paths: Vec<String> = vec![];
701        let values = json!({});
702
703        // Should not panic or modify state
704        instance.update_state_sparse(&paths, &values);
705        assert_eq!(instance.get_state()["count"], 0);
706    }
707
708    #[test]
709    fn test_update_state_sparse_path_not_in_values() {
710        let module = Module::new("TestModule");
711        let initial_state = json!({
712            "count": 0,
713            "name": "Alice"
714        });
715        let mut instance = ModuleInstance::new(module, initial_state);
716
717        // Path is specified but not in values - should be skipped
718        let paths = vec!["count".to_string(), "missing".to_string()];
719        let values = json!({
720            "count": 42
721            // "missing" not provided
722        });
723
724        instance.update_state_sparse(&paths, &values);
725        assert_eq!(instance.get_state()["count"], 42);
726        assert_eq!(instance.get_state()["name"], "Alice");
727    }
728
729    #[test]
730    fn test_update_state_sparse_invalid_values_type() {
731        let module = Module::new("TestModule");
732        let initial_state = json!({
733            "count": 0
734        });
735        let mut instance = ModuleInstance::new(module, initial_state);
736
737        // values is not an object - should not crash, just skip
738        let paths = vec!["count".to_string()];
739        let values = json!("not an object");
740
741        instance.update_state_sparse(&paths, &values);
742        // State should remain unchanged
743        assert_eq!(instance.get_state()["count"], 0);
744    }
745
746    #[test]
747    fn test_update_state_sparse_array_values() {
748        let module = Module::new("TestModule");
749        let initial_state = json!({
750            "items": []
751        });
752        let mut instance = ModuleInstance::new(module, initial_state);
753
754        let paths = vec!["items".to_string()];
755        let values = json!({
756            "items": ["a", "b", "c"]
757        });
758
759        instance.update_state_sparse(&paths, &values);
760        assert_eq!(instance.get_state()["items"], json!(["a", "b", "c"]));
761    }
762
763    #[test]
764    fn test_update_state_sparse_complex_nested_update() {
765        let module = Module::new("TestModule");
766        let initial_state = json!({
767            "app": {
768                "ui": {
769                    "theme": "light",
770                    "sidebar": {
771                        "collapsed": false,
772                        "width": 250
773                    }
774                },
775                "data": {
776                    "users": [],
777                    "cache": {}
778                }
779            }
780        });
781        let mut instance = ModuleInstance::new(module, initial_state);
782
783        let paths = vec![
784            "app.ui.theme".to_string(),
785            "app.ui.sidebar.collapsed".to_string(),
786            "app.data.users".to_string(),
787        ];
788        let values = json!({
789            "app.ui.theme": "dark",
790            "app.ui.sidebar.collapsed": true,
791            "app.data.users": [{"id": 1, "name": "Alice"}]
792        });
793
794        instance.update_state_sparse(&paths, &values);
795
796        assert_eq!(instance.get_state()["app"]["ui"]["theme"], "dark");
797        assert_eq!(
798            instance.get_state()["app"]["ui"]["sidebar"]["collapsed"],
799            true
800        );
801        assert_eq!(instance.get_state()["app"]["ui"]["sidebar"]["width"], 250); // Unchanged
802        assert_eq!(
803            instance.get_state()["app"]["data"]["users"][0]["name"],
804            "Alice"
805        );
806        assert!(instance.get_state()["app"]["data"]["cache"].is_object()); // Unchanged
807    }
808
809    #[test]
810    fn test_update_state_sparse_preserves_sibling_keys() {
811        let module = Module::new("TestModule");
812        let initial_state = json!({
813            "user": {
814                "name": "Alice",
815                "email": "alice@example.com",
816                "profile": {
817                    "bio": "Developer",
818                    "avatar": "alice.png",
819                    "social": {
820                        "twitter": "@alice",
821                        "github": "alice"
822                    }
823                }
824            }
825        });
826        let mut instance = ModuleInstance::new(module, initial_state);
827
828        // Only update one deeply nested field
829        let paths = vec!["user.profile.social.twitter".to_string()];
830        let values = json!({
831            "user.profile.social.twitter": "@alice_new"
832        });
833
834        instance.update_state_sparse(&paths, &values);
835
836        // Verify updated field
837        assert_eq!(
838            instance.get_state()["user"]["profile"]["social"]["twitter"],
839            "@alice_new"
840        );
841
842        // Verify all sibling fields are unchanged
843        assert_eq!(instance.get_state()["user"]["name"], "Alice");
844        assert_eq!(instance.get_state()["user"]["email"], "alice@example.com");
845        assert_eq!(instance.get_state()["user"]["profile"]["bio"], "Developer");
846        assert_eq!(
847            instance.get_state()["user"]["profile"]["avatar"],
848            "alice.png"
849        );
850        assert_eq!(
851            instance.get_state()["user"]["profile"]["social"]["github"],
852            "alice"
853        );
854    }
855}