From ca753e7e59b45a6397d8a296ac9e55fe3af954b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20G=C3=BCtzkow?= <70779496+CJGutz@users.noreply.github.com> Date: Sat, 31 Aug 2024 21:11:32 +0200 Subject: [PATCH] rework matching of rotues with wildcards. With zip longest method --- unchained/src/lib.rs | 1 + unchained/src/router.rs | 79 ++++++++++++++++++++++++++++-------- unchained/src/zip_longest.rs | 46 +++++++++++++++++++++ 3 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 unchained/src/zip_longest.rs diff --git a/unchained/src/lib.rs b/unchained/src/lib.rs index c8353f6..4144ea9 100644 --- a/unchained/src/lib.rs +++ b/unchained/src/lib.rs @@ -3,3 +3,4 @@ pub mod router; pub mod server; pub mod templates; pub mod workers; +pub mod zip_longest; diff --git a/unchained/src/router.rs b/unchained/src/router.rs index 0f7963f..4cdcc35 100644 --- a/unchained/src/router.rs +++ b/unchained/src/router.rs @@ -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, @@ -122,7 +124,6 @@ fn compare_route_w_path_and_get_path_params( ) -> (bool, HashMap) { let route_parts = route .trim_start_matches('/') - .trim_end_matches('*') .trim_end_matches('/') .split('/') .filter(|s| !s.is_empty()) @@ -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) } @@ -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); + } + } } diff --git a/unchained/src/zip_longest.rs b/unchained/src/zip_longest.rs new file mode 100644 index 0000000..67a9c3f --- /dev/null +++ b/unchained/src/zip_longest.rs @@ -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 { + a: T, + b: U, +} + +impl Iterator for ZipLongestIter +where + T: Iterator, + U: Iterator, +{ + type Item = (Option, Option); + + /// 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 { + match (self.a.next(), self.b.next()) { + (None, None) => None, + (a, b) => Some((a, b)), + } + } +} + +pub trait ZipLongest { + fn zip_longest(self, other: U) -> ZipLongestIter + where + Self: Sized, + U: IntoIterator; +} +impl ZipLongest for T +where + T: Iterator, +{ + fn zip_longest(self, other: U) -> ZipLongestIter + where + U: IntoIterator, + { + ZipLongestIter { a: self, b: other } + } +}