url_matcher/
lib.rs

1use lazy_static::lazy_static;
2use regex::Regex;
3use serde::de::DeserializeOwned;
4use std::collections::HashMap;
5
6#[cfg(test)]
7mod tests;
8
9pub trait FromPattern<T>
10where
11    T: DeserializeOwned,
12{
13    fn from_pattern(pattern: &str, url: &str) -> Result<T, Error> {
14        let re = Regex::new(&make_expression(pattern))?;
15        Ok(recap::from_captures::<T>(&re, url)?)
16    }
17}
18
19pub fn matcher<'a>(pattern: &'a str, url: &'a str) -> Result<HashMap<String, &'a str>, Error> {
20    let mut map = HashMap::new();
21
22    let expr = make_expression(pattern);
23    let re = Regex::new(&expr)?;
24
25    let caps = re.captures(url).ok_or(Error::NoCaptures)?;
26
27    for (index, key) in re.capture_names().enumerate() {
28        if let (Some(k), Some(c)) = (key, caps.get(index)) {
29            map.insert(k.to_owned(), c.as_str());
30        }
31    }
32
33    Ok(map)
34}
35
36#[derive(thiserror::Error, Debug)]
37pub enum Error {
38    #[error("Failed to find any captures")]
39    NoCaptures,
40    #[error(transparent)]
41    RegexError(#[from] regex::Error),
42    #[error(transparent)]
43    RecapError(#[from] recap::Error),
44}
45
46pub fn make_expression(pattern: &str) -> String {
47    lazy_static! {
48        static ref CHARS: Regex = Regex::new(r":(?P<key>[a-zA-Z0-9_.+-]+)").unwrap();
49        static ref KEY: Regex = Regex::new(r"(?P<key>[?&.])").unwrap();
50    }
51
52    let escaped = KEY.replace_all(pattern, r"\$key");
53
54    let mut exp = String::from(r"^");
55
56    exp.push_str(&CHARS.replace_all(&escaped, r"(?P<$key>[a-zA-Z0-9_.+-]+)"));
57    exp.push('$');
58
59    exp
60}