path_to_regexp/
lib.rs

1extern crate regex;
2extern crate fancy_regex;
3
4use regex::Regex;
5use fancy_regex::Regex as FancyRegex;
6
7/**
8 * Default configs.
9 */
10const DEFAULT_DELIMITER: char = '/';
11
12pub struct Options {
13    delimiter: char,
14    whitelist: Vec<String>,
15    strict: bool,
16    sensitive: bool,
17    end: bool,
18    start: bool,
19    ends_with: Vec<String>
20}
21impl Default for Options {
22    fn default () -> Options {
23        Options {
24            delimiter: DEFAULT_DELIMITER,
25            whitelist: Vec::new(),
26            strict: false,
27            sensitive: false,
28            end: true,
29            start: true,
30            ends_with: Vec::new()
31        }
32    }
33}
34
35#[derive(Debug, Clone)]
36pub struct Token {
37    name: String,
38    prefix: String,
39    delimiter: char,
40    optional: bool,
41    repeat: bool,
42    pattern: String
43}
44
45#[derive(Debug)]
46pub struct Match {
47    name: String,
48    value: String
49}
50
51#[derive(Debug, Clone)]
52pub struct Container {
53    token: Option<Token>,
54    path: String
55}
56
57/**
58 * Escape a regular expression string.
59 *
60 * @param  {String} string
61 * @return {String}
62 */
63fn escape_string (string: String) -> String {
64    let re = Regex::new(r"([.+*?=^!:${}()[\]|/\\]])").unwrap();
65    re.replace_all(string.as_str(), r"\$1").into_owned()
66}
67
68/**
69 * Escape the capturing group by escaping special characters and meaning.
70 *
71 * @param  {String} group
72 * @return {String}
73 */
74fn escape_group (group: String) -> String {
75    let re = Regex::new(r"([=!:$/()])").unwrap();
76    re.replace_all(group.as_str(), r"\$1").into_owned()
77}
78
79/**
80 * Get the flags for a regexp from the options.
81 *
82 * @param  {Options} options
83 * @return {String}
84 */
85fn flags (route: &str, options: Options) -> String {
86    if !options.sensitive {
87        format!("(?i){}", route)
88    } else {
89        String::from(route)
90    }
91}
92
93/**
94 * Parse a string for the raw tokens and paths.
95 *
96 * @param  {&str} text
97 * @param  {Options} options
98 * @return (Vec<Container>)
99 */
100pub fn parse (text: &str, options: Options) -> Vec<Container> {
101    let default_delimiter: char = options.delimiter;
102    let whitelist: &Vec<String> = &options.whitelist;
103    let path_regexp: Regex = Regex::new(vec![
104        // Match escaped characters that would otherwise appear in future matches.
105        // This allows the user to escape special characters that won't transform.
106        r"(\\.)",
107        // Match Express-style parameters and un-named parameters with a prefix
108        // and optional suffixes. Matches appear as:
109        //
110        // ":test(\\d+)?" => ["test", "\d+", NONE, "?"]
111        // "(\\d+)"  => [NONE, NONE, "\d+", NONE]
112        r"(?::(\w+)(?:\(((?:\\.|[^\\()])+)\))?|\(((?:\\.|[^\\()])+)\))([+*?])?"
113    ].join("|").as_str()).unwrap();
114    let mut index = 0;
115    let mut key = -1;
116    let mut path = String::new();
117    let mut path_escaped = false;
118    let mut containers: Vec<Container> = vec![];
119
120    fn unwrap_match_to_str (m: Option<regex::Match>) -> &str {
121        if m != None {
122            m.unwrap().as_str()
123        } else {
124            ""
125        }
126    }
127
128    if !path_regexp.is_match(text) {
129        return containers;
130    }
131
132    for res in path_regexp.captures_iter(text) {
133        let m = res.get(0).unwrap();
134        let escaped = res.get(1);
135        let offset = m.start();
136
137        path.push_str(text.get(index..offset).unwrap());
138        index = offset + m.as_str().len();
139
140        // Ignore already escaped sequences.
141        if escaped != None {
142            path.push_str(escaped.unwrap().as_str());
143            path_escaped = true;
144            continue;
145        }
146
147        let mut prev: String = String::new();
148        let name = unwrap_match_to_str(res.get(2));
149        let capture = unwrap_match_to_str(res.get(3));
150        let group = res.get(4);
151        let modifier = unwrap_match_to_str(res.get(5));
152
153        if !path_escaped && path.len() > 0 {
154            let k = path.len();
155            let c = String::from(path.get(k-1..k).unwrap());
156            let matches: bool = if whitelist.len() > 0 {
157                whitelist.into_iter().find(|&x| x == &c) != None
158            } else {
159                false
160            };
161
162            if matches {
163                prev = c;
164                path = String::from(path.get(0..k).unwrap());
165            }
166        }
167
168        // Push the current path onto the tokens.
169        if path != "" {
170            containers.push(Container {
171                path,
172                token: None
173            });
174            path = String::new();
175            path_escaped = false;
176        }
177
178        let repeat = modifier == "+" || modifier == "*";
179        let optional = modifier == "?" || modifier == "*";
180        let pattern = if capture.len() > 0 {
181            capture
182        } else if group != None {
183            group.unwrap().as_str()
184        } else {
185            ""
186        };
187        let delimiter: char = if prev != "" {
188            prev.chars().next().unwrap()
189        } else {
190            default_delimiter
191        };
192
193        containers.push(Container {
194            path: String::new(),
195            token: Some(Token {
196                name: if name != "" {
197                    name.to_owned()
198                } else {
199                    key += 1;
200                    key.to_string()
201                },
202                prefix: prev,
203                delimiter: delimiter,
204                optional: optional,
205                repeat: repeat,
206                pattern: if pattern != "" {
207                    escape_group(pattern.to_owned())
208                } else {
209                    let pattern_delimiter = if delimiter == default_delimiter {
210                        delimiter.to_string()
211                    } else {
212                        vec![delimiter, default_delimiter].into_iter().collect()
213                    };
214                    
215                    format!(r"[^\{}]+?", escape_string(pattern_delimiter))
216                }
217            })
218        });
219    }
220
221    // Push any remaining characters.
222    if path.len() > 0 || index < text.len() {
223        path.push_str(text.get(index..text.len()).unwrap());
224        containers.push(Container {
225            path,
226            token: None
227        });
228    }
229
230    containers
231}
232
233/**
234 * Expose a function for taking containers and returning a FancyRegex.
235 *
236 * @param  {Vec<Container>} containers
237 * @param  {Options} options
238 * @return {FancyRegex}
239 */
240pub fn to_regexp (containers: &Vec<Container>, options: Options) -> FancyRegex {
241    let strict = options.strict;
242    let start = options.start;
243    let end = options.end;
244    let delimiter = options.delimiter;
245    let ends_with = if options.ends_with.len() > 0 {
246        let mut _ends_with_vec = &options.ends_with;
247        let mut _ends_with: Vec<String> = _ends_with_vec.into_iter().map(|s| {
248            escape_string(s.to_string())
249        }).collect();
250        _ends_with.push(String::from("$"));
251        _ends_with.join("|")
252    } else {
253        String::from("$")
254    };
255    let mut route = if start == true {
256        String::from("^")
257    } else {
258        String::from("")
259    };
260
261    // Iterate over the containers and create our regexp string.
262    for i in 0..containers.len() {
263        let container = &containers[i];
264        
265        if !container.path.is_empty() {
266            route.push_str(escape_string(container.path.to_string()).as_str());
267        } else {
268            let token = container.token.as_ref().unwrap();
269            let prefix = String::from(token.prefix.as_str());
270            let capture = if token.repeat == true {
271                format!("(?:{})(?:{}(?:{}))*", token.pattern.as_str(), escape_string(token.delimiter.to_string()).as_str(), token.pattern.as_str())
272            } else {
273                format!("{}", token.pattern.as_str())
274            };
275
276            if token.optional {
277                if token.prefix != "" {
278                    route.push_str(format!("({})", capture.as_str()).as_str());
279                } else {
280                    route.push_str(format!("(?:{}({}))?", escape_string(prefix).as_str(), capture.as_str()).as_str());
281                }
282            } else {
283                route.push_str(format!("{}({})", escape_string(prefix).as_str(), capture.as_str()).as_str());
284            }
285        }
286    }
287
288    if end {
289        if !strict {
290            route.push_str(format!("(?:{})?", escape_string(delimiter.to_string())).as_str());
291        }
292
293        if ends_with == "$" {
294            route.push_str("$");
295        } else {
296            route.push_str(format!("(?={})", ends_with).as_str());
297        };
298    } else {
299        let last_container: &Container = containers.last().unwrap();
300        let is_end_delimited = if !last_container.path.is_empty() {
301            let last_path_char = last_container.path.get(last_container.path.len() - 1..last_container.path.len()).unwrap();
302            last_path_char.to_string()
303        } else {
304            String::new()
305        };
306
307        if !strict {
308            route.push_str(format!("(?:{}(?={}))?", escape_string(delimiter.to_string()), ends_with).as_str());
309        }
310
311        if is_end_delimited.is_empty() {
312            route.push_str(format!("(?={}|{})", escape_string(delimiter.to_string()), ends_with).as_str());
313        }
314    }
315
316    let regex_str = format!(r"{}", flags(route.as_str(), options).as_str());
317
318    FancyRegex::new(regex_str.as_str()).unwrap()
319}
320
321/**
322 * Function for matching text with parsed tokens.
323 *
324 * @param  {&str} text
325 * @param  {FancyRegex} regexp
326 * @param  {Vec<Container>} containers
327 * @return {Vec<Match>}
328 */
329pub fn match_str (text: &str, regexp: FancyRegex, containers: Vec<Container>) -> Vec<Match> {
330    let mut matches: Vec<Match> = vec![];
331
332    if !regexp.is_match(text).unwrap() {
333        return matches;
334    }
335    
336    let containers: Vec<Container> = containers.into_iter()
337        .filter(|container| container.path == "")
338        .collect();
339
340    if let Some(caps) = regexp.captures_from_pos(&text, 0).unwrap() {
341        for i in 0..caps.len() {
342            let cap = caps.at(i).unwrap();
343
344            if cap.len() == text.len() {
345                continue;
346            }
347
348            let container = containers.get(i-1).unwrap();
349            if let Some(token) = &container.token {
350                matches.push(Match {
351                    name: String::from(token.name.as_str()),
352                    value: cap.to_owned()
353                });
354            }
355        }
356    }
357
358    matches
359}