1use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
12use std::collections::BTreeMap;
13use std::fmt;
14
15const COMPONENT: &AsciiSet = &NON_ALPHANUMERIC
17 .remove(b'-')
18 .remove(b'.')
19 .remove(b'_')
20 .remove(b'~');
21
22#[derive(Clone, Debug, Default, Eq, PartialEq)]
24pub struct Values(BTreeMap<String, String>);
25
26impl Values {
27 #[must_use]
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 #[must_use]
34 pub fn get(&self, name: &str) -> Option<&str> {
35 self.0.get(name).map(String::as_str)
36 }
37
38 pub fn insert(&mut self, name: impl Into<String>, value: impl Into<String>) {
39 self.0.insert(name.into(), value.into());
40 }
41
42 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
43 self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
44 }
45
46 #[must_use]
48 pub fn single(&self) -> Option<&str> {
49 let mut values = self.0.values();
50 match (values.next(), values.next()) {
51 (Some(value), None) => Some(value),
52 _ => None,
53 }
54 }
55}
56
57impl std::ops::Index<&str> for Values {
58 type Output = str;
59
60 fn index(&self, name: &str) -> &str {
64 self.get(name)
65 .unwrap_or_else(|| panic!("no template value {name:?}"))
66 }
67}
68
69impl<K: Into<String>, V: Into<String>> FromIterator<(K, V)> for Values {
70 fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
71 Self(
72 iter.into_iter()
73 .map(|(k, v)| (k.into(), v.into()))
74 .collect(),
75 )
76 }
77}
78
79#[derive(Clone, Debug, Eq, PartialEq)]
81pub struct TemplateError(pub String);
82
83impl fmt::Display for TemplateError {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.write_str(&self.0)
86 }
87}
88
89impl std::error::Error for TemplateError {}
90
91#[derive(Clone, Debug, Eq, PartialEq)]
92enum Segment {
93 Literal(String),
94 Expression {
96 prefix: String,
97 name: String,
98 },
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct Template {
104 source: String,
105 segments: Vec<Segment>,
106}
107
108impl Template {
109 pub fn parse(source: &str) -> Result<Self, TemplateError> {
117 let error = |why: &str| Err(TemplateError(format!("{source:?}: {why}")));
118 let Some(path) = source.strip_prefix('/') else {
119 return error("a template is a path starting with /");
120 };
121 if source.contains(['?', '#']) {
122 return error("a template has no query or fragment");
123 }
124 let mut segments = Vec::new();
125 let mut names = Vec::new();
126 if !path.is_empty() {
127 for segment in path.split('/') {
128 if segment.is_empty() {
129 return error("empty segment");
130 }
131 match segment.find('{') {
132 None if segment.contains('}') => return error("unmatched }"),
133 None => segments.push(Segment::Literal(segment.to_owned())),
134 Some(open) => {
135 let (prefix, expression) = segment.split_at(open);
136 let Some(name) = expression
137 .strip_prefix('{')
138 .and_then(|rest| rest.strip_suffix('}'))
139 else {
140 return error("an expression is the end of its segment");
141 };
142 if name.is_empty()
143 || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
144 {
145 return error("an expression is {name}, with a plain name");
146 }
147 if names.contains(&name) {
148 return error("an expression name appears twice");
149 }
150 names.push(name);
151 segments.push(Segment::Expression {
152 prefix: prefix.to_owned(),
153 name: name.to_owned(),
154 });
155 }
156 }
157 }
158 }
159 Ok(Self {
160 source: source.to_owned(),
161 segments,
162 })
163 }
164
165 #[must_use]
167 pub fn as_str(&self) -> &str {
168 &self.source
169 }
170
171 pub fn names(&self) -> impl Iterator<Item = &str> {
173 self.segments.iter().filter_map(|segment| match segment {
174 Segment::Expression { name, .. } => Some(name.as_str()),
175 Segment::Literal(_) => None,
176 })
177 }
178
179 pub fn expand(&self, values: &Values) -> Result<String, TemplateError> {
185 let mut path = String::new();
186 for segment in &self.segments {
187 path.push('/');
188 match segment {
189 Segment::Literal(literal) => path.push_str(literal),
190 Segment::Expression { prefix, name } => {
191 let value = values.get(name).filter(|value| !value.is_empty());
192 let Some(value) = value else {
193 return Err(TemplateError(format!(
194 "{:?}: no value for {{{name}}}",
195 self.source
196 )));
197 };
198 path.push_str(prefix);
199 path.extend(utf8_percent_encode(value, COMPONENT));
200 }
201 }
202 }
203 if path.is_empty() {
204 path.push('/');
205 }
206 Ok(path)
207 }
208
209 #[must_use]
211 pub fn matches(&self, path: &str) -> Option<Values> {
212 let path = path.strip_prefix('/')?;
213 let parts: Vec<&str> = if path.is_empty() {
214 Vec::new()
215 } else {
216 path.split('/').collect()
217 };
218 if parts.len() != self.segments.len() {
219 return None;
220 }
221 let mut values = Values::new();
222 for (segment, part) in self.segments.iter().zip(parts) {
223 match segment {
224 Segment::Literal(literal) => {
225 if literal != part {
226 return None;
227 }
228 }
229 Segment::Expression { prefix, name } => {
230 let encoded = part.strip_prefix(prefix.as_str())?;
231 if encoded.is_empty() {
232 return None;
233 }
234 let value = percent_decode_str(encoded).decode_utf8().ok()?;
235 values.insert(name.clone(), value.into_owned());
236 }
237 }
238 }
239 Some(values)
240 }
241
242 #[must_use]
248 pub fn specificity(&self, other: &Self) -> std::cmp::Ordering {
249 fn rank(segment: &Segment) -> (u8, usize) {
250 match segment {
251 Segment::Literal(literal) => (1, literal.len()),
252 Segment::Expression { prefix, .. } => (0, prefix.len()),
253 }
254 }
255 self.segments
256 .iter()
257 .zip(&other.segments)
258 .map(|(a, b)| rank(a).cmp(&rank(b)))
259 .find(|order| order.is_ne())
260 .unwrap_or(std::cmp::Ordering::Equal)
261 }
262
263 #[must_use]
265 pub fn overlaps(&self, other: &Self) -> bool {
266 self.segments.len() == other.segments.len()
267 && self
268 .segments
269 .iter()
270 .zip(&other.segments)
271 .all(|pair| match pair {
272 (Segment::Literal(a), Segment::Literal(b)) => a == b,
273 (Segment::Literal(literal), Segment::Expression { prefix, .. })
274 | (Segment::Expression { prefix, .. }, Segment::Literal(literal)) => {
275 literal.len() > prefix.len() && literal.starts_with(prefix.as_str())
276 }
277 (
278 Segment::Expression { prefix: a, .. },
279 Segment::Expression { prefix: b, .. },
280 ) => a.starts_with(b.as_str()) || b.starts_with(a.as_str()),
281 })
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 fn template(source: &str) -> Template {
290 Template::parse(source).unwrap()
291 }
292
293 fn values(pairs: &[(&str, &str)]) -> Values {
294 pairs.iter().copied().collect()
295 }
296
297 #[test]
298 fn a_value_survives_the_round_trip() {
299 let t = template("/ap/users/{user_id}");
300 for id in [
301 "0190c2f0-6f3b-7c3e-9d61-3c1b0f6e1a2b",
302 "alice",
303 "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2",
304 "그림 쟁이",
305 "a/b?c#d%e",
306 ] {
307 let path = t.expand(&values(&[("user_id", id)])).unwrap();
308 assert!(!path[10..].contains(['/', '?', '#', ':', ' ']), "{path}");
309 assert_eq!(t.matches(&path).unwrap().get("user_id"), Some(id), "{path}");
310 }
311 }
312
313 #[test]
314 fn a_prefix_is_part_of_its_segment() {
315 let t = template("/@{handle}");
316 assert_eq!(t.expand(&values(&[("handle", "bob")])).unwrap(), "/@bob");
317 assert_eq!(t.matches("/@bob").unwrap().get("handle"), Some("bob"));
318 assert_eq!(t.matches("/bob"), None);
319 assert_eq!(t.matches("/@"), None, "an expression is never empty");
320 }
321
322 #[test]
323 fn several_expressions_are_read_by_name() {
324 let t = template("/users/{username}/statuses/{id}/activity");
325 let v = t.matches("/users/alice/statuses/42/activity").unwrap();
326 assert_eq!((&v["username"], &v["id"]), ("alice", "42"));
327 assert_eq!(t.names().collect::<Vec<_>>(), ["username", "id"]);
328 assert_eq!(t.matches("/users/alice/statuses/42"), None);
329 assert!(t.expand(&values(&[("username", "alice")])).is_err());
330 }
331
332 #[test]
333 fn a_literal_is_more_specific_than_an_expression() {
334 use std::cmp::Ordering;
335 let followers = template("/@{username}/followers");
336 let status = template("/@{username}/{status_id}");
337 assert!(followers.overlaps(&status));
338 assert_eq!(followers.specificity(&status), Ordering::Greater);
339 assert_eq!(status.specificity(&followers), Ordering::Less);
340 assert_eq!(
341 template("/@{name}").specificity(&template("/{name}")),
342 Ordering::Greater,
343 "a longer prefix is more specific"
344 );
345 assert_eq!(
346 template("/users/{a}").specificity(&template("/users/{b}")),
347 Ordering::Equal
348 );
349 }
350
351 #[test]
352 fn a_template_without_expressions_is_a_fixed_path() {
353 let t = template("/actor");
354 assert_eq!(t.expand(&Values::new()).unwrap(), "/actor");
355 assert_eq!(t.matches("/actor"), Some(Values::new()));
356 assert_eq!(template("/").expand(&Values::new()).unwrap(), "/");
357 assert_eq!(template("/").matches("/"), Some(Values::new()));
358 }
359
360 #[test]
361 fn malformed_templates_are_refused() {
362 for source in [
363 "users/{id}",
364 "/users//{id}",
365 "/users/{id}/",
366 "/users/{id}x",
367 "/users/{}",
368 "/users/{a-b}",
369 "/users/{id}/{id}",
370 "/users/id}",
371 "/users/{id}?page",
372 ] {
373 assert!(Template::parse(source).is_err(), "{source}");
374 }
375 }
376
377 #[test]
378 fn templates_that_could_match_one_path_overlap() {
379 let overlapping = [
380 ("/users/{a}", "/users/{b}"),
381 ("/users/{a}", "/users/alice"),
382 ("/@{a}", "/{b}"),
383 ("/@{a}", "/@alice"),
384 ];
385 for (a, b) in overlapping {
386 assert!(template(a).overlaps(&template(b)), "{a} {b}");
387 assert!(template(b).overlaps(&template(a)), "{b} {a}");
388 }
389 let apart = [
390 ("/users/{a}", "/users/{a}/followers"),
391 ("/users/{a}", "/groups/{a}"),
392 ("/@{a}", "/!{a}"),
393 ("/@{a}", "/@"),
394 ("/actor", "/inbox"),
395 ];
396 for (a, b) in apart {
397 assert!(!template(a).overlaps(&template(b)), "{a} {b}");
398 }
399 }
400}