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}