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}