1use crate::naming::RouteSite;
2use crate::{App, Method, ServedRoute};
3
4impl App {
5 #[must_use]
7 pub fn posted_route(&self, post: &str) -> Option<RouteSite<'_>> {
8 let posts: Vec<(RouteSite, String)> = self
9 .route_sites()
10 .into_iter()
11 .filter(|site| site.route.method == Method::Post)
12 .filter_map(|site| Some((site, site.full_path(self)?)))
13 .collect();
14 let paths: Vec<String> = posts.iter().map(|(_, path)| path.clone()).collect();
15 posts.get(chosen(&paths, post)?).map(|(site, _)| *site)
16 }
17}
18
19impl ServedRoute<'_> {
20 #[must_use]
22 pub fn takes_post(&self, post: &str) -> bool {
23 self.route.method == "POST" && reaches(&self.path, post)
24 }
25}
26
27fn chosen(paths: &[String], post: &str) -> Option<usize> {
29 let literals = |path: &str| -> Vec<bool> {
30 path.split('/')
31 .map(|segment| !segment.starts_with('{'))
32 .collect()
33 };
34 paths
35 .iter()
36 .enumerate()
37 .filter(|(_, path)| reaches(path, post))
38 .max_by(|(left_index, left), (right_index, right)| {
39 literals(left)
40 .cmp(&literals(right))
41 .then(right_index.cmp(left_index))
42 })
43 .map(|(index, _)| index)
44}
45
46fn reaches(path: &str, post: &str) -> bool {
47 let (routes, posts): (Vec<&str>, Vec<&str>) =
48 (path.split('/').collect(), post.split('/').collect());
49 routes.len() == posts.len()
50 && routes
51 .iter()
52 .zip(&posts)
53 .all(|(route, posted)| route.starts_with('{') || route == posted)
54}
55
56#[cfg(test)]
57mod tests {
58 use super::{chosen, reaches};
59 use crate::{ListedRoute, ServedRoute};
60
61 #[test]
62 fn a_post_reaches_a_package_route_only_by_post() {
63 let listed = |method: &str| ListedRoute {
64 mount: "customer".to_owned(),
65 method: method.to_owned(),
66 path: "/sign-out".to_owned(),
67 handler: "routes::sign_out".to_owned(),
68 permission: None,
69 };
70 let served = |route| ServedRoute {
71 area: "web".to_owned(),
72 path: "/account/sign-out".to_owned(),
73 name: "sofie_identity.customer.sign_out".to_owned(),
74 route,
75 };
76 let (post, get) = (listed("POST"), listed("GET"));
77
78 assert!(served(&post).takes_post("/account/sign-out"));
79 assert!(!served(&get).takes_post("/account/sign-out"));
80 assert!(!served(&post).takes_post("/account/sign-in"));
81 }
82
83 #[test]
84 fn a_post_goes_to_the_route_the_router_prefers_a_literal_segment_over_a_parameter() {
85 let paths = [
86 "/orders/{order}".to_owned(),
87 "/orders/new".to_owned(),
88 "/orders/{order}/cancel".to_owned(),
89 ];
90
91 assert_eq!(chosen(&paths, "/orders/new"), Some(1));
92 assert_eq!(chosen(&paths, "/orders/{}"), Some(0));
93 assert_eq!(chosen(&paths, "/carts/new"), None);
94 }
95
96 #[test]
97 fn a_post_reaches_the_route_its_literal_segments_name() {
98 assert!(reaches("/account/orders", "/account/orders"));
99 assert!(reaches(
100 "/account/orders/{order}/cancel",
101 "/account/orders/{}/cancel"
102 ));
103 assert!(!reaches("/account/orders", "/account/orders/{}"));
104 assert!(!reaches(
105 "/account/orders/{order}/cancel",
106 "/account/orders/{}/ship"
107 ));
108 }
109}