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}