Skip to main content

gpui_navigator/
route.rs

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