percy_router/
route.rs

1use std::fmt::Debug;
2use std::fmt::Error;
3use std::fmt::Formatter;
4use std::str::FromStr;
5
6/// Enables a type to be used as a route paramer
7///
8/// ```ignore
9/// // Example of a route param that only matches id's that are less than
10/// // 10 characters long
11///
12/// #[route(path = "/path/to/:id")
13/// fn my_route (id: ShortId) -> VirtualNode {
14/// }
15///
16/// struct ShortId {
17///     id: String,
18///     length: usize
19/// }
20///
21/// impl RouteParam for MyCustomType {
22///     fn from_str (param: &str) -> Result<MyCustomType, ()> {
23///         if param.len() > 10 {
24///             Ok(MyCustomType {
25///                 length: param.len(), id: param.to_string()
26///             })
27///         } else {
28///             Err(())
29///         }
30///     }
31/// }
32/// ```
33pub trait RouteParam {
34    /// Given some parameter, return Self
35    ///
36    /// For example, for the route path:
37    ///
38    ///   /route/path/:id
39    ///
40    /// And incoming path
41    ///
42    ///   /route/path/55
43    ///
44    /// If Self is a u32 we would return 55
45    fn from_str_param(param: &str) -> Result<Self, &str>
46    where
47        Self: Sized;
48}
49
50impl<T> RouteParam for T
51where
52    T: FromStr,
53{
54    fn from_str_param(param: &str) -> Result<T, &str> {
55        match param.parse::<T>() {
56            Ok(parsed) => Ok(parsed),
57            Err(_) => Err(param),
58        }
59    }
60}
61
62/// Given a param_key &str and a param_val &str, get the corresponding route parameter
63///
64/// ex: ("friend_count", "30")
65pub type ParseRouteParam = Box<dyn Fn(&str, &str) -> Option<Box<dyn RouteParam>>>;
66
67/// A route specifies a path to match against. When a match is found a `view_creator` is used
68/// to return an `impl View` that can be used to render the appropriate content for that route.
69pub struct Route {
70    route_definition: &'static str,
71    // FIXME: Do we need this to return the RouteParam ... or do we really just need a bool
72    // to check if the route exists? Seems like we're only using the boolean
73    route_param_parser: ParseRouteParam,
74}
75
76impl Debug for Route {
77    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
78        f.write_str(self.route_definition)?;
79        Ok(())
80    }
81}
82
83impl Route {
84    /// Create a new Route. You'll usually later call route.match(...) in order to see if a given
85    /// the path in the browser URL matches your route's path definition.
86    pub fn new(route_definition: &'static str, route_param_parser: ParseRouteParam) -> Route {
87        Route {
88            route_definition,
89            route_param_parser,
90        }
91    }
92}
93
94impl Route {
95    /// Determine whether or not our route matches a provided path.
96    ///
97    /// # Example
98    ///
99    /// ```rust,ignore
100    /// // path = "/food/:food_type"
101    ///
102    /// route.matches("/food/tacos");
103    /// ```
104    pub fn matches(&self, path: &str) -> bool {
105        // ex: [ "", "food", ":food_type" ]
106        let defined_segments = self
107            .route_definition
108            .split("/")
109            .filter(|segment| segment.len() > 0)
110            .collect::<Vec<&str>>();
111
112        // ex: [ "", "food", "tacos" ]
113        let incoming_segments = path
114            .split("/")
115            .filter(|segment| segment.len() > 0)
116            .collect::<Vec<&str>>();
117
118        // If we defined a certain number of segments and we don't see the same amount in
119        // the incoming route, we know that it isn't a match
120        if defined_segments.len() != incoming_segments.len() {
121            return false;
122        }
123
124        // Iterate over every segment and verify that they match, or if it's a
125        // RouteParam segment verify that we can parse it
126        for (index, defined_segment) in defined_segments.iter().enumerate() {
127            if defined_segment.len() == 0 {
128                continue;
129            }
130
131            let mut chars = defined_segment.chars();
132
133            let first_char = chars.next().unwrap();
134
135            // ex: ":food_type"
136            if first_char == ':' {
137                // food_type
138                let param_name = chars.collect::<String>();
139
140                // tacos
141                let incoming_param_value = incoming_segments[index];
142
143                let matches =
144                    (self.route_param_parser)(param_name.as_str(), incoming_param_value).is_some();
145                if !matches {
146                    return false;
147                }
148            } else {
149                // Compare segments on the same level
150                let incoming_segment = incoming_segments[index];
151
152                if defined_segment != &incoming_segment {
153                    return false;
154                }
155            }
156        }
157
158        true
159    }
160
161    /// Given an incoming path and a param_key, get the RouteParam
162    pub fn find_route_param<'a>(&self, incoming_path: &'a str, param_key: &str) -> Option<&'a str> {
163        let param_key = format!(":{}", param_key);
164
165        let mut incoming_segments = incoming_path.split("/");
166
167        for (idx, defined_segment) in self.route_definition.split("/").enumerate() {
168            if defined_segment == &param_key {
169                for _ in 0..idx {
170                    incoming_segments.next().unwrap();
171                }
172                return Some(incoming_segments.next().unwrap());
173            }
174        }
175
176        None
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    /// Verify that we can get the route parameter value for a given route parameter.
185    ///
186    /// For a route definition is `/:id`, and a path "/5" we confirm that when we ask for the
187    /// "id" parameter of the path we get back "5".
188    #[test]
189    fn find_route_param() {
190        let route = Route::new(
191            "/:id",
192            Box::new(|param_key, param_val| {
193                if param_key == "id" {
194                    Some(Box::new(u32::from_str_param(param_val).unwrap()));
195                }
196
197                None
198            }),
199        );
200
201        assert_eq!(route.find_route_param("/5", "id"), Some("5"));
202    }
203
204    /// Verify that route parameters such as `/:id` are typed.
205    ///
206    /// For instance, confirm that a route parameter that has an integer type does not match a
207    /// string.
208    #[test]
209    fn route_type_safety() {
210        MatchRouteTestCase {
211            route_definition: "/users/:id",
212            matches: vec![
213                // Verify that an integer matches a parameter that is defined as an integer.
214                ("/users/5", true),
215                // Verify that a string does not match a parameter that is defined as an integer.
216                ("/users/foo", false),
217            ],
218        }
219            .test();
220    }
221
222    /// Verify that a `/` route definition doesn't capture `/some-route`.
223    #[test]
224    fn route_cascade() {
225        MatchRouteTestCase {
226            route_definition: "/",
227            matches: vec![
228                // Verify that a "/" segment is not a catch-all.
229                // So, "/" should not match "/foo".
230                ("/foo", false),
231            ],
232        }
233            .test();
234    }
235
236    /// Verify that we correctly match when a static segment comes after a dynamic segment.
237    ///
238    /// This helps ensure that are not ignoring segments that come after a dynamic segment.
239    #[test]
240    fn static_segment_after_dynamic_segment() {
241        MatchRouteTestCase {
242            route_definition: "/:id/foo",
243            matches: vec![
244                // Verify that a correct segment after a dynamic segment leads to a match.
245                ("/5/foo", true),
246                // Verify that an incorrect segment after a dynamic segment does not lead to a match.
247                ("/5/bar", false),
248            ],
249        }
250            .test();
251    }
252
253    struct MatchRouteTestCase {
254        route_definition: &'static str,
255        // (path, should it match)
256        matches: Vec<(&'static str, bool)>,
257    }
258
259    impl MatchRouteTestCase {
260        fn test(&self) {
261            fn get_param(param_key: &str, param_val: &str) -> Option<Box<dyn RouteParam>> {
262                // /some/route/path/:id/
263                match param_key {
264                    "id" => match u32::from_str_param(param_val) {
265                        Ok(num) => Some(Box::new(num)),
266                        Err(_) => None,
267                    },
268                    _ => None,
269                }
270            }
271
272            let route = Route::new(self.route_definition, Box::new(get_param));
273
274            for match_case in self.matches.iter() {
275                assert_eq!(
276                    route.matches(match_case.0),
277                    match_case.1,
278                    "Test case failed: {}",
279                    match_case.0,
280                );
281            }
282        }
283    }
284}