1mod 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#[derive(Clone)]
17pub struct PathRegex {
18 pub(crate) re: Regex,
19 pub(crate) keys: Vec<Key>,
20}
21
22impl PathRegex {
23 #[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 #[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 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#[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#[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}