Skip to main content

gpui_navigator/
guards.rs

1//! Route guards for authentication, authorization, and validation
2//!
3//! Guards are middleware that can block, allow, or redirect navigation.
4//! They're useful for authentication, authorization, and validation.
5
6use crate::{NavigationRequest, RouteMatch};
7use gpui::App;
8use std::future::Future;
9use std::pin::Pin;
10
11/// Result of a guard check
12#[derive(Debug, Clone, PartialEq)]
13pub enum GuardResult {
14    /// Allow navigation to proceed
15    Allow,
16
17    /// Deny navigation with a reason
18    Deny {
19        /// Reason for denying navigation
20        reason: String,
21    },
22
23    /// Redirect to a different path
24    Redirect {
25        /// Path to redirect to
26        to: String,
27        /// Reason for redirect (optional)
28        reason: Option<String>,
29    },
30}
31
32impl GuardResult {
33    /// Create an allow result
34    pub fn allow() -> Self {
35        GuardResult::Allow
36    }
37
38    /// Create a deny result with reason
39    pub fn deny(reason: impl Into<String>) -> Self {
40        GuardResult::Deny {
41            reason: reason.into(),
42        }
43    }
44
45    /// Create a redirect result
46    pub fn redirect(to: impl Into<String>) -> Self {
47        GuardResult::Redirect {
48            to: to.into(),
49            reason: None,
50        }
51    }
52
53    /// Create a redirect result with reason
54    pub fn redirect_with_reason(to: impl Into<String>, reason: impl Into<String>) -> Self {
55        GuardResult::Redirect {
56            to: to.into(),
57            reason: Some(reason.into()),
58        }
59    }
60
61    /// Check if result is allow
62    pub fn is_allow(&self) -> bool {
63        matches!(self, GuardResult::Allow)
64    }
65
66    /// Check if result is deny
67    pub fn is_deny(&self) -> bool {
68        matches!(self, GuardResult::Deny { .. })
69    }
70
71    /// Check if result is redirect
72    pub fn is_redirect(&self) -> bool {
73        matches!(self, GuardResult::Redirect { .. })
74    }
75
76    /// Get redirect path if this is a redirect
77    pub fn redirect_path(&self) -> Option<&str> {
78        match self {
79            GuardResult::Redirect { to, .. } => Some(to.as_str()),
80            _ => None,
81        }
82    }
83}
84
85/// Trait for route guards
86///
87/// Guards can block navigation, allow it, or redirect to a different path.
88/// Guards use an associated `Future` type for async operations without boxing.
89///
90/// # Design Benefits
91///
92/// - Zero-cost: No `Box<dyn Future>` allocation for concrete types
93/// - Compile-time: Types checked at compile time
94/// - Composable: Can be wrapped and combined
95/// - Trait object safe: Can still use `Box<dyn RouteGuard>` when needed
96///
97/// # Example
98///
99/// ```no_run
100/// use gpui_navigator::{RouteGuard, GuardResult, NavigationRequest};
101/// use std::future::Future;
102/// use std::pin::Pin;
103///
104/// struct MyAuthGuard {
105///     redirect_to: String,
106/// }
107///
108/// impl RouteGuard for MyAuthGuard {
109///     type Future = Pin<Box<dyn Future<Output = GuardResult> + Send>>;
110///
111///     fn check(&self, _cx: &gpui::App, _request: &NavigationRequest) -> Self::Future {
112///         let redirect_to = self.redirect_to.clone();
113///         let is_authenticated = true; // Replace with actual check
114///
115///         Box::pin(async move {
116///             if is_authenticated {
117///                 GuardResult::allow()
118///             } else {
119///                 GuardResult::redirect(redirect_to)
120///             }
121///         })
122///     }
123/// }
124/// ```
125///
126/// # For Simple Guards
127///
128/// Use the `guard_fn` helper to create guards from async closures:
129///
130/// ```no_run
131/// use gpui_navigator::{guard_fn, GuardResult};
132///
133/// let guard = guard_fn(|_cx, _request| async move {
134///     // Replace with actual authentication check
135///     let is_authenticated = true;
136///     if is_authenticated {
137///         GuardResult::allow()
138///     } else {
139///         GuardResult::redirect("/login")
140///     }
141/// });
142/// ```
143pub trait RouteGuard: Send + Sync + 'static {
144    /// The future returned by check
145    type Future: Future<Output = GuardResult> + Send + 'static;
146
147    /// Check if navigation should be allowed
148    ///
149    /// # Parameters
150    /// - `cx`: Application context
151    /// - `request`: Navigation request with extensions for context
152    ///
153    /// # Returns
154    /// A future that resolves to:
155    /// - `GuardResult::Allow` to allow navigation
156    /// - `GuardResult::Deny` to block navigation
157    /// - `GuardResult::Redirect` to redirect to a different path
158    ///
159    /// # Note
160    ///
161    /// Guards can access route parameters and request data through the
162    /// `request` parameter to make authorization decisions.
163    fn check(&self, cx: &App, request: &NavigationRequest) -> Self::Future;
164
165    /// Get guard name (for debugging and error messages)
166    fn name(&self) -> &str {
167        "RouteGuard"
168    }
169
170    /// Optional priority for guard execution order
171    ///
172    /// Higher priority guards run first. Default is 0.
173    fn priority(&self) -> i32 {
174        0
175    }
176}
177
178/// Boxed route guard for dynamic dispatch
179pub type BoxedGuard =
180    Box<dyn RouteGuard<Future = Pin<Box<dyn Future<Output = GuardResult> + Send>>>>;
181
182/// Create a guard from an async function or closure
183///
184/// This is a convenience helper for creating guards without manually implementing
185/// the `RouteGuard` trait.
186///
187/// # Example
188///
189/// ```no_run
190/// use gpui_navigator::{guard_fn, GuardResult};
191///
192/// let auth_guard = guard_fn(|_cx, _request| async move {
193///     // Replace with actual authentication check
194///     let is_authenticated = true;
195///     if is_authenticated {
196///         GuardResult::allow()
197///     } else {
198///         GuardResult::redirect("/login")
199///     }
200/// });
201/// ```
202pub fn guard_fn<F, Fut>(f: F) -> FnGuard<F>
203where
204    F: Fn(&App, &NavigationRequest) -> Fut + Send + Sync + 'static,
205    Fut: Future<Output = GuardResult> + Send + 'static,
206{
207    FnGuard { f }
208}
209
210/// Guard created from a function or closure
211pub struct FnGuard<F> {
212    f: F,
213}
214
215impl<F, Fut> RouteGuard for FnGuard<F>
216where
217    F: Fn(&App, &NavigationRequest) -> Fut + Send + Sync + 'static,
218    Fut: Future<Output = GuardResult> + Send + 'static,
219{
220    type Future = Fut;
221
222    fn check(&self, cx: &App, request: &NavigationRequest) -> Self::Future {
223        (self.f)(cx, request)
224    }
225}
226
227/// Guard context provides information about the navigation
228#[derive(Debug, Clone)]
229pub struct GuardContext {
230    /// Source route
231    pub from: Option<String>,
232
233    /// Target route
234    pub to: String,
235
236    /// Target route match
237    pub to_match: RouteMatch,
238}
239
240impl GuardContext {
241    /// Create a new guard context
242    pub fn new(from: Option<String>, to: String, to_match: RouteMatch) -> Self {
243        Self { from, to, to_match }
244    }
245
246    /// Get parameter from target route
247    pub fn param(&self, key: &str) -> Option<&String> {
248        self.to_match.params.get(key)
249    }
250
251    /// Get query parameter from target route
252    pub fn query(&self, key: &str) -> Option<&String> {
253        self.to_match.query.get(key)
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    use std::collections::HashMap;
262
263    #[test]
264    fn test_guard_result_allow() {
265        let result = GuardResult::allow();
266        assert!(result.is_allow());
267        assert!(!result.is_deny());
268        assert!(!result.is_redirect());
269        assert_eq!(result.redirect_path(), None);
270    }
271
272    #[test]
273    fn test_guard_result_deny() {
274        let result = GuardResult::deny("Not authorized");
275        assert!(!result.is_allow());
276        assert!(result.is_deny());
277        assert!(!result.is_redirect());
278
279        match result {
280            GuardResult::Deny { reason } => {
281                assert_eq!(reason, "Not authorized");
282            }
283            _ => panic!("Expected Deny"),
284        }
285    }
286
287    #[test]
288    fn test_guard_result_redirect() {
289        let result = GuardResult::redirect("/login");
290        assert!(!result.is_allow());
291        assert!(!result.is_deny());
292        assert!(result.is_redirect());
293        assert_eq!(result.redirect_path(), Some("/login"));
294    }
295
296    #[test]
297    fn test_guard_result_redirect_with_reason() {
298        let result = GuardResult::redirect_with_reason("/login", "Authentication required");
299
300        match result {
301            GuardResult::Redirect { to, reason } => {
302                assert_eq!(to, "/login");
303                assert_eq!(reason, Some("Authentication required".to_string()));
304            }
305            _ => panic!("Expected Redirect"),
306        }
307    }
308
309    #[test]
310    fn test_guard_context() {
311        let route_match = RouteMatch {
312            path: "/users/123".to_string(),
313            params: {
314                let mut map = HashMap::new();
315                map.insert("id".to_string(), "123".to_string());
316                map
317            },
318            query: {
319                let mut map = HashMap::new();
320                map.insert("page".to_string(), "1".to_string());
321                map
322            },
323        };
324
325        let ctx = GuardContext::new(Some("/".to_string()), "/users/123".to_string(), route_match);
326
327        assert_eq!(ctx.from, Some("/".to_string()));
328        assert_eq!(ctx.to, "/users/123");
329        assert_eq!(ctx.param("id"), Some(&"123".to_string()));
330        assert_eq!(ctx.query("page"), Some(&"1".to_string()));
331        assert_eq!(ctx.param("missing"), None);
332    }
333
334    // Mock guard for testing
335    struct AlwaysAllowGuard;
336
337    impl RouteGuard for AlwaysAllowGuard {
338        type Future = Pin<Box<dyn Future<Output = GuardResult> + Send>>;
339
340        fn check(&self, _cx: &App, _request: &NavigationRequest) -> Self::Future {
341            Box::pin(async { GuardResult::allow() })
342        }
343
344        fn name(&self) -> &str {
345            "AlwaysAllowGuard"
346        }
347    }
348
349    #[test]
350    fn test_guard_trait_name() {
351        let guard = AlwaysAllowGuard;
352        assert_eq!(guard.name(), "AlwaysAllowGuard");
353    }
354
355    #[test]
356    fn test_guard_trait_priority() {
357        let guard = AlwaysAllowGuard;
358        assert_eq!(guard.priority(), 0); // Default priority
359    }
360
361    #[test]
362    fn test_guard_fn_helper() {
363        let guard = guard_fn(|_cx, _request| async { GuardResult::allow() });
364
365        assert_eq!(guard.name(), "RouteGuard"); // Default name
366    }
367}
368
369// ============================================================================
370// Authentication and Authorization Guards
371// ============================================================================
372
373/// Type alias for authentication check function.
374///
375/// The function receives the application context and returns whether the user is authenticated.
376pub type AuthCheckFn = Box<dyn Fn(&App) -> bool + Send + Sync>;
377
378/// Authentication guard that checks if user is logged in.
379///
380/// Unlike placeholder guards, this guard is fully configurable with a custom
381/// authentication check function that you provide.
382///
383/// # Example
384///
385/// ```ignore
386/// use gpui_navigator::*;
387/// use gpui::App;
388///
389/// // Define your authentication check
390/// fn is_authenticated(cx: &App) -> bool {
391///     // Check your auth state here
392///     cx.try_global::<AuthState>()
393///         .map(|state| state.is_logged_in())
394///         .unwrap_or(false)
395/// }
396///
397/// // Use the guard
398/// Route::new("/dashboard", dashboard_page)
399///     .guard(AuthGuard::new(is_authenticated, "/login"))
400/// # ;
401/// # fn dashboard_page(_: &mut App, _: &gpui_navigator::RouteParams) -> gpui::AnyElement { todo!() }
402/// # struct AuthState;
403/// # impl AuthState { fn is_logged_in(&self) -> bool { false } }
404/// ```
405pub struct AuthGuard {
406    /// Function to check if user is authenticated
407    check_fn: AuthCheckFn,
408    /// Path to redirect to if not authenticated
409    redirect_path: String,
410}
411
412impl AuthGuard {
413    /// Create a new auth guard with a custom check function and redirect path.
414    ///
415    /// # Arguments
416    ///
417    /// * `check_fn` - Function that returns `true` if the user is authenticated
418    /// * `redirect_path` - Path to redirect to if authentication fails
419    ///
420    /// # Example
421    ///
422    /// ```ignore
423    /// use gpui_navigator::*;
424    ///
425    /// let guard = AuthGuard::new(
426    ///     |cx| cx.try_global::<IsLoggedIn>().is_some(),
427    ///     "/login"
428    /// );
429    /// # struct IsLoggedIn;
430    /// ```
431    pub fn new<F>(check_fn: F, redirect_path: impl Into<String>) -> Self
432    where
433        F: Fn(&App) -> bool + Send + Sync + 'static,
434    {
435        Self {
436            check_fn: Box::new(check_fn),
437            redirect_path: redirect_path.into(),
438        }
439    }
440
441    /// Create an auth guard that always allows access (for testing/development).
442    ///
443    /// **Warning**: Do not use in production!
444    #[cfg(debug_assertions)]
445    pub fn allow_all() -> Self {
446        Self::new(|_| true, "/login")
447    }
448
449    /// Create an auth guard that always denies access (for testing/development).
450    ///
451    /// **Warning**: Do not use in production!
452    #[cfg(debug_assertions)]
453    pub fn deny_all(redirect_path: impl Into<String>) -> Self {
454        Self::new(|_| false, redirect_path)
455    }
456}
457
458impl RouteGuard for AuthGuard {
459    type Future = Pin<Box<dyn Future<Output = GuardResult> + Send>>;
460
461    fn check(&self, cx: &App, _request: &NavigationRequest) -> Self::Future {
462        // Check authentication synchronously and return ready future
463        let is_authenticated = (self.check_fn)(cx);
464        let result = if is_authenticated {
465            GuardResult::allow()
466        } else {
467            GuardResult::redirect_with_reason(&self.redirect_path, "Authentication required")
468        };
469
470        Box::pin(async move { result })
471    }
472
473    fn name(&self) -> &str {
474        "AuthGuard"
475    }
476
477    fn priority(&self) -> i32 {
478        100 // High priority - check auth first
479    }
480}
481
482/// Type alias for role extraction function.
483///
484/// The function receives the application context and returns the user's current role(s).
485pub type RoleExtractorFn = Box<dyn Fn(&App) -> Option<String> + Send + Sync>;
486
487/// Role-based authorization guard.
488///
489/// Checks if user has required role for accessing a route. You must provide
490/// a function that extracts the user's role from your application state.
491///
492/// # Example
493///
494/// ```ignore
495/// use gpui_navigator::*;
496/// use gpui::App;
497///
498/// // Define how to get user's role
499/// fn get_user_role(cx: &App) -> Option<String> {
500///     cx.try_global::<CurrentUser>()
501///         .map(|user| user.role.clone())
502/// }
503///
504/// // Use the guard
505/// Route::new("/admin", admin_page)
506///     .guard(RoleGuard::new(get_user_role, "admin", Some("/forbidden")))
507/// # ;
508/// # fn admin_page(_: &mut App, _: &gpui_navigator::RouteParams) -> gpui::AnyElement { todo!() }
509/// # struct CurrentUser { role: String }
510/// ```
511pub struct RoleGuard {
512    /// Function to extract user's current role
513    role_extractor: RoleExtractorFn,
514    /// Required role
515    required_role: String,
516    /// Path to redirect to if unauthorized
517    redirect_path: Option<String>,
518}
519
520impl RoleGuard {
521    /// Create a new role guard with a role extractor function.
522    ///
523    /// # Arguments
524    ///
525    /// * `role_extractor` - Function that returns the user's current role (if any)
526    /// * `required_role` - The role required to access the route
527    /// * `redirect_path` - Optional path to redirect to if authorization fails
528    ///
529    /// # Example
530    ///
531    /// ```ignore
532    /// use gpui_navigator::*;
533    ///
534    /// let guard = RoleGuard::new(
535    ///     |cx| cx.try_global::<UserRole>().map(|r| r.0.clone()),
536    ///     "admin",
537    ///     Some("/forbidden")
538    /// );
539    /// # struct UserRole(String);
540    /// ```
541    pub fn new<F>(
542        role_extractor: F,
543        required_role: impl Into<String>,
544        redirect_path: Option<impl Into<String>>,
545    ) -> Self
546    where
547        F: Fn(&App) -> Option<String> + Send + Sync + 'static,
548    {
549        Self {
550            role_extractor: Box::new(role_extractor),
551            required_role: required_role.into(),
552            redirect_path: redirect_path.map(Into::into),
553        }
554    }
555
556    /// Check if the extracted role matches the required role
557    fn has_required_role(&self, cx: &App) -> bool {
558        (self.role_extractor)(cx)
559            .map(|role| role == self.required_role)
560            .unwrap_or(false)
561    }
562}
563
564impl RouteGuard for RoleGuard {
565    type Future = Pin<Box<dyn Future<Output = GuardResult> + Send>>;
566
567    fn check(&self, cx: &App, _request: &NavigationRequest) -> Self::Future {
568        let result = if self.has_required_role(cx) {
569            GuardResult::allow()
570        } else if let Some(redirect) = &self.redirect_path {
571            GuardResult::redirect_with_reason(
572                redirect,
573                format!("Requires '{}' role", self.required_role),
574            )
575        } else {
576            GuardResult::deny(format!("Missing required role: {}", self.required_role))
577        };
578
579        Box::pin(async move { result })
580    }
581
582    fn name(&self) -> &str {
583        "RoleGuard"
584    }
585
586    fn priority(&self) -> i32 {
587        90 // Slightly lower than auth guard
588    }
589}
590
591/// Type alias for permission check function.
592///
593/// The function receives the application context and the required permission,
594/// and returns whether the user has that permission.
595pub type PermissionCheckFn = Box<dyn Fn(&App, &str) -> bool + Send + Sync>;
596
597/// Permission-based authorization guard.
598///
599/// Checks if user has specific permission. You must provide a function
600/// that checks permissions against your application's permission system.
601///
602/// # Example
603///
604/// ```ignore
605/// use gpui_navigator::*;
606/// use gpui::App;
607///
608/// // Define permission check
609/// fn has_permission(cx: &App, permission: &str) -> bool {
610///     cx.try_global::<UserPermissions>()
611///         .map(|perms| perms.contains(permission))
612///         .unwrap_or(false)
613/// }
614///
615/// // Use the guard
616/// Route::new("/users/:id/delete", delete_user)
617///     .guard(PermissionGuard::new(has_permission, "users.delete"))
618/// # ;
619/// # fn delete_user(_: &mut App, _: &gpui_navigator::RouteParams) -> gpui::AnyElement { todo!() }
620/// # struct UserPermissions;
621/// # impl UserPermissions { fn contains(&self, _: &str) -> bool { false } }
622/// ```
623pub struct PermissionGuard {
624    /// Function to check if user has a permission
625    check_fn: PermissionCheckFn,
626    /// Required permission
627    permission: String,
628    /// Optional redirect path
629    redirect_path: Option<String>,
630}
631
632impl PermissionGuard {
633    /// Create a new permission guard with a check function.
634    ///
635    /// # Arguments
636    ///
637    /// * `check_fn` - Function that checks if user has the given permission
638    /// * `permission` - The permission required to access the route
639    ///
640    /// # Example
641    ///
642    /// ```ignore
643    /// use gpui_navigator::*;
644    ///
645    /// let guard = PermissionGuard::new(
646    ///     |cx, perm| {
647    ///         cx.try_global::<Permissions>()
648    ///             .map(|p| p.has(perm))
649    ///             .unwrap_or(false)
650    ///     },
651    ///     "users.delete"
652    /// );
653    /// # struct Permissions;
654    /// # impl Permissions { fn has(&self, _: &str) -> bool { false } }
655    /// ```
656    pub fn new<F>(check_fn: F, permission: impl Into<String>) -> Self
657    where
658        F: Fn(&App, &str) -> bool + Send + Sync + 'static,
659    {
660        Self {
661            check_fn: Box::new(check_fn),
662            permission: permission.into(),
663            redirect_path: None,
664        }
665    }
666
667    /// Add a redirect path for when permission is denied.
668    #[must_use]
669    pub fn with_redirect(mut self, path: impl Into<String>) -> Self {
670        self.redirect_path = Some(path.into());
671        self
672    }
673}
674
675impl RouteGuard for PermissionGuard {
676    type Future = Pin<Box<dyn Future<Output = GuardResult> + Send>>;
677
678    fn check(&self, cx: &App, _request: &NavigationRequest) -> Self::Future {
679        let has_perm = (self.check_fn)(cx, &self.permission);
680        let result = if has_perm {
681            GuardResult::allow()
682        } else if let Some(redirect) = &self.redirect_path {
683            GuardResult::redirect_with_reason(
684                redirect,
685                format!("Missing permission: {}", self.permission),
686            )
687        } else {
688            GuardResult::deny(format!("Missing permission: {}", self.permission))
689        };
690
691        Box::pin(async move { result })
692    }
693
694    fn name(&self) -> &str {
695        "PermissionGuard"
696    }
697
698    fn priority(&self) -> i32 {
699        80
700    }
701}
702
703// ============================================================================
704// Guard Composition
705// ============================================================================
706
707// ============================================================================
708// Guard Composition
709// ============================================================================
710
711/// Combines multiple guards with AND logic
712///
713/// All guards must allow navigation for the combined guard to allow.
714/// If any guard denies or redirects, that result is returned.
715///
716/// # Example
717///
718/// ```ignore
719/// use gpui_navigator::{Guards, AuthGuard, RoleGuard};
720///
721/// // Builder syntax for combining guards
722/// let guard = Guards::builder()
723///     .guard(AuthGuard::new(|_| true, "/login"))
724///     .guard(RoleGuard::new(|_| Some("admin".into()), "admin", None::<&str>))
725///     .build();
726/// ```
727pub struct Guards {
728    guards: Vec<BoxedGuard>,
729}
730
731impl Guards {
732    /// Create a new AND composition of guards
733    ///
734    /// # Example
735    /// ```ignore
736    /// use gpui_navigator::{Guards, BoxedGuard};
737    ///
738    /// let guard = Guards::new(vec![
739    ///     // Add boxed guards here
740    /// ]);
741    /// ```
742    pub fn new(guards: Vec<BoxedGuard>) -> Self {
743        Self { guards }
744    }
745
746    /// Create from individual guards (auto-boxing)
747    pub fn from_guards(guards: impl IntoIterator<Item = BoxedGuard>) -> Self {
748        Self {
749            guards: guards.into_iter().collect(),
750        }
751    }
752
753    /// Start building a guard composition
754    pub fn builder() -> GuardBuilder {
755        GuardBuilder::new()
756    }
757}
758
759/// Helper macro for creating Guards composition
760///
761/// # Example
762/// ```ignore
763/// use gpui_navigator::{guards, AuthGuard, RoleGuard};
764///
765/// let guard = guards![
766///     AuthGuard::new(|_| true, "/login"),
767/// ];
768/// ```
769#[macro_export]
770macro_rules! guards {
771($($guard:expr),* $(,)?) => {
772$crate::guards::Guards::new(
773    vec![$(Box::new($guard) as Box<dyn $crate::guards::RouteGuard>),*]
774)
775};
776}
777
778/// Builder for Guards with fluent API
779pub struct GuardBuilder {
780    guards: Vec<BoxedGuard>,
781}
782
783impl GuardBuilder {
784    /// Create a new builder
785    pub fn new() -> Self {
786        Self { guards: Vec::new() }
787    }
788
789    /// Add a guard to the composition
790    pub fn guard<G>(mut self, guard: G) -> Self
791    where
792        G: RouteGuard<Future = Pin<Box<dyn Future<Output = GuardResult> + Send>>>,
793    {
794        self.guards.push(Box::new(guard));
795        self
796    }
797
798    /// Add a boxed guard
799    pub fn boxed_guard(mut self, guard: BoxedGuard) -> Self {
800        self.guards.push(guard);
801        self
802    }
803
804    /// Build the final Guards
805    pub fn build(self) -> Guards {
806        Guards::new(self.guards)
807    }
808}
809
810impl Default for GuardBuilder {
811    fn default() -> Self {
812        Self::new()
813    }
814}
815
816impl RouteGuard for Guards {
817    type Future = Pin<Box<dyn Future<Output = GuardResult> + Send + 'static>>;
818
819    fn check(&self, cx: &App, request: &NavigationRequest) -> Self::Future {
820        // Execute guards in priority order
821        let mut sorted_guards: Vec<_> = self.guards.iter().collect();
822        sorted_guards.sort_by_key(|g| -g.priority());
823
824        let mut futures = Vec::new();
825        for guard in sorted_guards {
826            futures.push(guard.check(cx, request));
827        }
828
829        Box::pin(async move {
830            for future in futures {
831                match future.await {
832                    GuardResult::Allow => continue,
833                    other => return other,
834                }
835            }
836            GuardResult::Allow
837        })
838    }
839
840    fn name(&self) -> &str {
841        "Guards"
842    }
843
844    fn priority(&self) -> i32 {
845        // Priority is max of all child guards
846        self.guards.iter().map(|g| g.priority()).max().unwrap_or(0)
847    }
848}
849
850/// Inverts a guard result
851///
852/// Allow becomes Deny, Deny becomes Allow, Redirect is preserved.
853///
854/// # Example
855///
856/// ```ignore
857/// use gpui_navigator::{NotGuard, AuthGuard};
858///
859/// // Allow only if NOT authenticated (for login page)
860/// let guard = NotGuard::new(AuthGuard::new(|_| true, "/login"));
861/// ```
862pub struct NotGuard {
863    guard: BoxedGuard,
864}
865
866impl NotGuard {
867    /// Create a new NOT guard
868    pub fn new<G>(guard: G) -> Self
869    where
870        G: RouteGuard<Future = Pin<Box<dyn Future<Output = GuardResult> + Send>>>,
871    {
872        Self {
873            guard: Box::new(guard),
874        }
875    }
876
877    /// Create from a boxed guard
878    pub fn from_boxed(guard: BoxedGuard) -> Self {
879        Self { guard }
880    }
881}
882
883impl RouteGuard for NotGuard {
884    type Future = Pin<Box<dyn Future<Output = GuardResult> + Send + 'static>>;
885
886    fn check(&self, cx: &App, request: &NavigationRequest) -> Self::Future {
887        let future = self.guard.check(cx, request);
888
889        Box::pin(async move {
890            match future.await {
891                GuardResult::Allow => GuardResult::deny("Inverted: guard allowed but NOT expected"),
892                GuardResult::Deny { .. } => GuardResult::Allow,
893                redirect @ GuardResult::Redirect { .. } => redirect, // Preserve redirect
894            }
895        })
896    }
897
898    fn name(&self) -> &str {
899        "NotGuard"
900    }
901
902    fn priority(&self) -> i32 {
903        self.guard.priority()
904    }
905}
906
907// Additional imports for composition tests
908
909#[cfg(test)]
910mod auth_tests {
911    use super::*;
912    use gpui::TestAppContext;
913
914    #[gpui::test]
915    fn test_auth_guard_allows_authenticated(cx: &mut TestAppContext) {
916        // Create guard that always returns authenticated
917        let guard = AuthGuard::new(|_| true, "/login");
918        assert_eq!(guard.name(), "AuthGuard");
919        assert_eq!(guard.priority(), 100);
920
921        let request = NavigationRequest::new("/dashboard".to_string());
922        let result = cx.update(|cx| pollster::block_on(guard.check(cx, &request)));
923
924        assert!(result.is_allow());
925    }
926
927    #[gpui::test]
928    fn test_auth_guard_blocks_unauthenticated(cx: &mut TestAppContext) {
929        // Create guard that always returns not authenticated
930        let guard = AuthGuard::new(|_| false, "/login");
931        let request = NavigationRequest::new("/dashboard".to_string());
932
933        let result = cx.update(|cx| pollster::block_on(guard.check(cx, &request)));
934
935        assert!(result.is_redirect());
936        assert_eq!(result.redirect_path(), Some("/login"));
937    }
938
939    #[gpui::test]
940    fn test_role_guard_allows_correct_role(cx: &mut TestAppContext) {
941        // Create guard that returns "admin" role
942        let guard = RoleGuard::new(|_| Some("admin".to_string()), "admin", None::<String>);
943        assert_eq!(guard.name(), "RoleGuard");
944        assert_eq!(guard.priority(), 90);
945
946        let request = NavigationRequest::new("/admin".to_string());
947        let result = cx.update(|cx| pollster::block_on(guard.check(cx, &request)));
948
949        assert!(result.is_allow());
950    }
951
952    #[gpui::test]
953    fn test_role_guard_with_redirect(cx: &mut TestAppContext) {
954        // Create guard that returns wrong role
955        let guard = RoleGuard::new(|_| Some("user".to_string()), "admin", Some("/403"));
956        let request = NavigationRequest::new("/admin".to_string());
957
958        let result = cx.update(|cx| pollster::block_on(guard.check(cx, &request)));
959
960        assert!(result.is_redirect());
961        assert_eq!(result.redirect_path(), Some("/403"));
962    }
963
964    #[gpui::test]
965    fn test_role_guard_deny_without_redirect(cx: &mut TestAppContext) {
966        // Create guard that returns no role
967        let guard = RoleGuard::new(|_| None, "admin", None::<String>);
968        let request = NavigationRequest::new("/admin".to_string());
969
970        let result = cx.update(|cx| pollster::block_on(guard.check(cx, &request)));
971
972        assert!(result.is_deny());
973    }
974
975    #[gpui::test]
976    fn test_permission_guard_allows(cx: &mut TestAppContext) {
977        // Create guard that always allows
978        let guard = PermissionGuard::new(|_, _| true, "users.delete");
979        assert_eq!(guard.name(), "PermissionGuard");
980
981        let request = NavigationRequest::new("/users/123/delete".to_string());
982        let result = cx.update(|cx| pollster::block_on(guard.check(cx, &request)));
983
984        assert!(result.is_allow());
985    }
986
987    #[gpui::test]
988    fn test_permission_guard_denies(cx: &mut TestAppContext) {
989        // Create guard that always denies
990        let guard = PermissionGuard::new(|_, _| false, "users.delete");
991        let request = NavigationRequest::new("/users/123/delete".to_string());
992
993        let result = cx.update(|cx| pollster::block_on(guard.check(cx, &request)));
994
995        assert!(result.is_deny());
996    }
997
998    #[gpui::test]
999    fn test_permission_guard_with_redirect(cx: &mut TestAppContext) {
1000        let guard = PermissionGuard::new(|_, _| false, "users.delete").with_redirect("/forbidden");
1001        let request = NavigationRequest::new("/users/123/delete".to_string());
1002
1003        let result = cx.update(|cx| pollster::block_on(guard.check(cx, &request)));
1004
1005        assert!(result.is_redirect());
1006        assert_eq!(result.redirect_path(), Some("/forbidden"));
1007    }
1008}