Skip to main content

fncc_runtime/
router.rs

1//! Runtime state and navigation logic for Native File-Based Routing (NFBR).
2
3/// A generic stack-based router for navigating between screens/routes.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct Router<R> {
6    stack: Vec<R>,
7}
8
9impl<R: Clone> Router<R> {
10    /// Create a new router initialized with an initial route.
11    pub fn new(initial: R) -> Self {
12        Self { stack: vec![initial] }
13    }
14
15    /// Return a reference to the currently active route.
16    pub fn current(&self) -> &R {
17        self.stack.last().expect("router stack must never be empty")
18    }
19
20    /// Push a new route onto the navigation stack.
21    pub fn push(&mut self, route: R) {
22        self.stack.push(route);
23    }
24
25    /// Pop the current route off the stack, returning to the previous route.
26    /// Returns `true` if a route was popped, or `false` if already at the root route.
27    pub fn pop(&mut self) -> bool {
28        if self.stack.len() > 1 {
29            self.stack.pop();
30            true
31        } else {
32            false
33        }
34    }
35
36    /// Replace the top route on the stack with a new route.
37    pub fn replace(&mut self, route: R) {
38        *self.stack.last_mut().expect("router stack must never be empty") = route;
39    }
40
41    /// Get the current navigation stack depth.
42    pub fn len(&self) -> usize {
43        self.stack.len()
44    }
45
46    /// Returns `true` if the navigation stack is empty.
47    pub fn is_empty(&self) -> bool {
48        self.stack.is_empty()
49    }
50
51    /// Get a slice of all routes in the navigation history stack.
52    pub fn stack(&self) -> &[R] {
53        &self.stack
54    }
55
56    /// Navigate to a route parsed from a deep link URI string.
57    /// Returns `true` if the URI parsed successfully into a route and was pushed onto the stack.
58    pub fn push_uri<F>(&mut self, uri: &str, parser: F) -> bool
59    where
60        F: FnOnce(&str) -> Option<R>,
61    {
62        if let Some(route) = parser(uri) {
63            self.push(route);
64            true
65        } else {
66            false
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[derive(Debug, Clone, PartialEq, Eq)]
76    enum TestRoute {
77        Home,
78        Settings,
79        User { id: String },
80    }
81
82    impl TestRoute {
83        fn from_uri(uri: &str) -> Option<Self> {
84            match uri {
85                "/" | "/home" => Some(TestRoute::Home),
86                "/settings" => Some(TestRoute::Settings),
87                s if s.starts_with("/user/") => Some(TestRoute::User {
88                    id: s.trim_start_matches("/user/").to_string(),
89                }),
90                _ => None,
91            }
92        }
93    }
94
95    #[test]
96    fn test_router_initial_state() {
97        let router = Router::new(TestRoute::Home);
98        assert_eq!(router.current(), &TestRoute::Home);
99        assert_eq!(router.len(), 1);
100        assert!(!router.is_empty());
101    }
102
103    #[test]
104    fn test_router_push() {
105        let mut router = Router::new(TestRoute::Home);
106        router.push(TestRoute::Settings);
107        assert_eq!(router.current(), &TestRoute::Settings);
108        assert_eq!(router.len(), 2);
109    }
110
111    #[test]
112    fn test_router_pop() {
113        let mut router = Router::new(TestRoute::Home);
114        router.push(TestRoute::Settings);
115
116        assert!(router.pop());
117        assert_eq!(router.current(), &TestRoute::Home);
118        assert_eq!(router.len(), 1);
119
120        // Cannot pop root route
121        assert!(!router.pop());
122        assert_eq!(router.current(), &TestRoute::Home);
123    }
124
125    #[test]
126    fn test_router_replace() {
127        let mut router = Router::new(TestRoute::Home);
128        router.replace(TestRoute::Settings);
129        assert_eq!(router.current(), &TestRoute::Settings);
130        assert_eq!(router.len(), 1);
131    }
132
133    #[test]
134    fn test_router_push_uri() {
135        let mut router = Router::new(TestRoute::Home);
136
137        assert!(router.push_uri("/settings", TestRoute::from_uri));
138        assert_eq!(router.current(), &TestRoute::Settings);
139
140        assert!(router.push_uri("/user/42", TestRoute::from_uri));
141        assert_eq!(router.current(), &TestRoute::User { id: "42".to_string() });
142
143        assert!(!router.push_uri("/invalid", TestRoute::from_uri));
144        assert_eq!(router.current(), &TestRoute::User { id: "42".to_string() });
145    }
146}