tauri-plugin-rpstate 0.5.1

Tauri v2 plugin that exposes rpstate reactive persistent state to the frontend, with TypeScript codegen
Documentation
use rpstate::{DefaultStore, Store, StoreBuilder, rpstate};
use std::sync::Arc;
use tauri::Manager;
use tauri_plugin_rpstate::codegen;

#[rpstate]
pub struct TestNested {
    #[state(default = "nested_val".to_string())]
    pub name: String,
}

#[rpstate(prefix = "test_root")]
pub struct TestRoot {
    #[state(default = 42)]
    pub value: i32,

    #[state(default = "volatile_val".to_string(), volatile)]
    pub session: String,

    #[state(nested)]
    pub child: TestNested,
}

#[test]
fn test_schema_inventory_registrations() {
    let mut found_root = false;
    let mut found_nested = false;

    for entry in inventory::iter::<rpstate::tauri_codegen::SchemaExportEntry>() {
        if entry.struct_name == "TestRoot" {
            found_root = true;
            assert_eq!(entry.prefix, Some("test_root"));
            assert_eq!(entry.fields.len(), 3);
            assert_eq!(entry.fields[0].name, "value");
            assert_eq!(entry.fields[1].name, "session");
            assert_eq!(entry.fields[2].name, "child");
        } else if entry.struct_name == "TestNested" {
            found_nested = true;
            assert_eq!(entry.prefix, None);
            assert_eq!(entry.fields.len(), 1);
            assert_eq!(entry.fields[0].name, "name");
        }
    }

    assert!(found_root, "TestRoot was not registered in inventory!");
    assert!(found_nested, "TestNested was not registered in inventory!");
}

#[test]
fn test_typescript_codegen_export() {
    let out_path = std::env::temp_dir().join("rpstate_test_export.ts");
    if out_path.exists() {
        let _ = std::fs::remove_file(&out_path);
    }

    codegen::export(&out_path).expect("Failed to export TS bindings");

    assert!(out_path.exists());
    let ts_content = std::fs::read_to_string(&out_path).expect("Failed to read exported TS file");

    assert!(ts_content.contains("export class Field<T>"));
    assert!(ts_content.contains("export class ReadonlyField<T>"));
    assert!(ts_content.contains("export class ReactiveMapField<K extends string, V>"));

    assert!(ts_content.contains("\"test_root.value\": number;"));
    assert!(ts_content.contains("\"test_root.session\": string;"));
    assert!(ts_content.contains("\"test_root.child.name\": string;"));

    assert!(ts_content.contains("export class TestRoot {"));
    assert!(ts_content.contains("readonly value: Field<number>;"));
    assert!(ts_content.contains(
        "this.value = new Field<number>(\"test_root.value\", initialValues?.[\"test_root.value\"]);"
    ));
    assert!(ts_content.contains("readonly session: Field<string>;"));
    assert!(ts_content.contains("this.session = new Field<string>(\"test_root.session\", initialValues?.[\"test_root.session\"]);"));
    assert!(ts_content.contains("readonly child: TestNestedFields;"));
    assert!(
        ts_content
            .contains("this.child = new TestNestedFields(\"test_root.child\", initialValues);")
    );
    assert!(ts_content.contains("class TestNestedFields {"));
    assert!(ts_content.contains("readonly name: Field<string>;"));
    assert!(ts_content.contains(
        r#"this.name = new Field(`${prefix}.name`, initialValues?.[`${prefix}.name`]);"#
    ));
}

#[tokio::test]
async fn test_tauri_plugin_commands() {
    let db_path = std::env::temp_dir().join("rpstate_tauri_test_store.redb");
    if db_path.exists() {
        let _ = std::fs::remove_file(&db_path);
    }

    let store = StoreBuilder::new(&db_path).build().unwrap();

    store.set("test_root.value", &100i32).unwrap();
    store.save_now().unwrap();

    let app = tauri::test::mock_app();
    app.manage(store.clone());

    let state_store = app.state::<Arc<DefaultStore>>();

    let val = tauri_plugin_rpstate::commands::rpstate_get(
        state_store.clone(),
        "test_root.value".to_string(),
    )
    .await;
    assert_eq!(val, Ok(Some(serde_json::json!(100))));

    let set_res = tauri_plugin_rpstate::commands::rpstate_set(
        state_store.clone(),
        "test_root.value".to_string(),
        serde_json::json!(200),
    )
    .await;
    assert_eq!(set_res, Ok(()));

    let updated_val: Option<i32> = store.get("test_root.value").unwrap();
    assert_eq!(updated_val, Some(200));
}