Skip to content

Commit

Permalink
rework matching of rotues with wildcards. With zip longest method
Browse files Browse the repository at this point in the history
  • Loading branch information
CJGutz committed Aug 31, 2024
1 parent b2d6c86 commit ca753e7
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 16 deletions.
1 change: 1 addition & 0 deletions unchained/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pub mod router;
pub mod server;
pub mod templates;
pub mod workers;
pub mod zip_longest;
79 changes: 63 additions & 16 deletions unchained/src/router.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::{collections::HashMap, fmt::Display, path::PathBuf};

use crate::zip_longest::ZipLongest;

#[derive(Clone, Debug)]
pub struct Request {
pub verb: String,
Expand Down Expand Up @@ -122,7 +124,6 @@ fn compare_route_w_path_and_get_path_params(
) -> (bool, HashMap<String, String>) {
let route_parts = route
.trim_start_matches('/')
.trim_end_matches('*')
.trim_end_matches('/')
.split('/')
.filter(|s| !s.is_empty())
Expand All @@ -135,24 +136,30 @@ fn compare_route_w_path_and_get_path_params(
let mut params = HashMap::new();
let mut match_route = true;
let last_is_star = route.ends_with('*');
for (count, req_part) in req_parts.clone().enumerate() {
let route_part = route_parts.get(count);
if route_part.is_none() {
match_route = last_is_star;
break;
}
let route_part = *route_part.unwrap();

if let Some(route_part) = route_part.strip_prefix(':') {
params.insert(route_part.to_string(), req_part.to_string());
} else if route_part != req_part {
match_route = false;
break;
for (route_part, req_part) in route_parts.iter().zip_longest(req_parts) {
match (route_part, req_part) {
(None, None) => break,
(None, Some(_)) => {
match_route = last_is_star;
break;
}
(Some(part), None) => {
match_route = *part == "*";
break;
}
(Some(route_part), Some(req_part)) => {
if let Some(route_no_prefix) = route_part.strip_prefix(':') {
params.insert(route_no_prefix.to_string(), req_part.to_string());
} else if *route_part != req_part {
match_route = *route_part == "*";
if !match_route {
break;
}
}
}
}
}
if route_parts.len() > req_parts.count() {
match_route = false;
}
(match_route, params)
}

Expand Down Expand Up @@ -248,4 +255,44 @@ mod tests {
assert!(matches);
}
}

#[test]
fn test_matching_wildcard_routes() {
let route_paths = vec![
("/*", ""),
("*", ""),
("*/", ""),
("/*/", "/*/"),
("path/*", "/path/more-path"),
("path/*/", "/path/more-path"),
("path/*/*/correct-path", "/path/more/other/correct-path"),
("path/*/fixed-path", "/path/more/fixed-path"),
("path/*/fixed-path/*/", "/path/more/fixed-path/anything/"),
];
for (route, path) in route_paths {
let (matches, _) = compare_route_w_path_and_get_path_params(route, path);
assert!(matches);
}
}

#[test]
fn test_non_matching_wildcard_routes() {
let route_paths = vec![
("path/*/fixed-path", "/wrong-root/more-path/fixed-path"),
("path/*/fixed-path", "/path/more-path/wrong-end"),
("path/*/", "wrong-path"),
(
"path/*/*/correct-path",
"/path/more-path/other-path/wrong-path",
),
("path/", "/*"),
("path/", "path/*"),
("path/more", "path/*"),
("/experience/", "/experience/*"),
];
for (route, path) in route_paths {
let (matches, _) = compare_route_w_path_and_get_path_params(route, path);
assert!(!matches);
}
}
}
46 changes: 46 additions & 0 deletions unchained/src/zip_longest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Tools to handle zipping two iterators
//! and continuing until both are exhausted.
//! Inspired by [itertools::zip_longest](https://doc.servo.org/itertools/zip_longest/index.html)
/// Struct where the next element is an option tuple
/// with two optional elements.
pub struct ZipLongestIter<T, U> {
a: T,
b: U,
}

impl<T, U> Iterator for ZipLongestIter<T, U>
where
T: Iterator,
U: Iterator,
{
type Item = (Option<T::Item>, Option<U::Item>);

/// Returns the next element in the iterator.
/// Returns Some as long as one element is available.
/// If both iterators are exhausted, returns None.
fn next(&mut self) -> Option<Self::Item> {
match (self.a.next(), self.b.next()) {
(None, None) => None,
(a, b) => Some((a, b)),
}
}
}

pub trait ZipLongest {
fn zip_longest<U>(self, other: U) -> ZipLongestIter<Self, U>
where
Self: Sized,
U: IntoIterator;
}
impl<T> ZipLongest for T
where
T: Iterator,
{
fn zip_longest<U>(self, other: U) -> ZipLongestIter<Self, U>
where
U: IntoIterator,
{
ZipLongestIter { a: self, b: other }
}
}

0 comments on commit ca753e7

Please sign in to comment.