1use gpui::{AnyElement, App};
29use std::fmt;
30use std::sync::Arc;
31
32#[derive(Debug, Clone)]
41#[non_exhaustive]
42pub enum NavigationResult {
43 Success {
45 path: String,
47 },
48 NotFound {
50 path: String,
52 },
53 Blocked {
55 reason: String,
57 redirect: Option<String>,
59 },
60 Error(NavigationError),
62}
63
64#[derive(Debug, Clone)]
69#[non_exhaustive]
70pub enum NavigationError {
71 RouteNotFound {
73 path: String,
75 },
76
77 GuardBlocked {
79 reason: String,
81 },
82
83 InvalidParams {
85 message: String,
87 },
88
89 NavigationFailed {
91 message: String,
93 },
94
95 Custom {
97 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 #[must_use]
129 pub const fn is_success(&self) -> bool {
130 matches!(self, Self::Success { .. })
131 }
132
133 #[must_use]
135 pub const fn is_not_found(&self) -> bool {
136 matches!(self, Self::NotFound { .. })
137 }
138
139 #[must_use]
141 pub const fn is_blocked(&self) -> bool {
142 matches!(self, Self::Blocked { .. })
143 }
144
145 #[must_use]
147 pub const fn is_error(&self) -> bool {
148 matches!(self, Self::Error(_))
149 }
150
151 #[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
164pub type ErrorHandler = Arc<dyn Fn(&App, &NavigationError) -> AnyElement + Send + Sync>;
172
173pub type NotFoundHandler = Arc<dyn Fn(&App, &str) -> AnyElement + Send + Sync>;
177
178#[must_use]
194#[derive(Clone)]
195pub struct ErrorHandlers {
196 pub not_found: Option<NotFoundHandler>,
198
199 pub error: Option<ErrorHandler>,
201}
202
203impl ErrorHandlers {
204 pub fn new() -> Self {
206 Self {
207 not_found: None,
208 error: None,
209 }
210 }
211
212 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 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 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 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#[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}