yew_nav_link/hooks/route_info/
breadcrumbs.rs1use std::rc::Rc;
5
6use yew::prelude::*;
7use yew_router::prelude::*;
8
9pub trait BreadcrumbLabelProvider: Send + Sync {
11 fn label_for_path(&self, path: &str) -> String;
13}
14
15#[derive(Clone)]
28pub struct BreadcrumbLabelProviderContext(Rc<dyn BreadcrumbLabelProvider>);
29
30impl BreadcrumbLabelProviderContext {
31 #[must_use]
33 pub fn new(provider: Rc<dyn BreadcrumbLabelProvider>) -> Self {
34 Self(provider)
35 }
36
37 #[must_use]
40 pub fn provider(&self) -> Rc<dyn BreadcrumbLabelProvider> {
41 Rc::clone(&self.0)
42 }
43}
44
45impl PartialEq for BreadcrumbLabelProviderContext {
46 fn eq(&self, other: &Self) -> bool {
47 Rc::ptr_eq(&self.0, &other.0)
48 }
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct BreadcrumbItem<R> {
54 pub route: R,
56 pub label: String,
58 pub is_active: bool
60}
61
62#[hook]
65pub fn use_breadcrumbs<R>() -> Vec<BreadcrumbItem<R>>
66where
67 R: Routable + Clone + PartialEq + 'static
68{
69 let current = use_route::<R>();
70 let provider = use_context::<BreadcrumbLabelProviderContext>();
71
72 current.map_or_else(Vec::new, |route| {
73 let path = route.to_path();
74 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
75 let mut items = Vec::new();
76 let mut built = String::new();
77 let root_label = provider
79 .as_ref()
80 .map_or_else(|| "/".to_string(), |p| p.0.label_for_path("/"));
81 items.push(BreadcrumbItem {
82 route: route.clone(),
83 label: root_label,
84 is_active: segments.is_empty()
85 });
86 let total = segments.len();
88 for (i, segment) in segments.iter().enumerate() {
89 built.push('/');
90 built.push_str(segment);
91 let is_last = i + 1 == total;
92 let label = provider
93 .as_ref()
94 .map_or_else(|| built.clone(), |p| p.0.label_for_path(&built));
95 items.push(BreadcrumbItem {
96 route: route.clone(),
97 label,
98 is_active: is_last
99 });
100 }
101 items
102 })
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[derive(Clone, PartialEq, Debug, Routable)]
110 enum SimpleRoute {
111 #[at("/")]
112 Home,
113 #[at("/about")]
114 About,
115 #[at("/docs")]
116 Docs,
117 #[at("/docs/api")]
118 Api,
119 #[at("/docs/api/v1")]
120 ApiV1
121 }
122
123 #[derive(Clone, PartialEq, Debug, Routable)]
124 enum ParamRoute {
125 #[at("/")]
126 Home,
127 #[at("/users/:id")]
128 User { id: String }
129 }
130
131 #[derive(Clone, PartialEq, Debug, Routable)]
132 enum RootOnlyRoute {
133 #[at("/")]
134 Root
135 }
136
137 struct TestLabelProvider;
138
139 impl BreadcrumbLabelProvider for TestLabelProvider {
140 fn label_for_path(&self, path: &str) -> String {
141 match path {
142 "/" => "Home".to_string(),
143 "/about" => "About".to_string(),
144 "/docs" => "Docs".to_string(),
145 "/docs/api" => "API".to_string(),
146 "/docs/api/v1" => "V1".to_string(),
147 "/users/42" => "User #42".to_string(),
148 _ => path.to_string()
149 }
150 }
151 }
152
153 #[test]
156 fn breadcrumb_item_new() {
157 let item = BreadcrumbItem {
158 route: SimpleRoute::Home,
159 label: "Home".to_string(),
160 is_active: true
161 };
162 assert_eq!(item.label, "Home");
163 assert!(item.is_active);
164 assert_eq!(item.route.to_path(), "/");
165 }
166
167 #[test]
168 fn breadcrumb_item_inactive() {
169 let item = BreadcrumbItem {
170 route: SimpleRoute::About,
171 label: "About".to_string(),
172 is_active: false
173 };
174 assert!(!item.is_active);
175 assert_eq!(item.label, "About");
176 }
177
178 #[test]
179 fn breadcrumb_item_clone_preserves_all_fields() {
180 let item1 = BreadcrumbItem {
181 route: SimpleRoute::Api,
182 label: "Root".to_string(),
183 is_active: true
184 };
185 let item2 = item1.clone();
186 assert_eq!(item1, item2);
187 assert_eq!(item2.label, "Root");
188 assert!(item2.is_active);
189 }
190
191 #[test]
192 fn breadcrumb_item_eq_with_same_values() {
193 let item1 = BreadcrumbItem {
194 route: SimpleRoute::Home,
195 label: "Home".to_string(),
196 is_active: true
197 };
198 let item2 = BreadcrumbItem {
199 route: SimpleRoute::Home,
200 label: "Home".to_string(),
201 is_active: true
202 };
203 assert_eq!(item1, item2);
204 }
205
206 #[test]
207 fn breadcrumb_item_neq_different_label() {
208 let item1 = BreadcrumbItem {
209 route: SimpleRoute::Home,
210 label: "Home".to_string(),
211 is_active: true
212 };
213 let item2 = BreadcrumbItem {
214 route: SimpleRoute::Home,
215 label: "Index".to_string(),
216 is_active: true
217 };
218 assert_ne!(item1, item2);
219 }
220
221 #[test]
222 fn breadcrumb_item_neq_different_state() {
223 let item1 = BreadcrumbItem {
224 route: SimpleRoute::Home,
225 label: "Home".to_string(),
226 is_active: true
227 };
228 let item2 = BreadcrumbItem {
229 route: SimpleRoute::Home,
230 label: "Home".to_string(),
231 is_active: false
232 };
233 assert_ne!(item1, item2);
234 }
235
236 #[test]
237 fn breadcrumb_item_neq_different_route() {
238 let item1 = BreadcrumbItem {
239 route: SimpleRoute::Docs,
240 label: "Docs".to_string(),
241 is_active: false
242 };
243 let item2 = BreadcrumbItem {
244 route: SimpleRoute::Api,
245 label: "Docs".to_string(),
246 is_active: false
247 };
248 assert_ne!(item1, item2);
249 }
250
251 #[test]
252 fn breadcrumb_item_debug_contains_all_fields() {
253 let item = BreadcrumbItem {
254 route: SimpleRoute::Home,
255 label: "Home".to_string(),
256 is_active: true
257 };
258 let debug_str = format!("{item:?}");
259 assert!(debug_str.contains("BreadcrumbItem"));
260 assert!(debug_str.contains("Home"));
261 assert!(debug_str.contains("is_active"));
262 }
263
264 #[test]
265 fn breadcrumb_item_long_label() {
266 let label = "Extremely long breadcrumb label to test string handling in various scenarios"
267 .to_string();
268 let item = BreadcrumbItem {
269 route: SimpleRoute::Home,
270 label: label.clone(),
271 is_active: false
272 };
273 assert_eq!(item.label, label);
274 assert!(!item.is_active);
275 }
276
277 #[test]
278 fn breadcrumb_item_short_label() {
279 let item = BreadcrumbItem {
280 route: SimpleRoute::Home,
281 label: "a".to_string(),
282 is_active: true
283 };
284 assert_eq!(item.label, "a");
285 }
286
287 #[test]
288 fn breadcrumb_item_clone_deep_copy() {
289 let item1 = BreadcrumbItem {
290 route: SimpleRoute::ApiV1,
291 label: "Deep".to_string(),
292 is_active: true
293 };
294 let item2 = item1.clone();
295 assert_eq!(item1, item2);
296 }
297
298 #[test]
299 fn breadcrumb_item_root_path() {
300 let item = BreadcrumbItem {
301 route: SimpleRoute::Home,
302 label: "/".to_string(),
303 is_active: true
304 };
305 assert_eq!(item.route.to_path(), "/");
306 }
307
308 #[test]
309 fn breadcrumb_item_nested_path() {
310 let item = BreadcrumbItem {
311 route: SimpleRoute::ApiV1,
312 label: "/docs/api/v1".to_string(),
313 is_active: true
314 };
315 assert_eq!(item.route.to_path(), "/docs/api/v1");
316 }
317
318 #[test]
321 fn breadcrumb_label_provider_returns_custom_labels() {
322 let provider = TestLabelProvider;
323 assert_eq!(provider.label_for_path("/"), "Home");
324 assert_eq!(provider.label_for_path("/about"), "About");
325 assert_eq!(provider.label_for_path("/docs/api/v1"), "V1");
326 }
327
328 #[test]
329 fn breadcrumb_label_provider_returns_path_for_unknown() {
330 let provider = TestLabelProvider;
331 assert_eq!(provider.label_for_path("/unknown/path"), "/unknown/path");
332 assert_eq!(provider.label_for_path("/missing"), "/missing");
333 }
334
335 #[test]
336 fn breadcrumb_label_provider_empty_path_not_root() {
337 let provider = TestLabelProvider;
338 assert_ne!(provider.label_for_path(""), "Home");
339 }
340
341 #[test]
342 fn breadcrumb_label_provider_whitespace() {
343 let provider = TestLabelProvider;
344 assert_eq!(provider.label_for_path(" "), " ");
345 }
346
347 #[test]
348 fn breadcrumb_label_provider_special_chars() {
349 let provider = TestLabelProvider;
350 assert_eq!(provider.label_for_path("@#$%"), "@#$%");
351 }
352
353 #[test]
356 fn context_eq_same_rc() {
357 let rc = Rc::new(TestLabelProvider);
358 let ctx1 = BreadcrumbLabelProviderContext(rc.clone());
359 let ctx2 = BreadcrumbLabelProviderContext(rc);
360 assert!(ctx1 == ctx2);
361 }
362
363 #[test]
364 fn context_neq_different_rc() {
365 let ctx1 = BreadcrumbLabelProviderContext(Rc::new(TestLabelProvider));
366 let ctx2 = BreadcrumbLabelProviderContext(Rc::new(TestLabelProvider));
367 assert!(ctx1 != ctx2);
368 }
369
370 #[test]
371 fn context_clone_preserves_identity() {
372 let rc = Rc::new(TestLabelProvider);
373 let ctx1 = BreadcrumbLabelProviderContext(rc);
374 let ctx2 = ctx1.clone();
375 assert!(ctx1 == ctx2);
376 }
377
378 #[test]
381 fn use_breadcrumbs_simple_route() {
382 let _result = use_breadcrumbs::<SimpleRoute>();
383 }
384
385 #[test]
386 fn use_breadcrumbs_param_route() {
387 let _result = use_breadcrumbs::<ParamRoute>();
388 }
389
390 #[test]
391 fn use_breadcrumbs_multiple_calls() {
392 let _r1 = use_breadcrumbs::<SimpleRoute>();
393 let _r2 = use_breadcrumbs::<SimpleRoute>();
394 let _r3 = use_breadcrumbs::<SimpleRoute>();
395 }
396
397 #[test]
398 fn use_breadcrumbs_root_only_route() {
399 let _result = use_breadcrumbs::<RootOnlyRoute>();
400 }
401
402 #[test]
403 fn use_breadcrumbs_all_route_types() {
404 let _simple = use_breadcrumbs::<SimpleRoute>();
405 let _param = use_breadcrumbs::<ParamRoute>();
406 let _root = use_breadcrumbs::<RootOnlyRoute>();
407 }
408
409 #[test]
412 fn breadcrumb_item_neq_negatives() {
413 let item1 = BreadcrumbItem {
414 route: SimpleRoute::Home,
415 label: "Home".to_string(),
416 is_active: true
417 };
418 let mut item2 = item1.clone();
419 item2.label = "Other".to_string();
420 assert_ne!(item1, item2);
421 item2.is_active = false;
422 assert_ne!(item1, item2);
423 }
424}