hypen_engine/lifecycle/
module.rs

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