entity_graph_tauri/
lib.rs1pub 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
53pub(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
99pub 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
110pub 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
138pub struct EntityGraphPlugin;
140
141impl EntityGraphPlugin {
142 pub fn new<R: Runtime>() -> TauriPlugin<R> {
144 init()
145 }
146}
147
148#[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}