path2regex/re/
mod.rs

1//! Path regex
2mod builder;
3
4use anyhow::Result;
5
6use regex::{Regex, RegexBuilder};
7
8pub use builder::{PathRegexBuilder, PathRegexOptions};
9
10use crate::{
11    internal::{escape_string, END_WITH_DELIMITER},
12    Key, Parser, ParserOptions, Token, TryIntoWith,
13};
14
15/// Path regex
16#[derive(Clone)]
17pub struct PathRegex {
18    pub(crate) re: Regex,
19    pub(crate) keys: Vec<Key>,
20}
21
22impl PathRegex {
23    /// Create a [`PathRegex`](struct.PathRegex.html)
24    #[inline]
25    pub fn new<S>(source: S) -> Result<Self>
26    where
27        S: TryIntoWith<PathRegex, PathRegexOptions>,
28    {
29        PathRegexBuilder::new(source).build()
30    }
31
32    /// Create a [`PathRegex`](struct.PathRegex.html) with the options
33    #[inline]
34    pub fn new_with_options<S>(source: S, options: PathRegexOptions) -> Result<Self>
35    where
36        S: TryIntoWith<PathRegex, PathRegexOptions>,
37    {
38        PathRegexBuilder::new_with_options(source, options).build()
39    }
40
41    /// Get then parameter matches in the path
42    pub fn keys(&self) -> &Vec<Key> {
43        &self.keys
44    }
45}
46
47impl std::fmt::Display for PathRegex {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        std::fmt::Debug::fmt(&self, f)
50    }
51}
52
53impl std::fmt::Debug for PathRegex {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.write_str(self.re.as_str())
56    }
57}
58
59impl AsRef<Regex> for PathRegex {
60    #[inline]
61    fn as_ref(&self) -> &Regex {
62        &self.re
63    }
64}
65
66impl std::ops::Deref for PathRegex {
67    type Target = Regex;
68
69    #[inline]
70    fn deref(&self) -> &Self::Target {
71        &self.re
72    }
73}
74
75///
76#[inline]
77pub(crate) fn regex_to_path_regex(path: Regex, keys: &mut Vec<Key>) -> Result<Regex> {
78    if keys.is_empty() {
79        return Ok(path);
80    }
81
82    let groups_regex = RegexBuilder::new(r"\((?:\?<(.*?)>)?").build()?;
83
84    let mut index: usize = 0;
85    for name in groups_regex.captures_iter(path.as_str()) {
86        keys.push(Key {
87            name: name.get(1).map_or_else(
88                || {
89                    let p = index;
90                    index += 1;
91                    format!("{p}")
92                },
93                |m| m.as_str().to_owned(),
94            ),
95            prefix: Default::default(),
96            suffix: Default::default(),
97            pattern: Default::default(),
98            modifier: Default::default(),
99        });
100    }
101
102    Ok(path)
103}
104
105///
106#[inline]
107fn tokens_to_path_regex(
108    tokens: Vec<Token>,
109    keys: &mut Vec<Key>,
110    options: &PathRegexOptions,
111) -> Result<Regex, regex::Error> {
112    let PathRegexOptions {
113        sensitive,
114        strict,
115        end,
116        start,
117        delimiter,
118        ends_with,
119        encode,
120        ..
121    } = options;
122    let ends_with_re = (!ends_with.is_empty())
123        .then(|| format!("[{}]|$", escape_string(ends_with)))
124        .unwrap_or_else(|| "$".to_string());
125    let delimiter_re = (!delimiter.is_empty())
126        .then(|| format!("[{}]", escape_string(delimiter)))
127        .unwrap_or_default();
128    let route = if *start { "^" } else { "" };
129    let mut route = String::from(route);
130
131    for token in tokens.iter() {
132        match token {
133            Token::Static(token) => route += &escape_string(&encode(token)),
134            Token::Key(token) => {
135                let Key {
136                    prefix,
137                    suffix,
138                    pattern,
139                    modifier,
140                    ..
141                } = token;
142                let prefix = escape_string(&encode(prefix));
143                let suffix = escape_string(&encode(suffix));
144
145                if !pattern.is_empty() {
146                    keys.push(token.clone());
147
148                    if !prefix.is_empty() || !suffix.is_empty() {
149                        let modifier = modifier.as_str();
150                        if matches!(modifier, "+" | "*") {
151                            let mo = if modifier == "*" { "?" } else { "" };
152                            route += &format!(
153                                "(?:{prefix}((?:{pattern})(?:{suffix}{prefix}(?:{pattern}))*){suffix}){mo}"
154                            );
155                        } else {
156                            route += &format!("(?:{prefix}({pattern}){suffix}){modifier}");
157                        }
158                    } else {
159                        let modifier = token.modifier.as_str();
160                        if matches!(modifier, "+" | "*") {
161                            route += &format!("((?:{pattern}){modifier})");
162                        } else {
163                            route += &format!("({pattern}){modifier}");
164                        }
165                    }
166                } else {
167                    route += &format!("(?:{prefix}{suffix}){modifier}");
168                }
169            }
170        }
171    }
172
173    if *end {
174        if !strict {
175            route += &format!("{delimiter_re}?");
176        }
177        route += "$";
178        if ends_with.is_empty() {
179            route += "$";
180        } else {
181            route += &format!("(?P<{END_WITH_DELIMITER}>{ends_with_re})");
182        };
183    } else {
184        let end_token = tokens.last();
185        let is_end_delimited = match end_token {
186            Some(token) => match token {
187                Token::Static(end_token) if !end_token.is_empty() => {
188                    delimiter_re.contains(end_token.chars().last().unwrap())
189                }
190                _ => false,
191            },
192            None => true,
193        };
194
195        if !strict {
196            route += &format!("(?:{delimiter_re}{ends_with_re})?");
197        }
198
199        if !is_end_delimited {
200            route += &format!("(?P<{END_WITH_DELIMITER}>{delimiter_re}|{ends_with_re})");
201        }
202    }
203
204    RegexBuilder::new(&route)
205        .case_insensitive(!sensitive)
206        .build()
207}
208
209#[inline]
210pub(crate) fn string_to_path_regex<S>(path: S, options: &PathRegexOptions) -> Result<PathRegex>
211where
212    S: AsRef<str>,
213{
214    let mut keys = vec![];
215    let tokens = Parser::new_with_options(ParserOptions::from(options.clone())).parse_str(path)?;
216
217    let re = tokens_to_path_regex(tokens, &mut keys, options)?;
218    Ok(PathRegex { re, keys })
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::Parser;
225
226    #[test]
227    fn test_compile_tokens_to_regexp() -> anyhow::Result<()> {
228        let tokens = Parser::new().parse_str("/user/:id")?;
229        let re = tokens_to_path_regex(tokens, &mut vec![], &Default::default())?;
230        let matches = re
231            .captures("/user/123")
232            .unwrap()
233            .iter()
234            .map(|x| match x {
235                Some(x) => x.as_str(),
236                None => Default::default(),
237            })
238            .collect::<Vec<_>>();
239        assert_eq!(matches, vec!["/user/123", "123"]);
240        Ok(())
241    }
242}