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
131        for route in &self.routes {
132            if let Some(path_params) = parsed_url.match_pattern(&route.pattern) {
133                // Route matched! Build enriched request
134                let enriched_request = EnrichedRequest::new(
135                    ipc_request,
136                    parsed_url.path.clone(),
137                    path_params,
138                    parsed_url.query_params.clone(),
139                );
140
141                // Call handler
142                return match route.handler.handle(&enriched_request) {
143                    Ok(response) => response,
144                    Err(err) => err.into(),
145                };
146            }
147        }
148
149        // No route matched
150        IpcResponse::not_found(&parsed_url.path)
151    }
152
153    /// Parse raw JSON request from JavaScript into IpcRequest
154    fn parse_raw_request(&self, raw: &Value) -> Result<IpcRequest, IpcError> {
155        let obj = raw.as_object().ok_or_else(|| {
156            IpcError::BadRequest("Request must be an object".to_string())
157        })?;
158
159        let id = obj
160            .get("id")
161            .and_then(|v| v.as_u64())
162            .ok_or_else(|| IpcError::BadRequest("Missing 'id' field".to_string()))?;
163
164        let method = obj
165            .get("method")
166            .and_then(|v| v.as_str())
167            .unwrap_or("GET")
168            .to_string();
169
170        let url = obj
171            .get("url")
172            .and_then(|v| v.as_str())
173            .ok_or_else(|| IpcError::BadRequest("Missing 'url' field".to_string()))?
174            .to_string();
175
176        // Parse headers
177        let headers = if let Some(headers_obj) = obj.get("headers").and_then(|v| v.as_object()) {
178            headers_obj
179                .iter()
180                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
181                .collect()
182        } else {
183            HashMap::new()
184        };
185
186        // Parse body if present
187        let body = if let Some(body_value) = obj.get("body") {
188            let content_type = headers
189                .get("Content-Type")
190                .map(|s| s.as_str())
191                .unwrap_or("application/json");
192
193            if body_value.is_string() {
194                // Body is a string - parse based on Content-Type
195                let body_str = body_value.as_str().unwrap();
196                Some(parse_body(content_type, body_str)?)
197            } else {
198                // Body is already JSON object
199                Some(RequestBody::Json(body_value.clone()))
200            }
201        } else {
202            None
203        };
204
205        Ok(IpcRequest {
206            id,
207            method,
208            url,
209            headers,
210            body,
211        })
212    }
213
214    /// Returns a list of all registered routes
215    ///
216    /// Useful for debugging and introspection.
217    pub fn list_routes(&self) -> Vec<(String, String)> {
218        self.routes
219            .iter()
220            .map(|route| (route.method.clone(), route.pattern.clone()))
221            .collect()
222    }
223
224    /// Start the IPC message handler loop
225    ///
226    /// This method sets up the eval channel and automatically handles incoming
227    /// IPC requests, dispatching them to registered routes and sending responses back.
228    ///
229    /// # Example
230    /// ```rust,ignore
231    /// use dioxus::prelude::*;
232    /// use dioxus_ipc_bridge::prelude::*;
233    ///
234    /// fn app() -> Element {
235    ///     let router = use_signal(|| {
236    ///         let mut r = IpcRouter::new();
237    ///         r.register("POST", "/greeting", Box::new(GreetingHandler));
238    ///         r
239    ///     });
240    ///
241    ///     use_effect(move || {
242    ///         router.read().start();
243    ///     });
244    ///
245    ///     rsx! { /* ... */ }
246    /// }
247    /// ```
248    pub fn start(&self) {
249        let ipc_router = self.clone();
250
251        #[cfg(not(target_arch = "wasm32"))]
252        {
253            use dioxus::prelude::*;
254            spawn(async move {
255                let mut eval = dioxus::document::eval("window.dioxus = dioxus;");
256
257                loop {
258                    match eval.recv::<Value>().await {
259                        Ok(msg) => {
260                            println!("📨 IPC Router received: {:?}", msg);
261
262                            // Dispatch to router
263                            let response = ipc_router.dispatch(&msg);
264
265                            // Send response back via callback
266                            if let Some(request_id) = msg.get("id").and_then(|v| v.as_i64()) {
267                                let script = format!(
268                                    r#"
269                                    if (window.dioxusBridge && window.dioxusBridge.callbacks) {{
270                                        const callback = window.dioxusBridge.callbacks.get({});
271                                        if (callback) {{
272                                            callback.resolve({});
273                                            window.dioxusBridge.callbacks.delete({});
274                                        }}
275                                    }}
276                                    "#,
277                                    request_id,
278                                    serde_json::to_string(&response).unwrap_or_else(|_| "null".to_string()),
279                                    request_id
280                                );
281
282                                let _ = dioxus::document::eval(&script);
283                            }
284                        }
285                        Err(e) => {
286                            eprintln!("❌ IPC Router error: {:?}", e);
287                            break;
288                        }
289                    }
290                }
291            });
292        }
293
294        #[cfg(target_arch = "wasm32")]
295        {
296            use dioxus::prelude::*;
297            use wasm_bindgen::prelude::*;
298
299            spawn(async move {
300                // WASM-specific implementation
301                let eval_result = dioxus::document::eval("window.dioxus = dioxus;");
302                let mut eval = eval_result;
303
304                loop {
305                    match eval.recv::<Value>().await {
306                        Ok(msg) => {
307                            // Dispatch to router
308                            let response = ipc_router.dispatch(&msg);
309
310                            // Send response back
311                            if let Some(request_id) = msg.get("id").and_then(|v| v.as_i64()) {
312                                let script = format!(
313                                    r#"
314                                    if (window.dioxusBridge && window.dioxusBridge.callbacks) {{
315                                        const callback = window.dioxusBridge.callbacks.get({});
316                                        if (callback) {{
317                                            callback.resolve({});
318                                            window.dioxusBridge.callbacks.delete({});
319                                        }}
320                                    }}
321                                    "#,
322                                    request_id,
323                                    serde_json::to_string(&response).unwrap_or_else(|_| "null".to_string()),
324                                    request_id
325                                );
326
327                                let _ = dioxus::document::eval(&script);
328                            }
329                        }
330                        Err(e) => {
331                            web_sys::console::error_1(&format!("IPC Router error: {:?}", e).into());
332                            break;
333                        }
334                    }
335                }
336            });
337        }
338    }
339}
340
341impl Default for IpcRouter {
342    fn default() -> Self {
343        Self::new()
344    }
345}
346
347/// Builder for IpcRouter
348pub struct IpcRouterBuilder {
349    routes: Vec<(String, String, Box<dyn RouteHandler>)>,
350}
351
352impl IpcRouterBuilder {
353    /// Create a new router builder
354    pub fn new() -> Self {
355        Self { routes: Vec::new() }
356    }
357
358    /// Add a route to the router
359    ///
360    /// # Arguments
361    /// * `method` - HTTP method (e.g., "GET", "POST")
362    /// * `pattern` - URL pattern (e.g., "/user/:id")
363    /// * `handler` - Route handler
364    ///
365    /// # Example
366    /// ```rust,ignore
367    /// use dioxus_ipc_bridge::prelude::*;
368    ///
369    /// let router = IpcRouter::builder()
370    ///     .route("GET", "/hello/:name", Box::new(HelloHandler))
371    ///     .route("POST", "/submit", Box::new(SubmitHandler))
372    ///     .build();
373    /// ```
374    pub fn route(mut self, method: &str, pattern: &str, handler: Box<dyn RouteHandler>) -> Self {
375        self.routes
376            .push((method.to_string(), pattern.to_string(), handler));
377        self
378    }
379
380    /// Build the IpcRouter
381    pub fn build(self) -> IpcRouter {
382        let mut router = IpcRouter::new();
383        for (method, pattern, handler) in self.routes {
384            router.register(&method, &pattern, handler);
385        }
386        router
387    }
388}
389
390impl Default for IpcRouterBuilder {
391    fn default() -> Self {
392        Self::new()
393    }
394}