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 == ¶m_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}