Skip to main content

sofie_model/
posts.rs

1use crate::naming::RouteSite;
2use crate::{App, Method, ServedRoute};
3
4impl App {
5    /// The POST route the router serves `post` with; a bound path part is written `{}`.
6    #[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    /// Whether `post`, with a bound path part written `{}`, reaches this package route.
21    #[must_use]
22    pub fn takes_post(&self, post: &str) -> bool {
23        self.route.method == "POST" && reaches(&self.path, post)
24    }
25}
26
27/// Of the routes a post reaches, the one with a literal segment where another has a parameter.
28fn 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}