Skip to main content

gpui_navigator/
error.rs

1//! Error handling for the router.
2//!
3//! This module defines the types returned when a navigation attempt cannot
4//! complete successfully:
5//!
6//! - [`NavigationResult`] — the top-level outcome of any navigation
7//!   (`Success`, `NotFound`, `Blocked`, `Error`).
8//! - [`NavigationError`] — a detailed error variant (route not found, guard
9//!   blocked, invalid params, etc.).
10//! - [`ErrorHandlers`] — a builder for registering custom 404 and error page
11//!   renderers.
12//!
13//! # Examples
14//!
15//! ```
16//! use gpui_navigator::error::NavigationResult;
17//!
18//! let result = NavigationResult::Success { path: "/home".into() };
19//! assert!(result.is_success());
20//!
21//! let blocked = NavigationResult::Blocked {
22//!     reason: "Not authenticated".into(),
23//!     redirect: Some("/login".into()),
24//! };
25//! assert_eq!(blocked.redirect_path(), Some("/login"));
26//! ```
27
28use gpui::{AnyElement, App};
29use std::fmt;
30use std::sync::Arc;
31
32// ============================================================================
33// Navigation Result Types
34// ============================================================================
35
36/// Outcome of a navigation attempt through the guard/middleware pipeline.
37///
38/// Every call to [`GlobalRouter::push`](crate::context::GlobalRouter::push)
39/// (and friends) returns this enum.
40#[derive(Debug, Clone)]
41#[non_exhaustive]
42pub enum NavigationResult {
43    /// Navigation succeeded.
44    Success {
45        /// The path that was navigated to.
46        path: String,
47    },
48    /// Route not found.
49    NotFound {
50        /// The path that could not be matched.
51        path: String,
52    },
53    /// Navigation blocked by a guard.
54    Blocked {
55        /// Human-readable reason the navigation was blocked.
56        reason: String,
57        /// Optional redirect path suggested by the guard.
58        redirect: Option<String>,
59    },
60    /// Navigation error
61    Error(NavigationError),
62}
63
64/// Detailed error variants that can occur during navigation.
65///
66/// Implements [`std::error::Error`] and [`Display`](std::fmt::Display) for
67/// idiomatic error handling.
68#[derive(Debug, Clone)]
69#[non_exhaustive]
70pub enum NavigationError {
71    /// Route not found.
72    RouteNotFound {
73        /// The path that could not be resolved.
74        path: String,
75    },
76
77    /// Guard blocked navigation.
78    GuardBlocked {
79        /// Reason the guard denied access.
80        reason: String,
81    },
82
83    /// Invalid route parameters.
84    InvalidParams {
85        /// Description of the parameter error.
86        message: String,
87    },
88
89    /// Navigation failed.
90    NavigationFailed {
91        /// Description of the failure.
92        message: String,
93    },
94
95    /// Custom application-specific error.
96    Custom {
97        /// Error message.
98        message: String,
99    },
100}
101
102impl fmt::Display for NavigationError {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match self {
105            Self::RouteNotFound { path } => {
106                write!(f, "Route not found: {path}")
107            }
108            Self::GuardBlocked { reason } => {
109                write!(f, "Navigation blocked: {reason}")
110            }
111            Self::InvalidParams { message } => {
112                write!(f, "Invalid parameters: {message}")
113            }
114            Self::NavigationFailed { message } => {
115                write!(f, "Navigation failed: {message}")
116            }
117            Self::Custom { message } => {
118                write!(f, "{message}")
119            }
120        }
121    }
122}
123
124impl std::error::Error for NavigationError {}
125
126impl NavigationResult {
127    /// Check if navigation was successful
128    #[must_use]
129    pub const fn is_success(&self) -> bool {
130        matches!(self, Self::Success { .. })
131    }
132
133    /// Check if route was not found
134    #[must_use]
135    pub const fn is_not_found(&self) -> bool {
136        matches!(self, Self::NotFound { .. })
137    }
138
139    /// Check if navigation was blocked
140    #[must_use]
141    pub const fn is_blocked(&self) -> bool {
142        matches!(self, Self::Blocked { .. })
143    }
144
145    /// Check if there was an error
146    #[must_use]
147    pub const fn is_error(&self) -> bool {
148        matches!(self, Self::Error(_))
149    }
150
151    /// Get redirect path if blocked with redirect
152    #[must_use]
153    pub fn redirect_path(&self) -> Option<&str> {
154        match self {
155            Self::Blocked {
156                redirect: Some(path),
157                ..
158            } => Some(path),
159            _ => None,
160        }
161    }
162}
163
164// ============================================================================
165// Error Handlers
166// ============================================================================
167
168/// Handler for navigation errors.
169///
170/// Takes `&App` (immutable) because rendering should not mutate application state.
171pub type ErrorHandler = Arc<dyn Fn(&App, &NavigationError) -> AnyElement + Send + Sync>;
172
173/// Handler for 404 not found.
174///
175/// Takes `&App` (immutable) because rendering should not mutate application state.
176pub type NotFoundHandler = Arc<dyn Fn(&App, &str) -> AnyElement + Send + Sync>;
177
178/// Builder for registering custom error-page renderers.
179///
180/// # Examples
181///
182/// ```ignore
183/// use gpui_navigator::error::ErrorHandlers;
184///
185/// let handlers = ErrorHandlers::new()
186///     .on_not_found(|cx, path| {
187///         gpui::div().child(format!("404: {path}")).into_any_element()
188///     })
189///     .on_error(|cx, err| {
190///         gpui::div().child(format!("Error: {err}")).into_any_element()
191///     });
192/// ```
193#[must_use]
194#[derive(Clone)]
195pub struct ErrorHandlers {
196    /// Handler for 404 not found errors
197    pub not_found: Option<NotFoundHandler>,
198
199    /// Handler for general navigation errors
200    pub error: Option<ErrorHandler>,
201}
202
203impl ErrorHandlers {
204    /// Create new empty error handlers
205    pub fn new() -> Self {
206        Self {
207            not_found: None,
208            error: None,
209        }
210    }
211
212    /// Set the 404 not found handler
213    pub fn on_not_found<F>(mut self, handler: F) -> Self
214    where
215        F: Fn(&App, &str) -> AnyElement + Send + Sync + 'static,
216    {
217        self.not_found = Some(Arc::new(handler));
218        self
219    }
220
221    /// Set the general error handler
222    pub fn on_error<F>(mut self, handler: F) -> Self
223    where
224        F: Fn(&App, &NavigationError) -> AnyElement + Send + Sync + 'static,
225    {
226        self.error = Some(Arc::new(handler));
227        self
228    }
229
230    /// Render a 404 not found page
231    pub fn render_not_found(&self, cx: &App, path: &str) -> Option<AnyElement> {
232        self.not_found.as_ref().map(|handler| handler(cx, path))
233    }
234
235    /// Render an error page
236    pub fn render_error(&self, cx: &App, error: &NavigationError) -> Option<AnyElement> {
237        self.error.as_ref().map(|handler| handler(cx, error))
238    }
239}
240
241impl Default for ErrorHandlers {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247// ============================================================================
248// Tests
249// ============================================================================
250
251#[cfg(test)]
252#[allow(clippy::needless_pass_by_ref_mut)]
253mod tests {
254    use super::*;
255    use gpui::{div, IntoElement, ParentElement, TestAppContext};
256
257    #[test]
258    fn test_navigation_result_success() {
259        let result = NavigationResult::Success {
260            path: "/home".to_string(),
261        };
262        assert!(result.is_success());
263        assert!(!result.is_not_found());
264        assert!(!result.is_blocked());
265        assert!(!result.is_error());
266    }
267
268    #[test]
269    fn test_navigation_result_not_found() {
270        let result = NavigationResult::NotFound {
271            path: "/invalid".to_string(),
272        };
273        assert!(!result.is_success());
274        assert!(result.is_not_found());
275    }
276
277    #[test]
278    fn test_navigation_result_blocked_with_redirect() {
279        let result = NavigationResult::Blocked {
280            reason: "Not authenticated".to_string(),
281            redirect: Some("/login".to_string()),
282        };
283        assert!(result.is_blocked());
284        assert_eq!(result.redirect_path(), Some("/login"));
285    }
286
287    #[test]
288    fn test_navigation_error_display() {
289        let error = NavigationError::RouteNotFound {
290            path: "/test".to_string(),
291        };
292        assert_eq!(error.to_string(), "Route not found: /test");
293    }
294
295    #[test]
296    fn test_error_handlers_creation() {
297        let handlers = ErrorHandlers::new();
298        assert!(handlers.not_found.is_none());
299        assert!(handlers.error.is_none());
300    }
301
302    #[gpui::test]
303    fn test_on_not_found(cx: &mut TestAppContext) {
304        let handlers = ErrorHandlers::new()
305            .on_not_found(|_cx, path| div().child(format!("404: {path}")).into_any_element());
306
307        assert!(handlers.not_found.is_some());
308
309        let element = cx.read(|cx| handlers.render_not_found(cx, "/invalid"));
310        assert!(element.is_some());
311    }
312
313    #[gpui::test]
314    fn test_on_error(cx: &mut TestAppContext) {
315        let handlers = ErrorHandlers::new()
316            .on_error(|_cx, error| div().child(format!("Error: {error}")).into_any_element());
317
318        assert!(handlers.error.is_some());
319
320        let error = NavigationError::RouteNotFound {
321            path: "/test".to_string(),
322        };
323
324        let element = cx.read(|cx| handlers.render_error(cx, &error));
325        assert!(element.is_some());
326    }
327}