use path_tree::PathTree;
use super::normalize::normalize_path;
#[derive(Default)]
pub struct LocationMatcher {
tree: PathTree<usize>,
}
impl LocationMatcher {
pub fn new(locations: impl IntoIterator<Item = Vec<String>>) -> Self {
let mut tree = PathTree::new();
for (index, patterns) in locations.into_iter().enumerate() {
for pattern in patterns {
let _ = tree.insert(&pattern, index);
}
}
Self { tree }
}
pub fn find(&self, path: &str) -> Option<usize> {
self.tree
.find(&normalize_path(path))
.map(|(index, _)| *index)
}
}
#[cfg(test)]
mod tests {
use super::LocationMatcher;
fn patterns(patterns: &[&str]) -> Vec<String> {
patterns.iter().map(ToString::to_string).collect()
}
fn matcher() -> LocationMatcher {
LocationMatcher::new([
patterns(&["/*"]),
patterns(&["/v1/session", "/v1/session/*"]),
patterns(&["/auth"]),
])
}
#[test]
fn test_most_specific_location_wins() {
let matcher = matcher();
assert_eq!(matcher.find("/"), Some(0));
assert_eq!(matcher.find("/v1/session"), Some(1));
assert_eq!(matcher.find("/v1/session/"), Some(1));
assert_eq!(matcher.find("/v1/session/x/y"), Some(1));
assert_eq!(matcher.find("/v1/sessions"), Some(0));
}
#[test]
fn test_exact_location_only_matches_its_path() {
let matcher = matcher();
assert_eq!(matcher.find("/auth"), Some(2));
assert_eq!(matcher.find("/auth/x"), Some(0));
}
#[test]
fn test_paths_are_normalized_before_matching() {
let matcher = matcher();
assert_eq!(matcher.find("//v1/session"), Some(1));
assert_eq!(matcher.find("/v1//session"), Some(1));
assert_eq!(matcher.find("/x/../v1/session"), Some(1));
assert_eq!(matcher.find("/v1/./session"), Some(1));
}
#[test]
fn test_no_matching_location() {
let matcher = LocationMatcher::new([patterns(&["/api/*"])]);
assert_eq!(matcher.find("/other"), None);
}
}