Skip to main content

gpui_navigator/
middleware.rs

1//! Route middleware trait and types
2//!
3//! Middleware processes navigation requests before and after navigation occurs.
4//! Unlike guards (which decide IF navigation happens), middleware handles
5//! cross-cutting concerns like logging, metrics, context setup, etc.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use gpui_navigator::{RouteMiddleware, NavigationRequest};
11//! use std::future::Future;
12//! use std::pin::Pin;
13//!
14//! struct LoggingMiddleware;
15//!
16//! impl RouteMiddleware for LoggingMiddleware {
17//!     type Future = Pin<Box<dyn Future<Output = ()> + Send>>;
18//!
19//!     fn before_navigation(&self, cx: &gpui::App, request: &NavigationRequest) -> Self::Future {
20//!         println!("Navigating to: {}", request.to);
21//!         Box::pin(async {})
22//!     }
23//!
24//!     fn after_navigation(&self, cx: &gpui::App, request: &NavigationRequest) -> Self::Future {
25//!         println!("Navigated to: {}", request.to);
26//!         Box::pin(async {})
27//!     }
28//! }
29//! ```
30use crate::NavigationRequest;
31use gpui::App;
32use std::future::Future;
33
34/// Middleware that processes navigation requests.
35///
36/// Middleware runs code before and after navigation, allowing you to:
37/// - Log navigation events
38/// - Track analytics
39/// - Set up context in App
40/// - Handle cleanup
41/// - Measure performance
42///
43/// This trait uses associated Future types for zero-cost abstractions
44/// and efficient composition.
45///
46/// # Example
47///
48/// ```ignore
49/// use gpui_navigator::{RouteMiddleware, NavigationRequest};
50/// use std::future::Future;
51/// use std::pin::Pin;
52///
53/// struct AnalyticsMiddleware;
54///
55/// impl RouteMiddleware for AnalyticsMiddleware {
56///     type Future = Pin<Box<dyn Future<Output = ()> + Send>>;
57///
58///     fn before_navigation(&self, cx: &gpui::App, request: &NavigationRequest) -> Self::Future {
59///         let path = request.to.clone();
60///         Box::pin(async move {
61///             // Track page view
62///             println!("Tracking page view: {}", path);
63///         })
64///     }
65///
66///     fn after_navigation(&self, cx: &gpui::App, request: &NavigationRequest) -> Self::Future {
67///         Box::pin(async {})
68///     }
69/// }
70/// ```
71pub trait RouteMiddleware: Send + Sync + 'static {
72    /// The future returned by middleware methods
73    type Future: Future<Output = ()> + Send + 'static;
74
75    /// Called before navigation occurs
76    ///
77    /// Use this to:
78    /// - Log the navigation attempt
79    /// - Set up context in App
80    /// - Start timers for performance tracking
81    /// - Validate preconditions
82    ///
83    /// # Example
84    ///
85    /// ```no_run,ignore
86    /// fn before_navigation(&self, cx: &App, request: &NavigationRequest) -> Self::Future {
87    ///     println!("Navigating to: {}", request.to);
88    ///     Box::pin(async {})
89    /// }
90    /// ```
91    fn before_navigation(&self, cx: &App, request: &NavigationRequest) -> Self::Future;
92
93    /// Called after navigation completes successfully
94    ///
95    /// Use this to:
96    /// - Log successful navigation
97    /// - Track analytics
98    /// - Clean up resources
99    /// - Calculate metrics
100    ///
101    /// # Example
102    ///
103    /// ```no_run,ignore
104    /// fn after_navigation(&self, cx: &App, request: &NavigationRequest) -> Self::Future {
105    ///     println!("Successfully navigated to: {}", request.to);
106    ///     Box::pin(async {})
107    /// }
108    /// ```
109    fn after_navigation(&self, cx: &App, request: &NavigationRequest) -> Self::Future;
110
111    /// Middleware name for debugging
112    fn name(&self) -> &str {
113        "RouteMiddleware"
114    }
115
116    /// Middleware priority (higher runs first)
117    ///
118    /// Default is 0. Use this to control execution order when multiple
119    /// middleware are composed.
120    fn priority(&self) -> i32 {
121        0
122    }
123}
124
125/// Helper to create middleware from functions
126///
127/// # Example
128///
129/// ```ignore
130/// use gpui_navigator::middleware_fn;
131///
132/// let middleware = middleware_fn(
133///     |_cx, request| {
134///         println!("Before: {}", request.to);
135///         Box::pin(async {})
136///     },
137///     |_cx, request| {
138///         println!("After: {}", request.to);
139///         Box::pin(async {})
140///     },
141/// );
142/// ```
143pub fn middleware_fn<F, Fut>(before: F, after: F) -> FnMiddleware<F>
144where
145    F: Fn(&App, &NavigationRequest) -> Fut + Send + Sync + 'static,
146    Fut: Future<Output = ()> + Send + 'static,
147{
148    FnMiddleware { before, after }
149}
150
151/// Middleware created from functions
152pub struct FnMiddleware<F> {
153    before: F,
154    after: F,
155}
156
157impl<F, Fut> RouteMiddleware for FnMiddleware<F>
158where
159    F: Fn(&App, &NavigationRequest) -> Fut + Send + Sync + 'static,
160    Fut: Future<Output = ()> + Send + 'static,
161{
162    type Future = Fut;
163
164    fn before_navigation(&self, cx: &App, request: &NavigationRequest) -> Self::Future {
165        (self.before)(cx, request)
166    }
167
168    fn after_navigation(&self, cx: &App, request: &NavigationRequest) -> Self::Future {
169        (self.after)(cx, request)
170    }
171}
172
173/// Type-erased middleware for dynamic dispatch
174pub type BoxedMiddleware =
175    Box<dyn RouteMiddleware<Future = std::pin::Pin<Box<dyn Future<Output = ()> + Send>>>>;
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use gpui::TestAppContext;
181    use std::pin::Pin;
182    use std::sync::{Arc, Mutex};
183
184    struct TestMiddleware {
185        calls: Arc<Mutex<Vec<String>>>,
186    }
187
188    impl RouteMiddleware for TestMiddleware {
189        type Future = Pin<Box<dyn Future<Output = ()> + Send>>;
190
191        fn before_navigation(&self, _cx: &App, request: &NavigationRequest) -> Self::Future {
192            self.calls
193                .lock()
194                .unwrap()
195                .push(format!("before:{}", request.to));
196            Box::pin(async {})
197        }
198
199        fn after_navigation(&self, _cx: &App, request: &NavigationRequest) -> Self::Future {
200            self.calls
201                .lock()
202                .unwrap()
203                .push(format!("after:{}", request.to));
204            Box::pin(async {})
205        }
206    }
207
208    #[gpui::test]
209    async fn test_middleware_before(cx: &mut TestAppContext) {
210        let calls = Arc::new(Mutex::new(Vec::new()));
211        let middleware = TestMiddleware {
212            calls: calls.clone(),
213        };
214        let request = NavigationRequest::new("/test".to_string());
215
216        cx.update(|cx| pollster::block_on(middleware.before_navigation(cx, &request)));
217
218        let log = calls.lock().unwrap();
219        assert_eq!(log.len(), 1);
220        assert_eq!(log[0], "before:/test");
221    }
222
223    #[gpui::test]
224    async fn test_middleware_after(cx: &mut TestAppContext) {
225        let calls = Arc::new(Mutex::new(Vec::new()));
226        let middleware = TestMiddleware {
227            calls: calls.clone(),
228        };
229        let request = NavigationRequest::new("/test".to_string());
230
231        cx.update(|cx| pollster::block_on(middleware.after_navigation(cx, &request)));
232
233        let log = calls.lock().unwrap();
234        assert_eq!(log.len(), 1);
235        assert_eq!(log[0], "after:/test");
236    }
237
238    #[test]
239    fn test_middleware_name() {
240        let calls = Arc::new(Mutex::new(Vec::new()));
241        let middleware = TestMiddleware { calls };
242        assert_eq!(middleware.name(), "RouteMiddleware");
243    }
244
245    #[test]
246    fn test_middleware_priority() {
247        let calls = Arc::new(Mutex::new(Vec::new()));
248        let middleware = TestMiddleware { calls };
249        assert_eq!(middleware.priority(), 0);
250    }
251}