entity_graph_tauri/
lib.rs

1//! lib.rs
2//!
3//! Tauri v2 entity-graph plugin entry point.
4//!
5//! Register in your app's `lib.rs`:
6//!
7//! ```rust,no_run,ignore
8//! use entity_graph_tauri::init;
9//!
10//! tauri::Builder::default()
11//!     .plugin(init())
12//!     .run(tauri::generate_context!())
13//!     .expect("error while running tauri application");
14//! ```
15//!
16//! The desktop implementation keeps graph operations in Rust. Android and iOS
17//! additionally register a tiny native plugin used by host/device smoke lanes.
18
19pub mod commands;
20#[cfg(desktop)]
21mod desktop;
22mod error;
23#[cfg(mobile)]
24mod mobile;
25pub mod state;
26pub mod types;
27
28use tauri::{
29    generate_handler,
30    plugin::{Builder as PluginBuilder, TauriPlugin},
31    Manager, Runtime,
32};
33
34use commands::{
35    graph_clear, graph_get_entity, graph_get_list, graph_patch_entity, graph_persist_snapshot,
36    graph_platform_ping, graph_remove_entity, graph_restore_snapshot, graph_set_list,
37    graph_upsert_entity,
38};
39pub use error::{Error, Result};
40use state::GraphPluginState;
41pub use types::PlatformPing;
42
43#[cfg(feature = "generate-bindings")]
44use types::{
45    EntityChangedEvent, GraphPluginErrorEvent, SnapshotPersistedEvent, SnapshotRestoredEvent,
46};
47
48#[cfg(desktop)]
49pub use desktop::EntityGraph;
50#[cfg(mobile)]
51pub use mobile::EntityGraph;
52
53/// The plugin name used for IPC routing.
54pub(crate) const PLUGIN_NAME: &str = "entity-graph-tauri";
55
56#[cfg(feature = "generate-bindings")]
57pub fn binding_builder() -> tauri_specta::Builder<tauri::test::MockRuntime> {
58    tauri_specta::Builder::<tauri::test::MockRuntime>::new()
59        .plugin_name(PLUGIN_NAME)
60        .commands(tauri_specta::collect_commands![
61            graph_upsert_entity,
62            graph_remove_entity,
63            graph_patch_entity,
64            graph_set_list,
65            graph_get_entity,
66            graph_get_list,
67            graph_platform_ping::<tauri::test::MockRuntime>,
68            graph_clear,
69            graph_persist_snapshot::<tauri::test::MockRuntime>,
70            graph_restore_snapshot::<tauri::test::MockRuntime>,
71        ])
72        .events(tauri_specta::collect_events![
73            EntityChangedEvent,
74            SnapshotPersistedEvent,
75            SnapshotRestoredEvent,
76            GraphPluginErrorEvent,
77        ])
78        .constant("PLUGIN_NAME", PLUGIN_NAME)
79        .dangerously_cast_bigints_to_number()
80}
81
82#[cfg(feature = "generate-bindings")]
83pub fn export_bindings(
84    path: impl AsRef<std::path::Path>,
85) -> std::result::Result<(), Box<dyn std::error::Error>> {
86    use specta_typescript::Typescript;
87
88    let path = path.as_ref();
89    binding_builder().export(
90        Typescript::default().header("// @generated by entity-graph-tauri; do not edit manually"),
91        path,
92    )?;
93
94    let generated = std::fs::read_to_string(path)?;
95    std::fs::write(path, format!("{}\n", generated.trim_end()))?;
96    Ok(())
97}
98
99/// Access the native platform bridge from a Tauri manager.
100pub trait EntityGraphExt<R: Runtime> {
101    fn entity_graph(&self) -> &EntityGraph<R>;
102}
103
104impl<R: Runtime, T: Manager<R>> EntityGraphExt<R> for T {
105    fn entity_graph(&self) -> &EntityGraph<R> {
106        self.state::<EntityGraph<R>>().inner()
107    }
108}
109
110/// Initialize the Tauri plugin.
111pub fn init<R: Runtime>() -> TauriPlugin<R> {
112    PluginBuilder::new(PLUGIN_NAME)
113        .setup(|app, api| {
114            #[cfg(desktop)]
115            let platform = desktop::init(app, api)?;
116            #[cfg(mobile)]
117            let platform = mobile::init(app, api)?;
118
119            app.manage(GraphPluginState::new());
120            app.manage(platform);
121            Ok(())
122        })
123        .invoke_handler(generate_handler![
124            graph_upsert_entity,
125            graph_remove_entity,
126            graph_patch_entity,
127            graph_set_list,
128            graph_get_entity,
129            graph_get_list,
130            graph_platform_ping,
131            graph_persist_snapshot,
132            graph_restore_snapshot,
133            graph_clear,
134        ])
135        .build()
136}
137
138/// Compatibility constructor retained for alpha consumers.
139pub struct EntityGraphPlugin;
140
141impl EntityGraphPlugin {
142    /// Create the plugin. New code can call [`init`] directly.
143    pub fn new<R: Runtime>() -> TauriPlugin<R> {
144        init()
145    }
146}
147
148// ── Tests ─────────────────────────────────────────────────────────────────────
149
150#[cfg(test)]
151mod tests {
152    use crate::state::GraphPluginState;
153
154    #[tokio::test]
155    async fn upsert_and_get_entity_roundtrip() {
156        let state = GraphPluginState::new();
157        let mut data = std::collections::HashMap::new();
158        data.insert("name".to_owned(), serde_json::json!("Alice"));
159
160        state.upsert_entity("User", "u-1", data).await;
161
162        let retrieved = state.get_entity("User", "u-1").await;
163        assert!(retrieved.is_some());
164        assert_eq!(retrieved.unwrap()["name"], serde_json::json!("Alice"));
165    }
166
167    #[tokio::test]
168    async fn remove_entity_clears_entry() {
169        let state = GraphPluginState::new();
170        let mut data = std::collections::HashMap::new();
171        data.insert("title".to_owned(), serde_json::json!("Hello"));
172
173        state.upsert_entity("Post", "p-1", data).await;
174        state.remove_entity("Post", "p-1").await;
175
176        assert!(state.get_entity("Post", "p-1").await.is_none());
177    }
178
179    #[tokio::test]
180    async fn set_and_get_list_roundtrip() {
181        let state = GraphPluginState::new();
182        state
183            .set_list("posts:all", vec!["p-1".into(), "p-2".into()], Some(2))
184            .await;
185
186        let entry = state.get_list("posts:all").await.unwrap();
187        assert_eq!(entry.ids, vec!["p-1", "p-2"]);
188        assert_eq!(entry.total, Some(2));
189    }
190
191    #[tokio::test]
192    async fn get_nonexistent_entity_returns_none() {
193        let state = GraphPluginState::new();
194        assert!(state.get_entity("Ghost", "g-1").await.is_none());
195    }
196
197    #[tokio::test]
198    async fn snapshot_roundtrip() {
199        let state = GraphPluginState::new();
200        let snap = r#"{"entities":{},"patches":{}}"#.to_owned();
201        state.set_snapshot("test:key", snap.clone()).await;
202
203        let retrieved = state.get_snapshot("test:key").await.unwrap();
204        assert_eq!(retrieved, snap);
205    }
206
207    #[tokio::test]
208    async fn clear_entities_empties_map() {
209        let state = GraphPluginState::new();
210        let mut data = std::collections::HashMap::new();
211        data.insert("x".to_owned(), serde_json::json!(1));
212        state.upsert_entity("T", "1", data).await;
213        state.clear_entities().await;
214        assert!(state.get_entity("T", "1").await.is_none());
215    }
216}