ratpack/
path.rs

1use crate::{Error, Params};
2
3#[derive(Debug, Clone, PartialOrd, PartialEq)]
4pub(crate) enum RoutePart {
5    PathComponent(String),
6    Param(String),
7    Leader,
8}
9
10#[derive(Debug, Clone, PartialOrd)]
11pub(crate) struct Path(Vec<RoutePart>);
12
13impl Eq for Path {}
14
15impl Ord for Path {
16    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
17        self.to_string().cmp(&other.to_string())
18    }
19}
20
21impl Path {
22    pub(crate) fn new(path: String) -> Self {
23        let mut parts = Self::default();
24
25        let path = path.trim_end_matches("/");
26
27        if !path.contains("/") {
28            return Self::default();
29        }
30
31        let args = path.split("/");
32
33        for arg in args {
34            if arg.starts_with(":") {
35                // is param
36                parts.push(RoutePart::Param(arg.trim_start_matches(":").to_string()));
37            } else if arg == "" {
38                // skip empties. this will push additional leaders if there is an duplicate slash
39                // (e.g.: `//one/two`), which will fail on matching; we don't want to support this
40                // syntax in the router.
41            } else {
42                // is not param
43                parts.push(RoutePart::PathComponent(arg.to_string()));
44            }
45        }
46
47        parts
48    }
49
50    pub(crate) fn push(&mut self, arg: RoutePart) -> Self {
51        self.0.push(arg);
52        self.clone()
53    }
54
55    /// This method lists all the params available to the path; useful for debugging.
56    #[allow(dead_code)]
57    pub(crate) fn params(&self) -> Vec<String> {
58        let mut params = Vec::new();
59        for arg in self.0.clone() {
60            match arg {
61                RoutePart::Param(p) => params.push(p),
62                _ => {}
63            }
64        }
65
66        params
67    }
68
69    pub(crate) fn extract(&self, provided: String) -> Result<Params, Error> {
70        let provided = provided.trim_end_matches("/");
71
72        if provided == "" && self.eq(&Self::default()) {
73            return Ok(Params::default());
74        }
75
76        let mut params = Params::default();
77
78        let parts: Vec<String> = provided
79            .split("/")
80            .map(|s| s.to_string())
81            .collect::<Vec<String>>();
82
83        if parts.len() != self.0.len() {
84            return Err(Error::new("invalid parameters"));
85        }
86
87        let mut i = 0;
88
89        for part in self.0.clone() {
90            match part {
91                RoutePart::Param(p) => params.insert(p, parts[i].clone()),
92                RoutePart::PathComponent(part) => {
93                    if part != parts[i] {
94                        return Err(Error::new("invalid path for parameter extraction"));
95                    }
96
97                    None
98                }
99                RoutePart::Leader => None,
100            };
101
102            i += 1
103        }
104
105        Ok(params)
106    }
107
108    pub(crate) fn matches(&self, s: String) -> bool {
109        self.eq(&Self::new(s))
110    }
111}
112
113impl PartialEq for Path {
114    fn eq(&self, other: &Self) -> bool {
115        if other.0.len() != self.0.len() {
116            return false;
117        }
118
119        let mut i = 0;
120        let mut leader_seen = false;
121        for arg in other.0.clone() {
122            let res = match self.0[i].clone() {
123                RoutePart::PathComponent(_) => self.0[i] == arg,
124                RoutePart::Param(_param) => {
125                    // FIXME advanced parameter shit here later
126                    true
127                }
128                RoutePart::Leader => {
129                    if leader_seen {
130                        false
131                    } else {
132                        leader_seen = true;
133                        true
134                    }
135                }
136            };
137
138            if !res {
139                return false;
140            }
141
142            i += 1;
143        }
144
145        true
146    }
147}
148
149impl Default for Path {
150    fn default() -> Self {
151        Self(vec![RoutePart::Leader])
152    }
153}
154
155impl ToString for Path {
156    fn to_string(&self) -> String {
157        let mut s = Vec::new();
158
159        for part in self.0.clone() {
160            s.push(match part {
161                RoutePart::PathComponent(pc) => pc.to_string(),
162                RoutePart::Param(param) => {
163                    format!(":{}", param)
164                }
165                RoutePart::Leader => "".to_string(),
166            });
167        }
168
169        if s.len() < 2 {
170            return "/".to_string();
171        }
172
173        s.join("/")
174    }
175}
176
177mod tests {
178    #[test]
179    fn test_path() {
180        use super::Path;
181        use crate::Params;
182        use std::collections::BTreeMap;
183
184        let path = Path::new("/abc/def/ghi".to_string());
185        assert!(path.matches("/abc/def/ghi".to_string()));
186        assert!(path.matches("//abc/def/ghi".to_string()));
187        assert!(!path.matches("/def/ghi".to_string()));
188        assert!(path.params().is_empty());
189
190        let path = Path::new("/abc/:def/:ghi/jkl".to_string());
191        assert!(!path.matches("/abc/def/ghi".to_string()));
192        assert!(path.matches("/abc/def/ghi/jkl".to_string()));
193        assert!(path.matches("/abc/ghi/def/jkl".to_string()));
194        assert!(path.matches("/abc/wooble/wakka/jkl".to_string()));
195        assert!(!path.matches("/nope/ghi/def/jkl".to_string()));
196        assert!(!path.matches("/abc/ghi/def/nope".to_string()));
197        assert_eq!(path.params().len(), 2);
198
199        let mut bt = BTreeMap::new();
200        bt.insert("def".to_string(), "wooble".to_string());
201        bt.insert("ghi".to_string(), "wakka".to_string());
202
203        assert_eq!(
204            path.extract("/abc/wooble/wakka/jkl".to_string()).unwrap(),
205            bt
206        );
207        assert!(path.extract("/wooble/wakka/jkl".to_string()).is_err());
208        assert!(path.extract("/def/wooble/wakka/jkl".to_string()).is_err());
209
210        assert_eq!(
211            Path::new("/abc/:wooble/:wakka/jkl".to_string()).to_string(),
212            "/abc/:wooble/:wakka/jkl".to_string()
213        );
214
215        assert_eq!(
216            Path::new("/".to_string()).extract("/".to_string()).unwrap(),
217            Params::default()
218        );
219
220        assert_eq!(
221            Path::new("/account/".to_string()),
222            Path::new("/account".to_string())
223        );
224
225        assert_eq!(Path::default().to_string(), "/".to_string());
226
227        let path = Path::new("/".to_string());
228        assert!(path.matches("/".to_string()));
229    }
230}