use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Module {
pub name: String,
pub actions: Vec<String>,
pub state_keys: Vec<String>,
pub persist: bool,
pub version: Option<u32>,
}
impl Module {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
actions: Vec::new(),
state_keys: Vec::new(),
persist: false,
version: None,
}
}
pub fn with_actions(mut self, actions: Vec<String>) -> Self {
self.actions = actions;
self
}
pub fn with_state_keys(mut self, state_keys: Vec<String>) -> Self {
self.state_keys = state_keys;
self
}
pub fn with_persist(mut self, persist: bool) -> Self {
self.persist = persist;
self
}
pub fn with_version(mut self, version: u32) -> Self {
self.version = Some(version);
self
}
}
pub trait ModuleLifecycle {
fn on_created(&mut self);
fn on_destroyed(&mut self);
fn on_state_changed(&mut self, state: serde_json::Value);
}
pub type LifecycleCallback = Box<dyn Fn() + Send + Sync>;
pub struct ModuleInstance {
pub module: Module,
state: Arc<serde_json::Value>,
pub mounted: bool,
on_created: Option<LifecycleCallback>,
on_destroyed: Option<LifecycleCallback>,
}
impl ModuleInstance {
pub fn new(module: Module, initial_state: serde_json::Value) -> Self {
Self {
module,
state: Arc::new(initial_state),
mounted: false,
on_created: None,
on_destroyed: None,
}
}
pub fn from_config(
name: &str,
actions: Vec<String>,
state_keys: Vec<String>,
initial_state: serde_json::Value,
) -> Self {
let module = Module::new(name)
.with_actions(actions)
.with_state_keys(state_keys);
Self::new(module, initial_state)
}
pub fn set_on_created<F>(&mut self, callback: F)
where
F: Fn() + Send + Sync + 'static,
{
self.on_created = Some(Box::new(callback));
}
pub fn set_on_destroyed<F>(&mut self, callback: F)
where
F: Fn() + Send + Sync + 'static,
{
self.on_destroyed = Some(Box::new(callback));
}
pub fn mount(&mut self) {
if !self.mounted {
self.mounted = true;
if let Some(ref callback) = self.on_created {
callback();
}
}
}
pub fn unmount(&mut self) {
if self.mounted {
if let Some(ref callback) = self.on_destroyed {
callback();
}
self.mounted = false;
}
}
pub fn update_state(&mut self, patch: serde_json::Value) {
let state = Arc::make_mut(&mut self.state);
merge_json(state, patch);
}
pub fn update_state_sparse(&mut self, paths: &[String], values: &serde_json::Value) {
if let serde_json::Value::Object(map) = values {
if paths.iter().any(|p| map.contains_key(p)) {
let state = Arc::make_mut(&mut self.state);
for path in paths {
if let Some(new_value) = map.get(path) {
set_value_at_path(state, path, new_value.clone());
}
}
}
}
}
pub fn get_state(&self) -> &serde_json::Value {
&self.state
}
pub fn get_state_shared(&self) -> Arc<serde_json::Value> {
Arc::clone(&self.state)
}
}
fn merge_json(target: &mut serde_json::Value, source: serde_json::Value) {
use serde_json::Value;
match (target, source) {
(Value::Object(target_map), Value::Object(source_map)) => {
for (key, value) in source_map {
if let Some(target_value) = target_map.get_mut(&key) {
merge_json(target_value, value);
} else {
target_map.insert(key, value);
}
}
}
(target, source) => {
*target = source;
}
}
}
fn set_value_at_path(target: &mut serde_json::Value, path: &str, value: serde_json::Value) {
use serde_json::Value;
let parts: Vec<&str> = path.split('.').collect();
if parts.is_empty() {
return;
}
let mut current = target;
for part in &parts[..parts.len() - 1] {
if let Ok(index) = part.parse::<usize>() {
if let Value::Array(arr) = current {
while arr.len() <= index {
arr.push(Value::Null);
}
current = &mut arr[index];
continue;
}
}
if !current.is_object() {
*current = Value::Object(serde_json::Map::new());
}
if let Value::Object(map) = current {
if !map.contains_key(*part) {
map.insert(part.to_string(), Value::Object(serde_json::Map::new()));
}
current = map.get_mut(*part).unwrap();
}
}
let final_key = parts[parts.len() - 1];
if let Ok(index) = final_key.parse::<usize>() {
if let Value::Array(arr) = current {
while arr.len() <= index {
arr.push(Value::Null);
}
arr[index] = value;
return;
}
}
if !current.is_object() {
*current = Value::Object(serde_json::Map::new());
}
if let Value::Object(map) = current {
map.insert(final_key.to_string(), value);
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_merge_json() {
let mut target = json!({
"user": {
"name": "Alice",
"age": 30
}
});
let patch = json!({
"user": {
"age": 31
}
});
merge_json(&mut target, patch);
assert_eq!(target["user"]["name"], "Alice");
assert_eq!(target["user"]["age"], 31);
}
#[test]
fn test_lifecycle_on_created_callback() {
use std::sync::{Arc, Mutex};
let module = Module::new("TestModule");
let initial_state = json!({});
let mut instance = ModuleInstance::new(module, initial_state);
let called = Arc::new(Mutex::new(false));
let called_clone = called.clone();
instance.set_on_created(move || {
*called_clone.lock().unwrap() = true;
});
assert!(!*called.lock().unwrap());
instance.mount();
assert!(*called.lock().unwrap());
assert!(instance.mounted);
}
#[test]
fn test_lifecycle_on_destroyed_callback() {
use std::sync::{Arc, Mutex};
let module = Module::new("TestModule");
let initial_state = json!({});
let mut instance = ModuleInstance::new(module, initial_state);
let called = Arc::new(Mutex::new(false));
let called_clone = called.clone();
instance.set_on_destroyed(move || {
*called_clone.lock().unwrap() = true;
});
instance.mount();
assert!(instance.mounted);
assert!(!*called.lock().unwrap());
instance.unmount();
assert!(*called.lock().unwrap());
assert!(!instance.mounted);
}
#[test]
fn test_lifecycle_callbacks_not_called_when_not_set() {
let module = Module::new("TestModule");
let initial_state = json!({});
let mut instance = ModuleInstance::new(module, initial_state);
instance.mount();
assert!(instance.mounted);
instance.unmount();
assert!(!instance.mounted);
}
#[test]
fn test_lifecycle_mount_idempotent() {
use std::sync::{Arc, Mutex};
let module = Module::new("TestModule");
let initial_state = json!({});
let mut instance = ModuleInstance::new(module, initial_state);
let call_count = Arc::new(Mutex::new(0));
let call_count_clone = call_count.clone();
instance.set_on_created(move || {
*call_count_clone.lock().unwrap() += 1;
});
instance.mount();
assert_eq!(*call_count.lock().unwrap(), 1);
instance.mount();
assert_eq!(*call_count.lock().unwrap(), 1);
}
#[test]
fn test_lifecycle_unmount_idempotent() {
use std::sync::{Arc, Mutex};
let module = Module::new("TestModule");
let initial_state = json!({});
let mut instance = ModuleInstance::new(module, initial_state);
let call_count = Arc::new(Mutex::new(0));
let call_count_clone = call_count.clone();
instance.set_on_destroyed(move || {
*call_count_clone.lock().unwrap() += 1;
});
instance.mount();
instance.unmount();
assert_eq!(*call_count.lock().unwrap(), 1);
instance.unmount();
assert_eq!(*call_count.lock().unwrap(), 1);
}
#[test]
fn test_lifecycle_full_cycle() {
use std::sync::{Arc, Mutex};
let module = Module::new("TestModule");
let initial_state = json!({});
let mut instance = ModuleInstance::new(module, initial_state);
let events = Arc::new(Mutex::new(Vec::new()));
let events_created = events.clone();
let events_destroyed = events.clone();
instance.set_on_created(move || {
events_created.lock().unwrap().push("created");
});
instance.set_on_destroyed(move || {
events_destroyed.lock().unwrap().push("destroyed");
});
instance.mount();
instance.unmount();
instance.mount();
instance.unmount();
let events = events.lock().unwrap();
assert_eq!(events.len(), 4);
assert_eq!(events[0], "created");
assert_eq!(events[1], "destroyed");
assert_eq!(events[2], "created");
assert_eq!(events[3], "destroyed");
}
#[test]
fn test_set_value_at_path_simple() {
let mut state = json!({
"count": 0
});
set_value_at_path(&mut state, "count", json!(42));
assert_eq!(state["count"], 42);
}
#[test]
fn test_set_value_at_path_nested() {
let mut state = json!({
"user": {
"name": "Alice",
"profile": {
"bio": "Developer"
}
}
});
set_value_at_path(&mut state, "user.profile.bio", json!("Engineer"));
assert_eq!(state["user"]["profile"]["bio"], "Engineer");
assert_eq!(state["user"]["name"], "Alice");
}
#[test]
fn test_set_value_at_path_creates_intermediate() {
let mut state = json!({});
set_value_at_path(&mut state, "user.profile.name", json!("Bob"));
assert_eq!(state["user"]["profile"]["name"], "Bob");
}
#[test]
fn test_set_value_at_path_array_index() {
let mut state = json!({
"items": ["a", "b", "c"]
});
set_value_at_path(&mut state, "items.1", json!("modified"));
assert_eq!(state["items"][1], "modified");
assert_eq!(state["items"][0], "a");
assert_eq!(state["items"][2], "c");
}
#[test]
fn test_update_state_sparse_single_path() {
let module = Module::new("TestModule");
let initial_state = json!({
"count": 0,
"name": "Alice"
});
let mut instance = ModuleInstance::new(module, initial_state);
let paths = vec!["count".to_string()];
let values = json!({
"count": 42
});
instance.update_state_sparse(&paths, &values);
assert_eq!(instance.get_state()["count"], 42);
assert_eq!(instance.get_state()["name"], "Alice"); }
#[test]
fn test_update_state_sparse_nested_path() {
let module = Module::new("TestModule");
let initial_state = json!({
"user": {
"name": "Alice",
"age": 30
},
"settings": {
"theme": "dark"
}
});
let mut instance = ModuleInstance::new(module, initial_state);
let paths = vec!["user.age".to_string()];
let values = json!({
"user.age": 31
});
instance.update_state_sparse(&paths, &values);
assert_eq!(instance.get_state()["user"]["age"], 31);
assert_eq!(instance.get_state()["user"]["name"], "Alice"); assert_eq!(instance.get_state()["settings"]["theme"], "dark"); }
#[test]
fn test_update_state_sparse_multiple_paths() {
let module = Module::new("TestModule");
let initial_state = json!({
"count": 0,
"user": {
"name": "Alice"
}
});
let mut instance = ModuleInstance::new(module, initial_state);
let paths = vec!["count".to_string(), "user.name".to_string()];
let values = json!({
"count": 100,
"user.name": "Bob"
});
instance.update_state_sparse(&paths, &values);
assert_eq!(instance.get_state()["count"], 100);
assert_eq!(instance.get_state()["user"]["name"], "Bob");
}
#[test]
fn test_set_value_at_path_empty_path() {
let mut state = json!({"count": 0});
set_value_at_path(&mut state, "", json!(42));
assert_eq!(state["count"], 0);
}
#[test]
fn test_set_value_at_path_null_value() {
let mut state = json!({
"user": {
"name": "Alice",
"email": "alice@example.com"
}
});
set_value_at_path(&mut state, "user.email", json!(null));
assert_eq!(state["user"]["email"], serde_json::Value::Null);
assert_eq!(state["user"]["name"], "Alice"); }
#[test]
fn test_set_value_at_path_type_change() {
let mut state = json!({
"data": "string value"
});
set_value_at_path(&mut state, "data", json!({"nested": true}));
assert_eq!(state["data"]["nested"], true);
set_value_at_path(&mut state, "data", json!([1, 2, 3]));
assert_eq!(state["data"][0], 1);
set_value_at_path(&mut state, "data", json!(42));
assert_eq!(state["data"], 42);
}
#[test]
fn test_set_value_at_path_deeply_nested() {
let mut state = json!({});
set_value_at_path(&mut state, "a.b.c.d.e.f", json!("deep value"));
assert_eq!(state["a"]["b"]["c"]["d"]["e"]["f"], "deep value");
}
#[test]
fn test_set_value_at_path_nested_array_object() {
let mut state = json!({
"users": [
{"name": "Alice", "tags": ["admin"]},
{"name": "Bob", "tags": ["user"]}
]
});
set_value_at_path(&mut state, "users.1.name", json!("Robert"));
assert_eq!(state["users"][1]["name"], "Robert");
assert_eq!(state["users"][0]["name"], "Alice");
set_value_at_path(&mut state, "users.0.tags.0", json!("superadmin"));
assert_eq!(state["users"][0]["tags"][0], "superadmin");
}
#[test]
fn test_set_value_at_path_extend_array() {
let mut state = json!({
"items": ["a", "b"]
});
set_value_at_path(&mut state, "items.5", json!("extended"));
assert_eq!(state["items"].as_array().unwrap().len(), 6);
assert_eq!(state["items"][5], "extended");
assert_eq!(state["items"][2], serde_json::Value::Null);
assert_eq!(state["items"][3], serde_json::Value::Null);
assert_eq!(state["items"][4], serde_json::Value::Null);
}
#[test]
fn test_set_value_at_path_overwrite_primitive_with_nested() {
let mut state = json!({
"config": 42
});
set_value_at_path(&mut state, "config.nested.value", json!("test"));
assert_eq!(state["config"]["nested"]["value"], "test");
}
#[test]
fn test_set_value_at_path_boolean_values() {
let mut state = json!({
"flags": {
"enabled": true,
"visible": false
}
});
set_value_at_path(&mut state, "flags.enabled", json!(false));
set_value_at_path(&mut state, "flags.visible", json!(true));
assert_eq!(state["flags"]["enabled"], false);
assert_eq!(state["flags"]["visible"], true);
}
#[test]
fn test_set_value_at_path_float_values() {
let mut state = json!({
"coordinates": {
"lat": 0.0,
"lng": 0.0
}
});
set_value_at_path(&mut state, "coordinates.lat", json!(37.7749));
set_value_at_path(&mut state, "coordinates.lng", json!(-122.4194));
let lat = state["coordinates"]["lat"].as_f64().unwrap();
let lng = state["coordinates"]["lng"].as_f64().unwrap();
assert!((lat - 37.7749).abs() < 0.0001);
assert!((lng - (-122.4194)).abs() < 0.0001);
}
#[test]
fn test_update_state_sparse_empty_paths() {
let module = Module::new("TestModule");
let initial_state = json!({
"count": 0
});
let mut instance = ModuleInstance::new(module, initial_state);
let paths: Vec<String> = vec![];
let values = json!({});
instance.update_state_sparse(&paths, &values);
assert_eq!(instance.get_state()["count"], 0);
}
#[test]
fn test_update_state_sparse_path_not_in_values() {
let module = Module::new("TestModule");
let initial_state = json!({
"count": 0,
"name": "Alice"
});
let mut instance = ModuleInstance::new(module, initial_state);
let paths = vec!["count".to_string(), "missing".to_string()];
let values = json!({
"count": 42
});
instance.update_state_sparse(&paths, &values);
assert_eq!(instance.get_state()["count"], 42);
assert_eq!(instance.get_state()["name"], "Alice");
}
#[test]
fn test_update_state_sparse_invalid_values_type() {
let module = Module::new("TestModule");
let initial_state = json!({
"count": 0
});
let mut instance = ModuleInstance::new(module, initial_state);
let paths = vec!["count".to_string()];
let values = json!("not an object");
instance.update_state_sparse(&paths, &values);
assert_eq!(instance.get_state()["count"], 0);
}
#[test]
fn test_update_state_sparse_array_values() {
let module = Module::new("TestModule");
let initial_state = json!({
"items": []
});
let mut instance = ModuleInstance::new(module, initial_state);
let paths = vec!["items".to_string()];
let values = json!({
"items": ["a", "b", "c"]
});
instance.update_state_sparse(&paths, &values);
assert_eq!(instance.get_state()["items"], json!(["a", "b", "c"]));
}
#[test]
fn test_update_state_sparse_complex_nested_update() {
let module = Module::new("TestModule");
let initial_state = json!({
"app": {
"ui": {
"theme": "light",
"sidebar": {
"collapsed": false,
"width": 250
}
},
"data": {
"users": [],
"cache": {}
}
}
});
let mut instance = ModuleInstance::new(module, initial_state);
let paths = vec![
"app.ui.theme".to_string(),
"app.ui.sidebar.collapsed".to_string(),
"app.data.users".to_string(),
];
let values = json!({
"app.ui.theme": "dark",
"app.ui.sidebar.collapsed": true,
"app.data.users": [{"id": 1, "name": "Alice"}]
});
instance.update_state_sparse(&paths, &values);
assert_eq!(instance.get_state()["app"]["ui"]["theme"], "dark");
assert_eq!(
instance.get_state()["app"]["ui"]["sidebar"]["collapsed"],
true
);
assert_eq!(instance.get_state()["app"]["ui"]["sidebar"]["width"], 250); assert_eq!(
instance.get_state()["app"]["data"]["users"][0]["name"],
"Alice"
);
assert!(instance.get_state()["app"]["data"]["cache"].is_object()); }
#[test]
fn test_update_state_sparse_preserves_sibling_keys() {
let module = Module::new("TestModule");
let initial_state = json!({
"user": {
"name": "Alice",
"email": "alice@example.com",
"profile": {
"bio": "Developer",
"avatar": "alice.png",
"social": {
"twitter": "@alice",
"github": "alice"
}
}
}
});
let mut instance = ModuleInstance::new(module, initial_state);
let paths = vec!["user.profile.social.twitter".to_string()];
let values = json!({
"user.profile.social.twitter": "@alice_new"
});
instance.update_state_sparse(&paths, &values);
assert_eq!(
instance.get_state()["user"]["profile"]["social"]["twitter"],
"@alice_new"
);
assert_eq!(instance.get_state()["user"]["name"], "Alice");
assert_eq!(instance.get_state()["user"]["email"], "alice@example.com");
assert_eq!(instance.get_state()["user"]["profile"]["bio"], "Developer");
assert_eq!(
instance.get_state()["user"]["profile"]["avatar"],
"alice.png"
);
assert_eq!(
instance.get_state()["user"]["profile"]["social"]["github"],
"alice"
);
}
}