Skip to main content

gpui_navigator/
route.rs

1//! Route definition and configuration.
2//!
3//! This module is the core of the routing system. It provides:
4//!
5//! - [`Route`] — a route with a path pattern, builder function, children, guards,
6//!   middleware, lifecycle hooks, and transitions.
7//! - [`RouteConfig`] — lightweight configuration (path, name, metadata) without
8//!   a builder or feature-gated fields.
9//! - [`NamedRouteRegistry`] — a registry that maps human-readable names to path
10//!   patterns for reverse URL generation.
11//! - [`PageRoute`], [`NamedRoute`], and the [`IntoRoute`] trait — flexible
12//!   navigation descriptors accepted by `Navigator::push`.
13//!
14//! # Route creation
15//!
16//! Routes are created via constructor methods on [`Route`]:
17//!
18//! | Constructor | Use case |
19//! |-------------|----------|
20//! | [`Route::new`] | Full control — receives `Window`, `App`, `RouteParams` |
21//! | [`Route::view`] | Stateless page — simple closure returning `AnyElement` |
22//! | [`Route::component`] | Stateful page — `Entity<T>` cached across navigations |
23//! | [`Route::component_with_params`] | Stateful page keyed by parameters |
24//!
25//! # Builder pattern
26//!
27//! After construction, routes are configured using a fluent builder API:
28//!
29//! ```no_run
30//! use gpui_navigator::{Route, render_router_outlet};
31//! use gpui::*;
32//!
33//! let route = Route::new("/dashboard", |window, cx, params| {
34//!         div()
35//!             .child("Dashboard")
36//!             .child(render_router_outlet(window, cx, None))
37//!             .into_any_element()
38//!     })
39//!     .name("dashboard")
40//!     .meta("title", "Dashboard")
41//!     .children(vec![
42//!         Route::new("", |_, _cx, _p| div().child("Overview").into_any_element()).into(),
43//!         Route::new("settings", |_, _cx, _p| div().child("Settings").into_any_element()).into(),
44//!     ]);
45//! ```
46//!
47//! # Sharing routes
48//!
49//! [`Route`] contains non-cloneable fields (guards, middleware, lifecycle hooks).
50//! Use [`RouteRef`] (`Arc<Route>`) to share routes cheaply across the route tree.
51
52#[cfg(feature = "guard")]
53use crate::guards::RouteGuard;
54use crate::lifecycle::RouteLifecycle;
55#[cfg(feature = "middleware")]
56use crate::middleware::RouteMiddleware;
57use crate::params::RouteParams;
58#[cfg(feature = "transition")]
59use crate::transition::TransitionConfig;
60use crate::{trace_log, warn_log, RouteMatch};
61use gpui::{AnyElement, AnyView, App, AppContext, BorrowAppContext, IntoElement, Render, Window};
62use std::collections::HashMap;
63use std::sync::Arc;
64
65// ============================================================================
66// NamedRouteRegistry
67// ============================================================================
68
69/// Registry that maps human-readable route names to path patterns.
70///
71/// Enables reverse URL generation: given a name and parameters, produce the
72/// concrete path. This decouples link targets from literal paths, so renaming
73/// a path only requires updating the route definition.
74///
75/// # Example
76///
77/// ```
78/// use gpui_navigator::{NamedRouteRegistry, RouteParams};
79///
80/// let mut registry = NamedRouteRegistry::new();
81/// registry.register("user.profile", "/users/:id/profile");
82///
83/// let mut params = RouteParams::new();
84/// params.set("id".to_string(), "42".to_string());
85///
86/// assert_eq!(registry.url_for("user.profile", &params), Some("/users/42/profile".to_string()));
87/// ```
88#[derive(Clone, Debug, Default)]
89pub struct NamedRouteRegistry {
90    /// Map of route names to path patterns
91    routes: HashMap<String, String>,
92}
93
94impl NamedRouteRegistry {
95    /// Create a new empty registry
96    #[must_use] 
97    pub fn new() -> Self {
98        Self::default()
99    }
100
101    /// Register a named route
102    pub fn register(&mut self, name: impl Into<String>, path: impl Into<String>) {
103        self.routes.insert(name.into(), path.into());
104    }
105
106    /// Get path pattern for a named route
107    pub fn get(&self, name: &str) -> Option<&str> {
108        self.routes.get(name).map(String::as_str)
109    }
110
111    /// Check if a route name exists
112    #[must_use] 
113    pub fn contains(&self, name: &str) -> bool {
114        self.routes.contains_key(name)
115    }
116
117    /// Generate URL for a named route with parameters
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// use gpui_navigator::{NamedRouteRegistry, RouteParams};
123    ///
124    /// let mut registry = NamedRouteRegistry::new();
125    /// registry.register("user.detail", "/users/:id");
126    ///
127    /// let mut params = RouteParams::new();
128    /// params.set("id".to_string(), "123".to_string());
129    ///
130    /// let url = registry.url_for("user.detail", &params).unwrap();
131    /// assert_eq!(url, "/users/123");
132    /// ```
133    #[must_use] 
134    pub fn url_for(&self, name: &str, params: &RouteParams) -> Option<String> {
135        let pattern = self.get(name)?;
136        Some(substitute_params(pattern, params))
137    }
138
139    /// Clear all registered routes
140    pub fn clear(&mut self) {
141        self.routes.clear();
142    }
143
144    /// Get number of registered routes
145    #[must_use] 
146    pub fn len(&self) -> usize {
147        self.routes.len()
148    }
149
150    /// Check if registry is empty
151    #[must_use] 
152    pub fn is_empty(&self) -> bool {
153        self.routes.is_empty()
154    }
155}
156
157/// Substitute route parameters in a path pattern
158///
159/// Replaces `:param` with actual values from `RouteParams`
160fn substitute_params(pattern: &str, params: &RouteParams) -> String {
161    let mut result = pattern.to_string();
162
163    // Replace :param with actual values
164    for (key, value) in params.iter() {
165        let placeholder = format!(":{key}");
166        result = result.replace(&placeholder, value);
167    }
168
169    result
170}
171
172// ============================================================================
173// Route Validation
174// ============================================================================
175
176/// Validate a route path pattern
177///
178/// Returns an error message if the path is invalid, None otherwise.
179///
180/// # Errors
181///
182/// Returns `Err(String)` if the path violates any of the following rules:
183///
184/// - Consecutive slashes (`//`) are not allowed
185/// - Parameter names (`:name`) must be non-empty and alphanumeric
186/// - Duplicate parameter names are not allowed
187///
188/// Empty paths are valid (index routes), and trailing slashes are permitted.
189pub fn validate_route_path(path: &str) -> Result<(), String> {
190    // Empty path is allowed for index routes
191    if path.is_empty() {
192        return Ok(());
193    }
194
195    // Consecutive slashes check
196    if path.contains("//") {
197        warn_log!("Invalid route path '{}': consecutive slashes", path);
198        return Err("Route path cannot contain consecutive slashes".to_string());
199    }
200
201    // Note: Trailing slashes are allowed for compatibility
202    // They are normalized during route matching
203
204    // Extract and validate parameters
205    let mut param_names = std::collections::HashSet::new();
206    for segment in path.split('/') {
207        if let Some(param) = segment.strip_prefix(':') {
208            // Check parameter name is not empty
209            if param.is_empty() {
210                warn_log!("Invalid route path '{}': empty parameter name", path);
211                return Err("Route parameter name cannot be empty".to_string());
212            }
213
214            // Check for constraint syntax (:id{uuid})
215            let param_name = param.find('{').map_or(param, |pos| &param[..pos]);
216
217            // Check parameter name is alphanumeric
218            if !param_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
219                warn_log!(
220                    "Invalid route path '{}': parameter '{}' has invalid characters",
221                    path,
222                    param_name
223                );
224                return Err(format!(
225                    "Route parameter '{param_name}' must contain only alphanumeric characters and underscores"
226                ));
227            }
228
229            // Check for duplicate parameters
230            if !param_names.insert(param_name.to_string()) {
231                warn_log!(
232                    "Invalid route path '{}': duplicate parameter '{}'",
233                    path,
234                    param_name
235                );
236                return Err(format!("Duplicate route parameter: '{param_name}'"));
237            }
238        }
239    }
240
241    Ok(())
242}
243
244// ============================================================================
245// RouteConfig
246// ============================================================================
247
248/// Lightweight route configuration without a builder or feature-gated fields.
249///
250/// Carries the path pattern, optional name, child configs, and metadata.
251/// Used internally by [`Route`] and also available standalone for scenarios
252/// where you need route metadata without a render function.
253#[must_use]
254#[derive(Debug, Clone)]
255pub struct RouteConfig {
256    /// Route path pattern (e.g., "/users/:id")
257    pub path: String,
258    /// Route name (optional)
259    pub name: Option<String>,
260    /// Child routes (NOTE: For nested routing, use `Route.children()` instead)
261    pub children: Vec<Self>,
262    /// Route metadata
263    pub meta: HashMap<String, String>,
264}
265
266impl RouteConfig {
267    /// Check if this is a layout route (has children but no explicit builder)
268    #[must_use] 
269    pub fn is_layout(&self) -> bool {
270        !self.children.is_empty()
271    }
272}
273
274impl RouteConfig {
275    /// Create a new route with path validation
276    ///
277    /// # Panics
278    ///
279    /// Panics if the path is invalid. Use `try_new` for non-panicking validation.
280    pub fn new(path: impl Into<String>) -> Self {
281        let path_str = path.into();
282        if let Err(e) = validate_route_path(&path_str) {
283            panic!("Invalid route path '{path_str}': {e}");
284        }
285        Self {
286            path: path_str,
287            name: None,
288            children: Vec::new(),
289            meta: HashMap::new(),
290        }
291    }
292
293    /// Create a new route with validation, returning Result
294    ///
295    /// Use this if you want to handle validation errors instead of panicking.
296    ///
297    /// # Errors
298    ///
299    /// Returns `Err(String)` if the path fails [`validate_route_path`] validation.
300    pub fn try_new(path: impl Into<String>) -> Result<Self, String> {
301        let path_str = path.into();
302        validate_route_path(&path_str)?;
303        Ok(Self {
304            path: path_str,
305            name: None,
306            children: Vec::new(),
307            meta: HashMap::new(),
308        })
309    }
310
311    /// Set route name
312    pub fn name(mut self, name: impl Into<String>) -> Self {
313        self.name = Some(name.into());
314        self
315    }
316
317    /// Add child routes
318    pub fn children(mut self, children: Vec<Self>) -> Self {
319        self.children = children;
320        self
321    }
322
323    /// Add a child route
324    pub fn child(mut self, child: Self) -> Self {
325        self.children.push(child);
326        self
327    }
328
329    /// Add metadata
330    pub fn meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
331        self.meta.insert(key.into(), value.into());
332        self
333    }
334}
335
336/// Type for route builder function
337///
338/// Builder receives Window, context and parameters, returns an `AnyElement`.
339/// Through context you have access to App, global state, and Navigator.
340///
341/// The `Window` parameter allows builders to use stateful GPUI features like
342/// `window.use_state()` or `window.use_keyed_state()` for caching expensive computations
343/// or creating persistent entities across renders.
344///
345/// Note: When using `Route::new()`, your builder can return any type that implements
346/// `IntoElement` - the conversion to `AnyElement` is done automatically.
347pub type RouteBuilder =
348    Arc<dyn Fn(&mut Window, &mut App, &RouteParams) -> AnyElement + Send + Sync>;
349
350/// Shared route handle.
351///
352/// A `Route` contains non-cloneable behavior (guards/middleware/lifecycle).
353/// To make route trees cheap to share and cache, the canonical way to pass
354/// routes around is via `Arc<Route>`.
355pub type RouteRef = Arc<Route>;
356
357/// Look up a cached component view by `key`, or create and cache a new one.
358///
359/// Used by [`Route::component`] and [`Route::component_with_params`] to
360/// avoid duplicating the cache-check/create/store pattern.
361fn get_or_create_cached_component<T: Render + 'static>(
362    cx: &mut App,
363    key: String,
364    create: impl FnOnce() -> T,
365) -> AnyElement {
366    // Check the global component cache first (survives across navigations)
367    if let Some(router) = cx.try_global::<crate::context::GlobalRouter>() {
368        if let Some(cached) = router.get_cached_component(&key) {
369            return cached.clone().into_any_element();
370        }
371    }
372
373    // Not cached — create a new entity and cache it
374    let entity: gpui::Entity<T> = cx.new(|_| create());
375    let view: AnyView = entity.into();
376
377    if cx.try_global::<crate::context::GlobalRouter>().is_some() {
378        cx.update_global::<crate::context::GlobalRouter, _>(
379            |router: &mut crate::context::GlobalRouter, _| {
380                router.cache_component(key, view.clone());
381            },
382        );
383    }
384
385    view.into_any_element()
386}
387
388/// A single route in the navigation tree.
389///
390/// Combines a path pattern, an optional builder function, child routes, and
391/// feature-gated behavior (guards, middleware, lifecycle hooks, transitions).
392///
393/// Create routes with [`Route::new`], [`Route::view`], [`Route::component`],
394/// or [`Route::component_with_params`], then configure them with the fluent
395/// builder methods.
396#[must_use]
397pub struct Route {
398    /// Route configuration
399    pub config: RouteConfig,
400    /// Builder function to create the view for this route
401    pub builder: Option<RouteBuilder>,
402    /// Child routes with their own builders
403    /// This is the preferred way to define nested routes (instead of RouteConfig.children)
404    pub children: Vec<RouteRef>,
405    /// Named outlets - map of outlet name to child routes
406    /// Allows multiple outlet areas in a single parent route
407    pub named_children: HashMap<String, Vec<RouteRef>>,
408    /// Guards that control access to this route
409    #[cfg(feature = "guard")]
410    pub guards: Vec<Box<dyn RouteGuard>>,
411    /// Middleware that runs before and after navigation to this route
412    #[cfg(feature = "middleware")]
413    pub middleware: Vec<Box<dyn RouteMiddleware>>,
414    /// Lifecycle hooks for this route
415    pub lifecycle: Option<Box<dyn RouteLifecycle>>,
416    /// Transition animation for this route
417    #[cfg(feature = "transition")]
418    pub transition: TransitionConfig,
419}
420
421impl Route {
422    /// Create a route with a builder function.
423    ///
424    /// The builder receives the window, app context and extracted route parameters,
425    /// and must return `AnyElement`. Use `.into_any_element()` on your element.
426    ///
427    /// # Example
428    ///
429    /// ```no_run
430    /// use gpui_navigator::Route;
431    /// use gpui::*;
432    ///
433    /// // Simple static route
434    /// Route::new("/home", |_window, _cx, _params| {
435    ///     div().child("Home Page").into_any_element()
436    /// });
437    ///
438    /// // Route with dynamic parameter
439    /// Route::new("/users/:id", |_window, _cx, params| {
440    ///     let id = params.get("id").unwrap();
441    ///     div().child(format!("User: {}", id)).into_any_element()
442    /// });
443    /// ```
444    pub fn new<F>(path: impl Into<String>, builder: F) -> Self
445    where
446        F: Fn(&mut Window, &mut App, &RouteParams) -> AnyElement + Send + Sync + 'static,
447    {
448        Self {
449            config: RouteConfig::new(path),
450            builder: Some(Arc::new(builder)),
451            children: Vec::new(),
452            named_children: HashMap::new(),
453            #[cfg(feature = "guard")]
454            guards: Vec::new(),
455            #[cfg(feature = "middleware")]
456            middleware: Vec::new(),
457            lifecycle: None,
458            #[cfg(feature = "transition")]
459            transition: TransitionConfig::default(),
460        }
461    }
462
463    /// Create a stateless route from a simple view function.
464    ///
465    /// Use this for simple, stateless pages that don't need access to route params,
466    /// window, or context. The view function is called on every render.
467    ///
468    /// # Example
469    ///
470    /// ```no_run
471    /// use gpui_navigator::Route;
472    /// use gpui::*;
473    ///
474    /// Route::view("/about", || {
475    ///     div().child("About Page").into_any_element()
476    /// });
477    /// ```
478    pub fn view(
479        path: impl Into<String>,
480        view: impl Fn() -> AnyElement + Send + Sync + 'static,
481    ) -> Self {
482        Self::new(path, move |_, _, _| view())
483    }
484
485    /// Create a stateful route with an Entity-based component
486    ///
487    /// Use this for pages that maintain internal state across navigation.
488    /// The component is cached using `window.use_keyed_state()`, so navigating
489    /// back to the route will preserve the component's state.
490    ///
491    /// # Example
492    ///
493    /// ```no_run
494    /// use gpui_navigator::Route;
495    /// use gpui::*;
496    ///
497    /// struct CounterPage {
498    ///     count: i32,
499    /// }
500    ///
501    /// impl CounterPage {
502    ///     fn new() -> Self {
503    ///         Self { count: 0 }
504    ///     }
505    /// }
506    ///
507    /// impl Render for CounterPage {
508    ///     fn render(&mut self, _window: &mut Window, _cx: &mut Context<'_, Self>) -> impl IntoElement {
509    ///         div().child(format!("Count: {}", self.count))
510    ///     }
511    /// }
512    ///
513    /// Route::component("/counter", CounterPage::new);
514    /// ```
515    pub fn component<T, F>(path: impl Into<String>, create: F) -> Self
516    where
517        T: Render + 'static,
518        F: Fn() -> T + Send + Sync + 'static + Clone,
519    {
520        let path_str = path.into();
521        let key_path = path_str.clone();
522        let type_id = std::any::TypeId::of::<T>();
523
524        Self::new(path_str, move |_window, cx, _| {
525            let key = format!("route:{key_path}:{type_id:?}");
526            let create_fn = create.clone();
527            get_or_create_cached_component(cx, key, create_fn)
528        })
529    }
530
531    /// Create a stateful route with parameters
532    ///
533    /// Like `component()`, but the create function receives route parameters.
534    /// The component is cached per unique set of parameter values, so navigating
535    /// to `/user/1` and `/user/2` will create two separate component instances.
536    ///
537    /// # Example
538    ///
539    /// ```no_run
540    /// use gpui_navigator::{Route, RouteParams};
541    /// use gpui::*;
542    ///
543    /// struct UserPage {
544    ///     user_id: String,
545    /// }
546    ///
547    /// impl UserPage {
548    ///     fn new(user_id: String) -> Self {
549    ///         Self { user_id }
550    ///     }
551    /// }
552    ///
553    /// impl Render for UserPage {
554    ///     fn render(&mut self, _window: &mut Window, _cx: &mut Context<'_, Self>) -> impl IntoElement {
555    ///         div().child(format!("User: {}", self.user_id))
556    ///     }
557    /// }
558    ///
559    /// Route::component_with_params("/user/:id", |params| {
560    ///     let id = params.get("id").unwrap().to_string();
561    ///     UserPage::new(id)
562    /// });
563    /// ```
564    pub fn component_with_params<T, F>(path: impl Into<String>, create: F) -> Self
565    where
566        T: Render + 'static,
567        F: Fn(&RouteParams) -> T + Send + Sync + 'static + Clone,
568    {
569        let path_str = path.into();
570        let key_path = path_str.clone();
571        let type_id = std::any::TypeId::of::<T>();
572
573        Self::new(path_str, move |_window, cx, params| {
574            let params_key = params
575                .iter()
576                .map(|(k, v)| format!("{k}={v}"))
577                .collect::<Vec<_>>()
578                .join("&");
579            let key = format!("route:{key_path}:{type_id:?}?{params_key}");
580            let params_clone = params.clone();
581            let create_fn = create.clone();
582            get_or_create_cached_component(cx, key, || create_fn(&params_clone))
583        })
584    }
585
586    /// Add child routes to this route
587    ///
588    /// Child routes will be rendered in a `RouterOutlet` within the parent's layout.
589    ///
590    /// # T038: Index Route Validation
591    ///
592    /// This method validates child routes and warns if there's an ambiguous default:
593    /// - If a parent has multiple children and no index route (path=""), users must
594    ///   explicitly navigate to a child path. Without an index, navigating to the
595    ///   parent path alone will show nothing.
596    ///
597    /// # Example
598    ///
599    /// ```no_run
600    /// use gpui_navigator::{Route, render_router_outlet};
601    /// use gpui::*;
602    ///
603    /// // Good: Has index route for default content
604    /// Route::new("/dashboard", |window, cx, params| {
605    ///     div()
606    ///         .child("Dashboard Header")
607    ///         .child(render_router_outlet(window, cx, None))
608    ///         .into_any_element()
609    /// })
610    /// .children(vec![
611    ///     Route::new("", |_, _cx, _params| {        // Index route
612    ///         div().child("Overview (Default)").into_any_element()
613    ///     })
614    ///     .into(),
615    ///     Route::new("settings", |_, _cx, _params| {
616    ///         div().child("Settings").into_any_element()
617    ///     })
618    ///     .into(),
619    /// ]);
620    ///
621    /// // Warning: No index route - navigating to "/dashboard" shows nothing
622    /// Route::new("/dashboard", |window, cx, params| {
623    ///     div()
624    ///         .child("Dashboard Header")
625    ///         .child(render_router_outlet(window, cx, None))
626    ///         .into_any_element()
627    /// })
628    /// .children(vec![
629    ///     Route::new("overview", |_, _cx, _params| {
630    ///         div().child("Overview").into_any_element()
631    ///     })
632    ///     .into(),
633    ///     Route::new("settings", |_, _cx, _params| {
634    ///         div().child("Settings").into_any_element()
635    ///     })
636    ///     .into(),
637    /// ]);
638    /// ```
639    pub fn children(mut self, children: Vec<RouteRef>) -> Self {
640        // T038: Validate index routes - warn if ambiguous default
641        if children.len() > 1 {
642            let has_index = children.iter().any(|child| {
643                let path = child.config.path.trim_matches('/');
644                path.is_empty() || path == "index"
645            });
646
647            if !has_index && !self.config.path.is_empty() {
648                warn_log!(
649                    "Route '{}' has {} children but no index route (path=\"\" or \"index\"). \
650                     Navigating to '{}' alone will show no content.",
651                    self.config.path,
652                    children.len(),
653                    self.config.path
654                );
655            }
656        }
657
658        self.children = children;
659        self
660    }
661
662    /// Add a single child route
663    ///
664    /// # Example
665    ///
666    /// ```no_run
667    /// use gpui_navigator::Route;
668    /// use gpui::*;
669    ///
670    /// Route::new("/dashboard", |_, _cx, _params| div().into_any_element())
671    ///     .child(Route::new("overview", |_, _cx, _params| div().into_any_element()).into())
672    ///     .child(Route::new("settings", |_, _cx, _params| div().into_any_element()).into());
673    /// ```
674    pub fn child(mut self, child: RouteRef) -> Self {
675        self.children.push(child);
676        self
677    }
678
679    /// Set route name
680    ///
681    /// Named routes can be referenced by name instead of path.
682    pub fn name(mut self, name: impl Into<String>) -> Self {
683        self.config.name = Some(name.into());
684        self
685    }
686
687    /// Add metadata to the route
688    ///
689    /// Metadata can be used for guards, analytics, titles, etc.
690    ///
691    /// # Example
692    ///
693    /// ```no_run
694    /// use gpui_navigator::Route;
695    /// use gpui::*;
696    ///
697    /// Route::new("/admin", |_, _cx, _params| div().into_any_element())
698    ///     .meta("requiresAuth", "true")
699    ///     .meta("requiredRole", "admin")
700    ///     .meta("title", "Admin Panel");
701    /// ```
702    pub fn meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
703        self.config.meta.insert(key.into(), value.into());
704        self
705    }
706
707    /// Add routes for a named outlet
708    ///
709    /// Named outlets allow you to have multiple content areas in a single parent route.
710    /// For example, a main content area and a sidebar.
711    ///
712    /// # Example
713    ///
714    /// ```no_run
715    /// use gpui_navigator::{Route, render_router_outlet};
716    /// use gpui::*;
717    ///
718    /// Route::new("/dashboard", |window, cx, _params| {
719    ///     div()
720    ///         .child(render_router_outlet(window, cx, None))             // Main content
721    ///         .child(render_router_outlet(window, cx, Some("sidebar")))  // Sidebar
722    ///         .into_any_element()
723    /// })
724    /// .children(vec![
725    ///     Route::new("analytics", |_, _cx, _params| div().into_any_element()).into(),
726    /// ])
727    /// .named_outlet("sidebar", vec![
728    ///     Route::new("stats", |_, _cx, _params| div().into_any_element()).into(),
729    /// ]);
730    /// ```
731    pub fn named_outlet(mut self, name: impl Into<String>, children: Vec<RouteRef>) -> Self {
732        self.named_children.insert(name.into(), children);
733        self
734    }
735
736    /// Add a guard to this route
737    ///
738    /// Guards control access to routes. If any guard denies access, navigation is blocked.
739    ///
740    /// # Example
741    ///
742    /// ```no_run
743    /// use gpui_navigator::{Route, AuthGuard, RoleGuard};
744    /// use gpui::*;
745    ///
746    /// fn is_authenticated(_cx: &App) -> bool { true }
747    /// fn get_role(_cx: &App) -> Option<String> { Some("user".into()) }
748    ///
749    /// Route::new("/dashboard", |_, _cx, _params| div().into_any_element())
750    ///     .guard(AuthGuard::new(is_authenticated, "/login"))
751    ///     .guard(RoleGuard::new(get_role, "user", Some("/forbidden")));
752    /// ```
753    #[cfg(feature = "guard")]
754    pub fn guard<G: crate::guards::RouteGuard>(mut self, guard: G) -> Self {
755        self.guards.push(Box::new(guard));
756        self
757    }
758
759    /// Add multiple guards at once (pre-boxed).
760    #[cfg(feature = "guard")]
761    pub fn guards(mut self, guards: Vec<Box<dyn crate::guards::RouteGuard>>) -> Self {
762        self.guards.extend(guards);
763        self
764    }
765
766    /// Add middleware to this route
767    ///
768    /// Middleware runs before and after navigation.
769    ///
770    /// # Example
771    ///
772    /// ```no_run
773    /// use gpui_navigator::Route;
774    /// use gpui::*;
775    ///
776    /// // Route::new("/dashboard", |_, _cx, _params| div().into_any_element())
777    /// //     .middleware(LoggingMiddleware::new());
778    /// ```
779    #[cfg(feature = "middleware")]
780    pub fn middleware<M: crate::middleware::RouteMiddleware>(mut self, middleware: M) -> Self {
781        self.middleware.push(Box::new(middleware));
782        self
783    }
784
785    /// Add multiple middleware at once (pre-boxed).
786    #[cfg(feature = "middleware")]
787    pub fn middlewares(
788        mut self,
789        middleware: Vec<Box<dyn crate::middleware::RouteMiddleware>>,
790    ) -> Self {
791        self.middleware.extend(middleware);
792        self
793    }
794
795    /// Add lifecycle hooks to this route
796    ///
797    /// Lifecycle hooks allow you to run code when entering/exiting routes.
798    ///
799    /// # Example
800    ///
801    /// ```no_run
802    /// use gpui_navigator::{Route, RouteLifecycle, NavigationRequest};
803    /// use gpui::*;
804    /// use std::pin::Pin;
805    /// use std::future::Future;
806    ///
807    /// // Lifecycle hooks allow running code when entering/exiting routes
808    /// // Implement RouteLifecycle trait for custom behavior
809    /// ```
810    pub fn lifecycle<L: crate::lifecycle::RouteLifecycle>(mut self, lifecycle: L) -> Self {
811        self.lifecycle = Some(Box::new(lifecycle));
812        self
813    }
814
815    /// Set the transition animation for this route
816    ///
817    /// # Example
818    /// ```no_run
819    /// use gpui_navigator::{Route, Transition};
820    /// use gpui::*;
821    ///
822    /// Route::new("/page", |_, _cx, _params| div().into_any_element())
823    ///     .transition(Transition::fade(200));
824    /// ```
825    #[cfg(feature = "transition")]
826    pub const fn transition(mut self, transition: crate::transition::Transition) -> Self {
827        self.transition = TransitionConfig::new(transition);
828        self
829    }
830
831    /// Get child routes for a named outlet
832    ///
833    /// Returns None if the outlet doesn't exist
834    pub fn get_named_children(&self, name: &str) -> Option<&[RouteRef]> {
835        self.named_children.get(name).map(Vec::as_slice)
836    }
837
838    /// Check if this route has a named outlet
839    #[must_use] 
840    pub fn has_named_outlet(&self, name: &str) -> bool {
841        self.named_children.contains_key(name)
842    }
843
844    /// Get all named outlet names
845    pub fn named_outlet_names(&self) -> Vec<&str> {
846        self.named_children.keys().map(String::as_str).collect()
847    }
848
849    /// Match a path against this route
850    #[must_use] 
851    pub fn matches(&self, path: &str) -> Option<RouteMatch> {
852        match_path(&self.config.path, path)
853    }
854
855    /// Build the view for this route
856    pub fn build(
857        &self,
858        window: &mut Window,
859        cx: &mut App,
860        params: &RouteParams,
861    ) -> Option<AnyElement> {
862        trace_log!(
863            "Building route '{}' with {} params",
864            self.config.path,
865            params.len()
866        );
867        self.builder.as_ref().map(|b| b(window, cx, params))
868    }
869
870    /// Find a child route by path segment
871    ///
872    /// Used internally by `RouterOutlet` to resolve child routes.
873    #[must_use] 
874    pub fn find_child(&self, segment: &str) -> Option<&RouteRef> {
875        self.children.iter().find(|child| {
876            child.config.path == segment || child.config.path.trim_start_matches('/') == segment
877        })
878    }
879
880    /// Get all child routes
881    #[must_use] 
882    pub fn get_children(&self) -> &[RouteRef] {
883        &self.children
884    }
885}
886
887impl std::fmt::Debug for Route {
888    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
889        f.debug_struct("Route")
890            .field("config", &self.config)
891            .field("builder", &self.builder.is_some())
892            .field("children", &self.children.len())
893            .field(
894                "named_children",
895                &self.named_children.keys().collect::<Vec<_>>(),
896            )
897            .finish_non_exhaustive()
898    }
899}
900
901/// Match a path pattern against an actual path
902///
903/// Supports:
904/// - Static paths: `/users`
905/// - Dynamic segments: `/users/:id`
906/// - Wildcard: `/files/*`
907fn match_path(pattern: &str, path: &str) -> Option<RouteMatch> {
908    let pattern_iter = pattern.split('/').filter(|s| !s.is_empty());
909    let mut path_iter = path.split('/').filter(|s| !s.is_empty());
910
911    let mut route_match = RouteMatch::new(path.to_string());
912
913    for pattern_seg in pattern_iter {
914        if pattern_seg == "*" {
915            // Wildcard matches rest of path
916            return Some(route_match);
917        }
918
919        let Some(path_seg) = path_iter.next() else {
920            // Path exhausted before pattern — no match
921            return None;
922        };
923
924        if let Some(param_name) = pattern_seg.strip_prefix(':') {
925            route_match
926                .params
927                .insert(param_name.to_string(), path_seg.to_string());
928        } else if pattern_seg != path_seg {
929            return None;
930        }
931    }
932
933    // All pattern segments consumed — path must also be exhausted
934    if path_iter.next().is_some() {
935        return None;
936    }
937
938    Some(route_match)
939}
940
941// ============================================================================
942// Route Builder Utilities
943// ============================================================================
944
945//
946// This module provides a flexible routing system that can accept both simple
947// string paths and route builders with parameters.
948
949/// Trait for types that can be converted into a route
950///
951/// This allows `Navigator.push()` to accept both strings and route builders:
952/// ```ignore
953/// use gpui_navigator::{Navigator, PageRoute};
954///
955/// // String path
956/// Navigator::push(cx, "/users");
957///
958/// // Route with builder
959/// Navigator::push(cx, PageRoute::builder("/profile", |_, _cx, _params| {
960///     gpui::div()
961/// }));
962/// ```
963pub trait IntoRoute {
964    /// Convert this type into a route path and optional builder
965    fn into_route(self) -> RouteDescriptor;
966}
967
968/// Type for route builder function
969pub type BuilderFn = Arc<dyn Fn(&mut Window, &mut App, &RouteParams) -> AnyElement + Send + Sync>;
970
971/// A route descriptor containing path, parameters, and optional builder
972pub struct RouteDescriptor {
973    /// The route path (e.g., "/users/:id")
974    pub path: String,
975
976    /// Parameters to pass to the route
977    pub params: RouteParams,
978
979    /// Optional builder function to create the view
980    pub builder: Option<BuilderFn>,
981}
982
983// RouteParams is now imported from crate::params::RouteParams
984
985// Implement IntoRoute for String (simple path navigation)
986impl IntoRoute for String {
987    fn into_route(self) -> RouteDescriptor {
988        RouteDescriptor {
989            path: self,
990            params: RouteParams::new(),
991            builder: None,
992        }
993    }
994}
995
996// Implement IntoRoute for &str
997impl IntoRoute for &str {
998    fn into_route(self) -> RouteDescriptor {
999        RouteDescriptor {
1000            path: self.to_string(),
1001            params: RouteParams::new(),
1002            builder: None,
1003        }
1004    }
1005}
1006
1007/// A page route with optional builder function
1008///
1009/// # Example
1010///
1011/// ```ignore
1012/// use gpui_navigator::{Navigator, PageRoute};
1013///
1014/// // Simple path (no builder)
1015/// Navigator::push(cx, PageRoute::new("/users"));
1016///
1017/// // With parameters
1018/// Navigator::push(
1019///     cx,
1020///     PageRoute::new("/users/:id")
1021///         .with_param("id".into(), "123".into())
1022/// );
1023///
1024/// // With builder function
1025/// Navigator::push(
1026///     cx,
1027///     PageRoute::builder("/profile", |_window, cx, params| {
1028///         div().child("Profile Page")
1029///     })
1030/// );
1031/// ```
1032#[must_use]
1033pub struct PageRoute {
1034    path: String,
1035    params: RouteParams,
1036    builder: Option<BuilderFn>,
1037}
1038
1039impl PageRoute {
1040    /// Create a new `PageRoute` with a path (no builder)
1041    pub fn new(path: impl Into<String>) -> Self {
1042        Self {
1043            path: path.into(),
1044            params: RouteParams::new(),
1045            builder: None,
1046        }
1047    }
1048
1049    /// Create a `PageRoute` with a builder function.
1050    ///
1051    /// The builder must return `AnyElement`.
1052    pub fn builder(
1053        path: impl Into<String>,
1054        builder: impl Fn(&mut Window, &mut App, &RouteParams) -> AnyElement + Send + Sync + 'static,
1055    ) -> Self {
1056        Self {
1057            path: path.into(),
1058            params: RouteParams::new(),
1059            builder: Some(Arc::new(builder)),
1060        }
1061    }
1062
1063    /// Set the builder function for this route.
1064    ///
1065    /// The builder must return `AnyElement`.
1066    pub fn with_builder(
1067        mut self,
1068        builder: impl Fn(&mut Window, &mut App, &RouteParams) -> AnyElement + Send + Sync + 'static,
1069    ) -> Self {
1070        self.builder = Some(Arc::new(builder));
1071        self
1072    }
1073
1074    /// Add a parameter to this route
1075    pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1076        self.params.insert(key.into(), value.into());
1077        self
1078    }
1079
1080    /// Add multiple parameters
1081    pub fn with_params(mut self, params: HashMap<String, String>) -> Self {
1082        self.params = RouteParams::from_map(params);
1083        self
1084    }
1085}
1086
1087impl IntoRoute for PageRoute {
1088    fn into_route(self) -> RouteDescriptor {
1089        RouteDescriptor {
1090            path: self.path,
1091            params: self.params,
1092            builder: self.builder,
1093        }
1094    }
1095}
1096
1097/// A named route for predefined routes
1098///
1099/// # Example
1100///
1101/// ```ignore
1102/// use gpui_navigator::{Navigator, NamedRoute, RouteParams};
1103///
1104/// let mut params = RouteParams::new();
1105/// params.set("userId".to_string(), "123".to_string());
1106/// Navigator::push_named(cx, "user_profile", &params);
1107/// ```
1108#[must_use]
1109pub struct NamedRoute {
1110    name: String,
1111    params: RouteParams,
1112}
1113
1114impl NamedRoute {
1115    /// Create a new named route
1116    pub fn new(name: impl Into<String>) -> Self {
1117        Self {
1118            name: name.into(),
1119            params: RouteParams::new(),
1120        }
1121    }
1122
1123    /// Add a parameter
1124    pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1125        self.params.insert(key.into(), value.into());
1126        self
1127    }
1128
1129    /// Add multiple parameters
1130    pub fn with_params(mut self, params: HashMap<String, String>) -> Self {
1131        self.params = RouteParams::from_map(params);
1132        self
1133    }
1134}
1135
1136impl IntoRoute for NamedRoute {
1137    fn into_route(self) -> RouteDescriptor {
1138        RouteDescriptor {
1139            path: self.name,
1140            params: self.params,
1141            builder: None,
1142        }
1143    }
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148    use super::*;
1149
1150    // NamedRouteRegistry tests
1151
1152    #[test]
1153    fn test_registry_register_and_get() {
1154        let mut registry = NamedRouteRegistry::new();
1155        registry.register("home", "/");
1156        registry.register("user.detail", "/users/:id");
1157
1158        assert_eq!(registry.get("home"), Some("/"));
1159        assert_eq!(registry.get("user.detail"), Some("/users/:id"));
1160        assert_eq!(registry.get("unknown"), None);
1161    }
1162
1163    #[test]
1164    fn test_registry_contains() {
1165        let mut registry = NamedRouteRegistry::new();
1166        registry.register("home", "/");
1167
1168        assert!(registry.contains("home"));
1169        assert!(!registry.contains("unknown"));
1170    }
1171
1172    #[test]
1173    fn test_url_for_simple() {
1174        let mut registry = NamedRouteRegistry::new();
1175        registry.register("home", "/");
1176
1177        let params = RouteParams::new();
1178        assert_eq!(registry.url_for("home", &params), Some("/".to_string()));
1179    }
1180
1181    #[test]
1182    fn test_url_for_with_params() {
1183        let mut registry = NamedRouteRegistry::new();
1184        registry.register("user.detail", "/users/:id");
1185
1186        let mut params = RouteParams::new();
1187        params.set("id".to_string(), "123".to_string());
1188
1189        assert_eq!(
1190            registry.url_for("user.detail", &params),
1191            Some("/users/123".to_string())
1192        );
1193    }
1194
1195    #[test]
1196    fn test_url_for_multiple_params() {
1197        let mut registry = NamedRouteRegistry::new();
1198        registry.register("post.comment", "/posts/:postId/comments/:commentId");
1199
1200        let mut params = RouteParams::new();
1201        params.set("postId".to_string(), "42".to_string());
1202        params.set("commentId".to_string(), "99".to_string());
1203
1204        assert_eq!(
1205            registry.url_for("post.comment", &params),
1206            Some("/posts/42/comments/99".to_string())
1207        );
1208    }
1209
1210    #[test]
1211    fn test_url_for_unknown_route() {
1212        let registry = NamedRouteRegistry::new();
1213        let params = RouteParams::new();
1214
1215        assert_eq!(registry.url_for("unknown", &params), None);
1216    }
1217
1218    #[test]
1219    fn test_registry_clear() {
1220        let mut registry = NamedRouteRegistry::new();
1221        registry.register("home", "/");
1222        registry.register("about", "/about");
1223
1224        assert_eq!(registry.len(), 2);
1225
1226        registry.clear();
1227
1228        assert_eq!(registry.len(), 0);
1229        assert!(registry.is_empty());
1230    }
1231
1232    #[test]
1233    fn test_substitute_params() {
1234        let mut params = RouteParams::new();
1235        params.set("id".to_string(), "123".to_string());
1236        params.set("action".to_string(), "edit".to_string());
1237
1238        let result = substitute_params("/users/:id/:action", &params);
1239        assert_eq!(result, "/users/123/edit");
1240    }
1241
1242    // Route tests
1243
1244    #[test]
1245    fn test_static_route() {
1246        let result = match_path("/users", "/users");
1247        assert!(result.is_some());
1248
1249        let result = match_path("/users", "/posts");
1250        assert!(result.is_none());
1251    }
1252
1253    #[test]
1254    fn test_dynamic_route() {
1255        let result = match_path("/users/:id", "/users/123");
1256        assert!(result.is_some());
1257
1258        let route_match = result.unwrap();
1259        assert_eq!(route_match.params.get("id"), Some(&"123".to_string()));
1260    }
1261
1262    #[test]
1263    fn test_wildcard_route() {
1264        let result = match_path("/files/*", "/files/documents/report.pdf");
1265        assert!(result.is_some());
1266
1267        let result = match_path("/files/*", "/other/path");
1268        assert!(result.is_none());
1269    }
1270
1271    #[test]
1272    fn test_string_into_route() {
1273        let route = "/users".into_route();
1274        assert_eq!(route.path, "/users");
1275        assert!(route.params.all().is_empty());
1276    }
1277
1278    #[test]
1279    fn test_material_route_with_params() {
1280        let route = PageRoute::new("/users/:id")
1281            .with_param("id", "123")
1282            .into_route();
1283
1284        assert_eq!(route.path, "/users/:id");
1285        assert_eq!(route.params.get("id"), Some(&"123".to_string()));
1286    }
1287
1288    #[test]
1289    fn test_named_route() {
1290        let route = NamedRoute::new("user_profile")
1291            .with_param("userId", "456")
1292            .into_route();
1293
1294        assert_eq!(route.path, "user_profile");
1295        assert_eq!(route.params.get("userId"), Some(&"456".to_string()));
1296    }
1297
1298    // Validation tests
1299
1300    #[test]
1301    fn test_validate_valid_paths() {
1302        assert!(validate_route_path("/").is_ok());
1303        assert!(validate_route_path("/users").is_ok());
1304        assert!(validate_route_path("/users/:id").is_ok());
1305        assert!(validate_route_path("/posts/:postId/comments/:commentId").is_ok());
1306        assert!(validate_route_path("/users/:id{uuid}").is_ok());
1307        assert!(validate_route_path("/api/v1/users").is_ok());
1308        assert!(validate_route_path("settings").is_ok()); // relative path
1309        assert!(validate_route_path("").is_ok()); // empty path (index route)
1310        assert!(validate_route_path("/users/").is_ok()); // trailing slash allowed
1311    }
1312
1313    #[test]
1314    fn test_validate_consecutive_slashes() {
1315        let result = validate_route_path("/users//profile");
1316        assert!(result.is_err());
1317        assert!(result.unwrap_err().contains("consecutive slashes"));
1318    }
1319
1320    #[test]
1321    fn test_validate_empty_parameter() {
1322        let result = validate_route_path("/users/:");
1323        assert!(result.is_err());
1324        assert!(result
1325            .unwrap_err()
1326            .contains("parameter name cannot be empty"));
1327    }
1328
1329    #[test]
1330    fn test_validate_invalid_parameter_name() {
1331        let result = validate_route_path("/users/:user-id");
1332        assert!(result.is_err());
1333        assert!(result.unwrap_err().contains("alphanumeric"));
1334    }
1335
1336    #[test]
1337    fn test_validate_duplicate_parameters() {
1338        let result = validate_route_path("/users/:id/posts/:id");
1339        assert!(result.is_err());
1340        assert!(result.unwrap_err().contains("Duplicate"));
1341    }
1342
1343    #[test]
1344    fn test_route_config_try_new_valid() {
1345        let result = RouteConfig::try_new("/users/:id");
1346        assert!(result.is_ok());
1347        assert_eq!(result.unwrap().path, "/users/:id");
1348    }
1349
1350    #[test]
1351    fn test_route_config_try_new_invalid() {
1352        let result = RouteConfig::try_new("/users//profile");
1353        assert!(result.is_err());
1354    }
1355
1356    #[test]
1357    #[should_panic(expected = "Invalid route path")]
1358    fn test_route_config_new_panics_on_invalid() {
1359        let _ = RouteConfig::new("/users//profile");
1360    }
1361}