Skip to main content

deckyfx_dioxus_ipc_bridge/
router.rs

1//! HTTP-like IPC Router
2//!
3//! Provides URL-based routing system for IPC communication between JavaScript and Rust.
4//! Supports path parameters, query strings, and multiple body formats.
5
6use crate::parser::{parse_body, ParsedUrl};
7use crate::request::{EnrichedRequest, IpcRequest, RequestBody};
8use crate::response::{IpcError, IpcResponse};
9use serde_json::Value;
10use std::collections::HashMap;
11use std::sync::Arc;
12
13/// Trait for implementing HTTP-style route handlers
14///
15/// Implement this trait to create custom route handlers that can be
16/// registered with the IpcRouter.
17///
18/// # Example
19/// ```rust,no_run
20/// use dioxus_ipc_bridge::prelude::*;
21///
22/// struct UserHandler;
23///
24/// impl RouteHandler for UserHandler {
25///     fn handle(&self, req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
26///         let user_id = req.path_param("id").ok_or(IpcError::BadRequest("Missing ID".into()))?;
27///         Ok(IpcResponse::ok(serde_json::json!({
28///             "user_id": user_id
29///         })))
30///     }
31/// }
32/// ```
33pub trait RouteHandler: Send + Sync {
34    /// Handles the incoming HTTP-like IPC request
35    ///
36    /// # Arguments
37    /// * `req` - Enriched IPC request with parsed URL, path params, query params, and body
38    ///
39    /// # Returns
40    /// * `Ok(IpcResponse)` - Success with HTTP-like response
41    /// * `Err(IpcError)` - Error during processing
42    fn handle(&self, req: &EnrichedRequest) -> Result<IpcResponse, IpcError>;
43}
44
45/// Route definition
46struct Route {
47    /// HTTP method (informational)
48    method: String,
49
50    /// Path pattern with optional parameters (e.g., "/user/:id/posts/:postId")
51    pattern: String,
52
53    /// Handler for this route
54    handler: Arc<dyn RouteHandler>,
55}
56
57impl Clone for Route {
58    fn clone(&self) -> Self {
59        Self {
60            method: self.method.clone(),
61            pattern: self.pattern.clone(),
62            handler: Arc::clone(&self.handler),
63        }
64    }
65}
66
67/// HTTP-like IPC Router
68///
69/// Routes incoming IPC requests to appropriate handlers based on URL patterns.
70/// Supports static paths, path parameters, query strings, and multiple body formats.
71#[derive(Clone)]
72pub struct IpcRouter {
73    /// Registered routes
74    routes: Vec<Route>,
75}
76
77impl IpcRouter {
78    /// Creates a new empty IPC router
79    pub fn new() -> Self {
80        Self {
81            routes: Vec::new(),
82        }
83    }
84
85    /// Create a new router builder
86    pub fn builder() -> IpcRouterBuilder {
87        IpcRouterBuilder::new()
88    }
89
90    /// Registers a new route handler
91    ///
92    /// # Arguments
93    /// * `method` - HTTP method (informational, e.g., "GET", "POST")
94    /// * `pattern` - URL pattern with optional parameters (e.g., "/user/:id")
95    /// * `handler` - Boxed handler implementing RouteHandler trait
96    ///
97    /// # Example
98    /// ```rust,ignore
99    /// router.register("POST", "/form/submit", Box::new(FormSubmitHandler));
100    /// router.register("GET", "/user/:id", Box::new(GetUserHandler));
101    /// ```
102    pub fn register(&mut self, method: &str, pattern: &str, handler: Box<dyn RouteHandler>) {
103        self.routes.push(Route {
104            method: method.to_string(),
105            pattern: pattern.to_string(),
106            handler: Arc::from(handler),
107        });
108    }
109
110    /// Dispatches an IPC request to the appropriate route handler
111    ///
112    /// # Arguments
113    /// * `raw_request` - Raw IPC request from JavaScript
114    ///
115    /// # Returns
116    /// * `IpcResponse` - HTTP-like response (includes 404 if no route matches)
117    pub fn dispatch(&self, raw_request: &Value) -> IpcResponse {
118        // Parse raw request
119        let ipc_request = match self.parse_raw_request(raw_request) {
120            Ok(req) => req,
121            Err(err) => return err.into(),
122        };
123
124        // Parse URL
125        let parsed_url = match ParsedUrl::parse(&ipc_request.url) {
126            Ok(url) => url,
127            Err(err) => return err.into(),
128        };
129
130        // Find matching route (checks both method and path pattern)
131        for route in &self.routes {
132            if !route.method.eq_ignore_ascii_case(&ipc_request.method) {
133                continue;
134            }
135            if let Some(path_params) = parsed_url.match_pattern(&route.pattern) {
136                // Route matched! Build enriched request
137                let enriched_request = EnrichedRequest::new(
138                    ipc_request,
139                    parsed_url.path.clone(),
140                    path_params,
141                    parsed_url.query_params.clone(),
142                );
143
144                // Call handler
145                return match route.handler.handle(&enriched_request) {
146                    Ok(response) => response,
147                    Err(err) => err.into(),
148                };
149            }
150        }
151
152        // No route matched
153        IpcResponse::not_found(&format!("{} {}", ipc_request.method, parsed_url.path))
154    }
155
156    /// Parse raw JSON request from JavaScript into IpcRequest
157    fn parse_raw_request(&self, raw: &Value) -> Result<IpcRequest, IpcError> {
158        let obj = raw.as_object().ok_or_else(|| {
159            IpcError::BadRequest("Request must be an object".to_string())
160        })?;
161
162        let id = obj
163            .get("id")
164            .and_then(|v| v.as_u64())
165            .ok_or_else(|| IpcError::BadRequest("Missing 'id' field".to_string()))?;
166
167        let method = obj
168            .get("method")
169            .and_then(|v| v.as_str())
170            .unwrap_or("GET")
171            .to_string();
172
173        let url = obj
174            .get("url")
175            .and_then(|v| v.as_str())
176            .ok_or_else(|| IpcError::BadRequest("Missing 'url' field".to_string()))?
177            .to_string();
178
179        // Parse headers
180        let headers = if let Some(headers_obj) = obj.get("headers").and_then(|v| v.as_object()) {
181            headers_obj
182                .iter()
183                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
184                .collect()
185        } else {
186            HashMap::new()
187        };
188
189        // Parse body if present
190        let body = if let Some(body_value) = obj.get("body") {
191            let content_type = headers
192                .get("Content-Type")
193                .map(|s| s.as_str())
194                .unwrap_or("application/json");
195
196            if body_value.is_string() {
197                // Body is a string - parse based on Content-Type
198                let body_str = body_value.as_str().unwrap();
199                Some(parse_body(content_type, body_str)?)
200            } else {
201                // Body is already JSON object
202                Some(RequestBody::Json(body_value.clone()))
203            }
204        } else {
205            None
206        };
207
208        Ok(IpcRequest {
209            id,
210            method,
211            url,
212            headers,
213            body,
214        })
215    }
216
217    /// Returns a list of all registered routes
218    ///
219    /// Useful for debugging and introspection.
220    pub fn list_routes(&self) -> Vec<(String, String)> {
221        self.routes
222            .iter()
223            .map(|route| (route.method.clone(), route.pattern.clone()))
224            .collect()
225    }
226
227    /// Start the IPC message handler loop
228    ///
229    /// This method sets up the eval channel and automatically handles incoming
230    /// IPC requests, dispatching them to registered routes and sending responses back.
231    ///
232    /// # Example
233    /// ```rust,ignore
234    /// use dioxus::prelude::*;
235    /// use dioxus_ipc_bridge::prelude::*;
236    ///
237    /// fn app() -> Element {
238    ///     let router = use_signal(|| {
239    ///         let mut r = IpcRouter::new();
240    ///         r.register("POST", "/greeting", Box::new(GreetingHandler));
241    ///         r
242    ///     });
243    ///
244    ///     use_effect(move || {
245    ///         router.read().start();
246    ///     });
247    ///
248    ///     rsx! { /* ... */ }
249    /// }
250    /// ```
251    pub fn start(&self) {
252        let ipc_router = self.clone();
253
254        #[cfg(not(target_arch = "wasm32"))]
255        {
256            use dioxus::prelude::*;
257            spawn(async move {
258                let mut eval = dioxus::document::eval("window.dioxus = dioxus;");
259
260                loop {
261                    match eval.recv::<Value>().await {
262                        Ok(msg) => {
263                            println!("📨 IPC Router received: {:?}", msg);
264
265                            // Dispatch to router
266                            let response = ipc_router.dispatch(&msg);
267
268                            // Send response back via callback
269                            if let Some(request_id) = msg.get("id").and_then(|v| v.as_i64()) {
270                                let script = format!(
271                                    r#"
272                                    if (window.dioxusBridge && window.dioxusBridge.callbacks) {{
273                                        const callback = window.dioxusBridge.callbacks.get({});
274                                        if (callback) {{
275                                            callback.resolve({});
276                                            window.dioxusBridge.callbacks.delete({});
277                                        }}
278                                    }}
279                                    "#,
280                                    request_id,
281                                    serde_json::to_string(&response).unwrap_or_else(|_| "null".to_string()),
282                                    request_id
283                                );
284
285                                let _ = dioxus::document::eval(&script);
286                            }
287                        }
288                        Err(e) => {
289                            eprintln!("❌ IPC Router error: {:?}", e);
290                            break;
291                        }
292                    }
293                }
294            });
295        }
296
297        #[cfg(target_arch = "wasm32")]
298        {
299            use dioxus::prelude::*;
300            use wasm_bindgen::prelude::*;
301
302            spawn(async move {
303                // WASM-specific implementation
304                let eval_result = dioxus::document::eval("window.dioxus = dioxus;");
305                let mut eval = eval_result;
306
307                loop {
308                    match eval.recv::<Value>().await {
309                        Ok(msg) => {
310                            // Dispatch to router
311                            let response = ipc_router.dispatch(&msg);
312
313                            // Send response back
314                            if let Some(request_id) = msg.get("id").and_then(|v| v.as_i64()) {
315                                let script = format!(
316                                    r#"
317                                    if (window.dioxusBridge && window.dioxusBridge.callbacks) {{
318                                        const callback = window.dioxusBridge.callbacks.get({});
319                                        if (callback) {{
320                                            callback.resolve({});
321                                            window.dioxusBridge.callbacks.delete({});
322                                        }}
323                                    }}
324                                    "#,
325                                    request_id,
326                                    serde_json::to_string(&response).unwrap_or_else(|_| "null".to_string()),
327                                    request_id
328                                );
329
330                                let _ = dioxus::document::eval(&script);
331                            }
332                        }
333                        Err(e) => {
334                            web_sys::console::error_1(&format!("IPC Router error: {:?}", e).into());
335                            break;
336                        }
337                    }
338                }
339            });
340        }
341    }
342}
343
344impl Default for IpcRouter {
345    fn default() -> Self {
346        Self::new()
347    }
348}
349
350/// Builder for IpcRouter
351pub struct IpcRouterBuilder {
352    routes: Vec<(String, String, Box<dyn RouteHandler>)>,
353}
354
355impl IpcRouterBuilder {
356    /// Create a new router builder
357    pub fn new() -> Self {
358        Self { routes: Vec::new() }
359    }
360
361    /// Add a route to the router
362    ///
363    /// # Arguments
364    /// * `method` - HTTP method (e.g., "GET", "POST")
365    /// * `pattern` - URL pattern (e.g., "/user/:id")
366    /// * `handler` - Route handler
367    ///
368    /// # Example
369    /// ```rust,ignore
370    /// use dioxus_ipc_bridge::prelude::*;
371    ///
372    /// let router = IpcRouter::builder()
373    ///     .route("GET", "/hello/:name", Box::new(HelloHandler))
374    ///     .route("POST", "/submit", Box::new(SubmitHandler))
375    ///     .build();
376    /// ```
377    pub fn route(mut self, method: &str, pattern: &str, handler: Box<dyn RouteHandler>) -> Self {
378        self.routes
379            .push((method.to_string(), pattern.to_string(), handler));
380        self
381    }
382
383    /// Build the IpcRouter
384    pub fn build(self) -> IpcRouter {
385        let mut router = IpcRouter::new();
386        for (method, pattern, handler) in self.routes {
387            router.register(&method, &pattern, handler);
388        }
389        router
390    }
391}
392
393impl Default for IpcRouterBuilder {
394    fn default() -> Self {
395        Self::new()
396    }
397}