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 parts.push(RoutePart::Param(arg.trim_start_matches(":").to_string()));
37 } else if arg == "" {
38 } else {
42 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 #[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 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}