Skip to main content

allframe_tauri/
lib.rs

1//! Tauri 2.x plugin for AllFrame
2//!
3//! Exposes AllFrame Router handlers as Tauri IPC commands for desktop apps.
4//! Designed for offline-first deployments where HTTP transport is unnecessary.
5//!
6//! # Rust (Tauri app)
7//!
8//! ```rust,ignore
9//! use allframe_core::router::Router;
10//!
11//! fn main() {
12//!     let mut router = Router::new();
13//!     router.register("get_user", || async {
14//!         r#"{"id":1,"name":"Alice"}"#.to_string()
15//!     });
16//!
17//!     tauri::Builder::default()
18//!         .plugin(allframe_tauri::init(router))
19//!         .run(tauri::generate_context!())
20//!         .unwrap();
21//! }
22//! ```
23//!
24//! # Frontend (TypeScript)
25//!
26//! ```text
27//! import { invoke } from "@tauri-apps/api/core";
28//!
29//! // List available handlers
30//! const handlers = await invoke("plugin:allframe-tauri|allframe_list");
31//!
32//! // Call a handler
33//! const result = await invoke("plugin:allframe-tauri|allframe_call", {
34//!     handler: "get_user",
35//!     args: { id: 42 }
36//! });
37//! ```
38//!
39//! # In-Process Dispatch (Local LLM / Ollama)
40//!
41//! `TauriServer` also supports direct in-process calls without Tauri runtime,
42//! useful for local LLM integration without opening a network port:
43//!
44//! ```rust
45//! use allframe_core::router::Router;
46//! use allframe_tauri::TauriServer;
47//!
48//! # async fn example() {
49//! let mut router = Router::new();
50//! router.register("skill", || async { "done".to_string() });
51//!
52//! let server = TauriServer::new(router);
53//! let result = server.call_handler("skill", "{}").await.unwrap();
54//! assert_eq!(result.result, "done");
55//! # }
56//! ```
57
58pub mod boot;
59pub mod commands;
60pub mod error;
61pub mod plugin;
62pub mod server;
63pub mod types;
64
65pub use allframe_core::router::StreamReceiver;
66pub use boot::{BootBuilder, BootContext, BootError, BootProgress};
67pub use error::TauriServerError;
68pub use plugin::{builder, init, init_with_state, PLUGIN_NAME};
69pub use server::TauriServer;
70pub use types::{CallResponse, HandlerInfo, HandlerKind, StreamStartResponse};
71
72#[cfg(test)]
73mod tests {
74    use allframe_core::router::Router;
75
76    use super::*;
77
78    #[test]
79    fn test_server_creation() {
80        let router = Router::new();
81        let server = TauriServer::new(router);
82        assert_eq!(server.handler_count(), 0);
83    }
84
85    #[test]
86    fn test_server_discovers_handlers() {
87        let mut router = Router::new();
88        router.register("get_user", || async { "User data".to_string() });
89        router.register("create_user", || async { "Created".to_string() });
90        router.register("delete_user", || async { "Deleted".to_string() });
91
92        let server = TauriServer::new(router);
93        assert_eq!(server.handler_count(), 3);
94    }
95
96    #[test]
97    fn test_handler_info_fields() {
98        let mut router = Router::new();
99        router.register("my_handler", || async { "result".to_string() });
100
101        let server = TauriServer::new(router);
102        let handlers = server.list_handlers();
103
104        assert_eq!(handlers.len(), 1);
105        assert_eq!(handlers[0].name, "my_handler");
106        assert!(!handlers[0].description.is_empty());
107    }
108
109    #[tokio::test]
110    async fn test_call_handler_success() {
111        let mut router = Router::new();
112        router.register("echo", || async { "echoed".to_string() });
113
114        let server = TauriServer::new(router);
115        let result = server.call_handler("echo", "{}").await;
116
117        assert!(result.is_ok());
118        assert_eq!(result.unwrap().result, "echoed");
119    }
120
121    #[tokio::test]
122    async fn test_call_handler_not_found() {
123        let router = Router::new();
124        let server = TauriServer::new(router);
125
126        let result = server.call_handler("missing", "{}").await;
127        assert!(result.is_err());
128
129        let err = result.unwrap_err();
130        match err {
131            TauriServerError::HandlerNotFound(name) => {
132                assert_eq!(name, "missing");
133            }
134            other => panic!("Expected HandlerNotFound, got: {other:?}"),
135        }
136    }
137
138    #[tokio::test]
139    async fn test_multiple_calls() {
140        let mut router = Router::new();
141        router.register("a", || async { "A".to_string() });
142        router.register("b", || async { "B".to_string() });
143
144        let server = TauriServer::new(router);
145
146        let a = server.call_handler("a", "{}").await.unwrap();
147        let b = server.call_handler("b", "{}").await.unwrap();
148
149        assert_eq!(a.result, "A");
150        assert_eq!(b.result, "B");
151    }
152
153    #[tokio::test]
154    async fn test_handler_isolation() {
155        let mut router = Router::new();
156        router.register("x", || async { "X".to_string() });
157        router.register("y", || async { "Y".to_string() });
158
159        let server = TauriServer::new(router);
160
161        let _ = server.call_handler("x", "{}").await;
162        let y = server.call_handler("y", "{}").await.unwrap();
163        assert_eq!(y.result, "Y");
164    }
165
166    #[test]
167    fn test_empty_router() {
168        let router = Router::new();
169        let server = TauriServer::new(router);
170        assert_eq!(server.handler_count(), 0);
171        assert!(server.list_handlers().is_empty());
172    }
173
174    #[tokio::test]
175    async fn test_list_empty() {
176        let router = Router::new();
177        let server = TauriServer::new(router);
178        assert!(server.list_handlers().is_empty());
179    }
180
181    #[test]
182    fn test_call_response_serialization() {
183        let response = CallResponse {
184            result: r#"{"id":1}"#.to_string(),
185        };
186        let json = serde_json::to_string(&response).unwrap();
187        assert!(json.contains("id"));
188    }
189
190    #[test]
191    fn test_handler_info_serialization() {
192        let info = HandlerInfo {
193            name: "test".to_string(),
194            description: "A test handler".to_string(),
195            kind: HandlerKind::RequestResponse,
196        };
197        let json = serde_json::to_string(&info).unwrap();
198        assert!(json.contains("test"));
199        assert!(json.contains("A test handler"));
200        assert!(json.contains("request_response"));
201    }
202
203    #[test]
204    fn test_error_serialization() {
205        let err = TauriServerError::HandlerNotFound("missing".to_string());
206        let json = serde_json::to_string(&err).unwrap();
207        assert!(json.contains("missing"));
208    }
209
210    #[tokio::test]
211    async fn test_typed_args_flow_through_tauri_server() {
212        #[derive(serde::Deserialize)]
213        struct Input {
214            name: String,
215        }
216
217        let mut router = Router::new();
218        router.register_with_args("greet", |args: Input| async move {
219            format!(r#"{{"greeting":"Hello {}"}}"#, args.name)
220        });
221
222        let server = TauriServer::new(router);
223        let result = server
224            .call_handler("greet", r#"{"name":"Alice"}"#)
225            .await
226            .unwrap();
227
228        assert_eq!(result.result, r#"{"greeting":"Hello Alice"}"#);
229    }
230
231    #[tokio::test]
232    async fn test_tauri_compat_handler_through_server() {
233        use allframe_macros::tauri_compat;
234
235        #[tauri_compat]
236        async fn greet(name: String, age: u32) -> String {
237            format!(r#"{{"greeting":"Hello {}, age {}"}}"#, name, age)
238        }
239
240        let mut router = Router::new();
241        router.register_with_args::<GreetArgs, _, _>("greet", greet);
242
243        let server = TauriServer::new(router);
244        let result = server
245            .call_handler("greet", r#"{"name":"Alice","age":30}"#)
246            .await
247            .unwrap();
248
249        assert_eq!(result.result, r#"{"greeting":"Hello Alice, age 30"}"#);
250    }
251
252    #[tokio::test]
253    async fn test_tauri_compat_optional_param() {
254        use allframe_macros::tauri_compat;
255
256        #[tauri_compat]
257        async fn greet_optional(name: String, title: Option<String>) -> String {
258            match title {
259                Some(t) => format!("{} {}", t, name),
260                None => name,
261            }
262        }
263
264        let mut router = Router::new();
265        router
266            .register_with_args::<GreetOptionalArgs, _, _>("greet_optional", greet_optional);
267
268        let server = TauriServer::new(router);
269
270        // With optional param
271        let result = server
272            .call_handler("greet_optional", r#"{"name":"Alice","title":"Dr."}"#)
273            .await
274            .unwrap();
275        assert_eq!(result.result, "Dr. Alice");
276
277        // Without optional param
278        let result = server
279            .call_handler("greet_optional", r#"{"name":"Bob"}"#)
280            .await
281            .unwrap();
282        assert_eq!(result.result, "Bob");
283    }
284
285    #[tokio::test]
286    async fn test_typed_return_struct() {
287        #[derive(serde::Serialize)]
288        struct UserResponse {
289            id: u32,
290            name: String,
291        }
292
293        let mut router = Router::new();
294        router.register_typed("get_user", || async {
295            UserResponse {
296                id: 1,
297                name: "Alice".to_string(),
298            }
299        });
300
301        let server = TauriServer::new(router);
302        let result = server.call_handler("get_user", "{}").await.unwrap();
303        assert_eq!(result.result, r#"{"id":1,"name":"Alice"}"#);
304    }
305
306    #[tokio::test]
307    async fn test_typed_return_with_args() {
308        #[derive(serde::Deserialize)]
309        struct Input {
310            x: i32,
311        }
312
313        #[derive(serde::Serialize)]
314        struct Output {
315            doubled: i32,
316        }
317
318        let mut router = Router::new();
319        router.register_typed_with_args("double", |args: Input| async move {
320            Output { doubled: args.x * 2 }
321        });
322
323        let server = TauriServer::new(router);
324        let result = server.call_handler("double", r#"{"x":21}"#).await.unwrap();
325        assert_eq!(result.result, r#"{"doubled":42}"#);
326    }
327
328    #[tokio::test]
329    async fn test_result_handler_ok() {
330        #[derive(serde::Serialize)]
331        struct Data {
332            value: i32,
333        }
334
335        let mut router = Router::new();
336        router.register_result("get_data", || async {
337            Ok::<_, String>(Data { value: 42 })
338        });
339
340        let server = TauriServer::new(router);
341        let result = server.call_handler("get_data", "{}").await.unwrap();
342        assert_eq!(result.result, r#"{"value":42}"#);
343    }
344
345    #[tokio::test]
346    async fn test_result_handler_err() {
347        #[derive(serde::Serialize)]
348        struct Data {
349            value: i32,
350        }
351
352        let mut router = Router::new();
353        router.register_result("fail", || async {
354            Err::<Data, String>("not found".to_string())
355        });
356
357        let server = TauriServer::new(router);
358        let result = server.call_handler("fail", "{}").await;
359        assert!(result.is_err());
360    }
361
362    #[tokio::test]
363    async fn test_typed_return_with_state() {
364        use allframe_core::router::State;
365        use std::sync::Arc;
366
367        struct AppState {
368            prefix: String,
369        }
370
371        #[derive(serde::Deserialize)]
372        struct Input {
373            name: String,
374        }
375
376        #[derive(serde::Serialize)]
377        struct Greeting {
378            message: String,
379        }
380
381        let mut router = Router::new().with_state(AppState {
382            prefix: "Hey".to_string(),
383        });
384        router.register_typed_with_state::<AppState, Input, Greeting, _, _>(
385            "greet",
386            |state: State<Arc<AppState>>, args: Input| async move {
387                Greeting {
388                    message: format!("{} {}", state.prefix, args.name),
389                }
390            },
391        );
392
393        let server = TauriServer::new(router);
394        let result = server
395            .call_handler("greet", r#"{"name":"Dave"}"#)
396            .await
397            .unwrap();
398        assert_eq!(result.result, r#"{"message":"Hey Dave"}"#);
399    }
400
401    #[tokio::test]
402    async fn test_tauri_compat_no_args() {
403        use allframe_macros::tauri_compat;
404
405        #[tauri_compat]
406        async fn health_check() -> String {
407            "ok".to_string()
408        }
409
410        let mut router = Router::new();
411        router.register("health_check", health_check);
412
413        let server = TauriServer::new(router);
414        let result = server.call_handler("health_check", "{}").await.unwrap();
415        assert_eq!(result.result, "ok");
416    }
417
418    #[tokio::test]
419    async fn test_state_injection_through_tauri_server() {
420        use allframe_core::router::State;
421        use std::sync::Arc;
422
423        struct AppState {
424            prefix: String,
425        }
426
427        #[derive(serde::Deserialize)]
428        struct Input {
429            name: String,
430        }
431
432        let mut router = Router::new().with_state(AppState {
433            prefix: "Hey".to_string(),
434        });
435        router.register_with_state::<AppState, Input, _, _>(
436            "greet",
437            |state: State<Arc<AppState>>, args: Input| async move {
438                format!("{} {}", state.prefix, args.name)
439            },
440        );
441
442        let server = TauriServer::new(router);
443        let result = server
444            .call_handler("greet", r#"{"name":"Bob"}"#)
445            .await
446            .unwrap();
447
448        assert_eq!(result.result, "Hey Bob");
449    }
450
451    #[tokio::test]
452    async fn test_tauri_compat_streaming_with_args() {
453        use allframe_core::router::StreamSender;
454        use allframe_macros::tauri_compat;
455
456        #[tauri_compat(streaming)]
457        async fn stream_greet(name: String, count: u32, tx: StreamSender) -> String {
458            for i in 0..count {
459                tx.send(format!("Hello {} #{}", name, i)).await.ok();
460            }
461            "done".to_string()
462        }
463
464        let mut router = Router::new();
465        router.register_streaming_with_args::<StreamGreetArgs, _, _, _>(
466            "stream_greet",
467            stream_greet,
468        );
469
470        let server = TauriServer::new(router);
471        let (mut rx, handle) = server
472            .call_streaming_handler("stream_greet", r#"{"name":"Alice","count":2}"#)
473            .unwrap();
474
475        let msg1 = rx.recv().await.unwrap();
476        assert_eq!(msg1, "Hello Alice #0");
477        let msg2 = rx.recv().await.unwrap();
478        assert_eq!(msg2, "Hello Alice #1");
479
480        let response = handle.await.unwrap().unwrap();
481        assert_eq!(response.result, "done");
482    }
483
484    /// Tests the inject_state path that the Tauri plugin uses to inject AppHandle.
485    /// We simulate it with a plain struct since we can't construct AppHandle in tests.
486    #[tokio::test]
487    async fn test_inject_state_simulates_app_handle_injection() {
488        use allframe_core::router::State;
489        use std::sync::Arc;
490
491        // Simulates what plugin::init does: inject_state after router construction
492        struct FakeAppHandle {
493            app_name: String,
494        }
495        struct DbPool {
496            url: String,
497        }
498
499        let mut router = Router::new().with_state(DbPool {
500            url: "sqlite://db".to_string(),
501        });
502
503        // Register handler that needs the "app handle"
504        router.register_with_state_only::<FakeAppHandle, _, _>(
505            "emit_event",
506            |app: State<Arc<FakeAppHandle>>| async move {
507                format!("emitted from {}", app.app_name)
508            },
509        );
510
511        // Register handler that needs the db pool
512        router.register_with_state_only::<DbPool, _, _>(
513            "db_url",
514            |db: State<Arc<DbPool>>| async move { db.url.clone() },
515        );
516
517        // Simulate what the plugin setup closure does
518        router.inject_state(FakeAppHandle {
519            app_name: "MyTauriApp".to_string(),
520        });
521
522        let server = TauriServer::new(router);
523
524        let result = server.call_handler("emit_event", "{}").await.unwrap();
525        assert_eq!(result.result, "emitted from MyTauriApp");
526
527        let result = server.call_handler("db_url", "{}").await.unwrap();
528        assert_eq!(result.result, "sqlite://db");
529    }
530
531    #[tokio::test]
532    async fn test_tauri_compat_streaming_no_args() {
533        use allframe_core::router::StreamSender;
534        use allframe_macros::tauri_compat;
535
536        #[tauri_compat(streaming)]
537        async fn stream_ping(tx: StreamSender) -> String {
538            tx.send("pong".to_string()).await.ok();
539            "done".to_string()
540        }
541
542        let mut router = Router::new();
543        router.register_streaming("stream_ping", stream_ping);
544
545        let server = TauriServer::new(router);
546        let (mut rx, handle) = server
547            .call_streaming_handler("stream_ping", "{}")
548            .unwrap();
549
550        let msg = rx.recv().await.unwrap();
551        assert_eq!(msg, "pong");
552
553        let response = handle.await.unwrap().unwrap();
554        assert_eq!(response.result, "done");
555    }
556}