use compact_str::CompactString;
use smallvec::SmallVec;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
pub const INLINE_PARAM_COUNT: usize = 8;
pub const INLINE_SEGMENT_COUNT: usize = 16;
#[derive(Debug, Clone, Copy)]
pub struct BorrowedParam<'a> {
pub name: &'a str,
pub value: &'a str,
}
impl<'a> BorrowedParam<'a> {
#[inline]
pub fn new(name: &'a str, value: &'a str) -> Self {
Self { name, value }
}
#[inline]
pub fn parse<T: std::str::FromStr>(&self) -> Result<T, T::Err> {
self.value.parse()
}
#[inline]
pub fn to_owned(&self) -> (String, String) {
(self.name.to_string(), self.value.to_string())
}
#[inline]
pub fn to_compact(&self) -> (CompactString, CompactString) {
(
CompactString::new(self.name),
CompactString::new(self.value),
)
}
}
#[derive(Debug, Clone)]
pub struct Params<'a> {
params: SmallVec<[BorrowedParam<'a>; INLINE_PARAM_COUNT]>,
wildcard: Option<Cow<'a, str>>,
catch_all_name: Option<&'a str>,
}
impl<'a> Params<'a> {
#[inline]
pub fn new() -> Self {
Self {
params: SmallVec::new(),
wildcard: None,
catch_all_name: None,
}
}
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
params: SmallVec::with_capacity(capacity),
wildcard: None,
catch_all_name: None,
}
}
#[inline]
pub fn push(&mut self, name: &'a str, value: &'a str) {
self.params.push(BorrowedParam::new(name, value));
PARAMS_STATS.record_param_add(self.is_inline());
}
#[inline]
pub fn set_wildcard(&mut self, value: impl Into<Cow<'a, str>>) {
self.wildcard = Some(value.into());
}
#[inline]
pub fn set_catch_all(&mut self, name: &'a str, value: impl Into<Cow<'a, str>>) {
self.wildcard = Some(value.into());
self.catch_all_name = Some(name);
}
#[inline]
pub fn get(&self, name: &str) -> Option<&str> {
if let Some(p) = self.params.iter().find(|p| p.name == name) {
return Some(p.value);
}
if self.catch_all_name == Some(name) {
return self.wildcard.as_deref();
}
None
}
#[inline]
pub fn get_parsed<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
self.get(name).map(|v| v.parse())
}
#[inline]
pub fn wildcard(&self) -> Option<&str> {
self.wildcard.as_deref()
}
#[inline]
pub fn len(&self) -> usize {
self.params.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.params.is_empty()
}
#[inline]
pub fn is_inline(&self) -> bool {
!self.params.spilled()
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &BorrowedParam<'a>> {
self.params.iter()
}
pub fn to_hash_map(&self) -> HashMap<String, String> {
let mut map = HashMap::with_capacity(self.params.len());
for param in &self.params {
map.insert(param.name.to_string(), param.value.to_string());
}
if let Some(ref wildcard) = self.wildcard {
if let Some(name) = self.catch_all_name {
map.insert(name.to_string(), wildcard.to_string());
}
map.insert("*".to_string(), wildcard.to_string());
}
map
}
pub fn to_compact_map(&self) -> SmallVec<[(CompactString, CompactString); INLINE_PARAM_COUNT]> {
let mut map = SmallVec::with_capacity(self.params.len());
for param in &self.params {
map.push(param.to_compact());
}
map
}
}
impl<'a> Default for Params<'a> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)] pub enum MatchResult<'a> {
Match(Params<'a>),
NoMatch,
}
impl<'a> MatchResult<'a> {
#[inline]
pub fn is_match(&self) -> bool {
matches!(self, MatchResult::Match(_))
}
#[inline]
pub fn params(self) -> Option<Params<'a>> {
match self {
MatchResult::Match(p) => Some(p),
MatchResult::NoMatch => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SegmentType {
Static,
Param,
Wildcard,
CatchAll,
}
#[derive(Debug, Clone)]
pub struct PatternSegment<'a> {
pub segment_type: SegmentType,
pub value: &'a str,
}
impl<'a> PatternSegment<'a> {
#[inline]
pub fn parse(segment: &'a str) -> Self {
if let Some(param_name) = segment.strip_prefix(':') {
Self {
segment_type: SegmentType::Param,
value: param_name,
}
} else if segment == "*" {
Self {
segment_type: SegmentType::Wildcard,
value: "*",
}
} else if segment == "**" || segment.starts_with('*') && segment.len() > 1 {
Self {
segment_type: SegmentType::CatchAll,
value: if segment == "**" { "*" } else { &segment[1..] },
}
} else {
Self {
segment_type: SegmentType::Static,
value: segment,
}
}
}
#[inline]
pub fn is_static(&self) -> bool {
self.segment_type == SegmentType::Static
}
#[inline]
pub fn is_param(&self) -> bool {
self.segment_type == SegmentType::Param
}
#[inline]
pub fn is_wildcard(&self) -> bool {
matches!(
self.segment_type,
SegmentType::Wildcard | SegmentType::CatchAll
)
}
}
#[derive(Debug, Clone)]
pub struct OwnedPatternSegment {
pub segment_type: SegmentType,
pub value: CompactString,
}
#[derive(Debug, Clone)]
pub struct CompiledPattern {
pub pattern: String,
pub segments: SmallVec<[OwnedPatternSegment; INLINE_SEGMENT_COUNT]>,
pub catch_all_index: Option<usize>,
pub required_segments: usize,
pub is_static: bool,
}
impl CompiledPattern {
pub fn new(pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
let mut segments = SmallVec::new();
let mut catch_all_index = None;
let mut is_static = true;
for (i, part) in pattern.split('/').filter(|s| !s.is_empty()).enumerate() {
let segment = PatternSegment::parse(part);
if segment.segment_type == SegmentType::CatchAll {
catch_all_index = Some(i);
}
if !segment.is_static() {
is_static = false;
}
segments.push(OwnedPatternSegment {
segment_type: segment.segment_type,
value: CompactString::new(segment.value),
});
}
let required_segments = catch_all_index.unwrap_or(segments.len());
Self {
pattern,
segments,
catch_all_index,
required_segments,
is_static,
}
}
pub fn match_path<'a>(&'a self, path: &'a str) -> MatchResult<'a> {
PARAMS_STATS.record_match_attempt();
let path_segments: SmallVec<[&str; INLINE_SEGMENT_COUNT]> =
path.split('/').filter(|s| !s.is_empty()).collect();
if self.catch_all_index.is_none() {
if path_segments.len() != self.segments.len() {
return MatchResult::NoMatch;
}
} else if path_segments.len() < self.required_segments {
return MatchResult::NoMatch;
}
let mut params = Params::with_capacity(self.segments.len());
for (i, pattern_seg) in self.segments.iter().enumerate() {
match pattern_seg.segment_type {
SegmentType::Static => {
if i >= path_segments.len() || path_segments[i] != pattern_seg.value.as_str() {
return MatchResult::NoMatch;
}
}
SegmentType::Param => {
if i >= path_segments.len() {
return MatchResult::NoMatch;
}
params.push(pattern_seg.value.as_str(), path_segments[i]);
}
SegmentType::Wildcard => {
if i >= path_segments.len() {
return MatchResult::NoMatch;
}
params.push(pattern_seg.value.as_str(), path_segments[i]);
}
SegmentType::CatchAll => {
let remaining = &path_segments[i..];
if remaining.is_empty() {
params.set_wildcard("");
} else {
let wildcard_value = remaining.join("/");
if pattern_seg.value.as_str() != "*" {
params.set_catch_all(pattern_seg.value.as_str(), wildcard_value);
} else {
params.set_wildcard(wildcard_value);
}
}
break;
}
}
}
PARAMS_STATS.record_match_success();
MatchResult::Match(params)
}
#[inline]
pub fn matches(&self, path: &str) -> bool {
self.match_path(path).is_match()
}
}
pub fn match_path_zero_alloc<'p, 'a>(pattern: &'p str, path: &'a str) -> Option<Params<'a>>
where
'p: 'a,
{
PARAMS_STATS.record_match_attempt();
let pattern_parts: SmallVec<[&str; INLINE_SEGMENT_COUNT]> =
pattern.split('/').filter(|s| !s.is_empty()).collect();
let path_parts: SmallVec<[&str; INLINE_SEGMENT_COUNT]> =
path.split('/').filter(|s| !s.is_empty()).collect();
let has_catch_all = pattern_parts
.last()
.is_some_and(|s| s.starts_with('*') && *s != "*");
if !has_catch_all && pattern_parts.len() != path_parts.len() {
return None;
}
if has_catch_all && path_parts.len() < pattern_parts.len() - 1 {
return None;
}
let mut params = Params::with_capacity(pattern_parts.len());
for (i, pattern_part) in pattern_parts.iter().enumerate() {
if let Some(name) = pattern_part.strip_prefix(':') {
if i >= path_parts.len() {
return None;
}
params.push(name, path_parts[i]);
} else if *pattern_part == "*" {
if i >= path_parts.len() {
return None;
}
params.push("*", path_parts[i]);
} else if let Some(name) = pattern_part.strip_prefix('*') {
let remaining: String = path_parts[i..].join("/");
params.set_catch_all(name, remaining);
break;
} else if i >= path_parts.len() || *pattern_part != path_parts[i] {
return None;
}
}
PARAMS_STATS.record_match_success();
Some(params)
}
pub trait ExtractParams<'a> {
fn extract(params: &Params<'a>) -> Option<Self>
where
Self: Sized;
}
impl<'a> ExtractParams<'a> for () {
#[inline]
fn extract(_params: &Params<'a>) -> Option<Self> {
Some(())
}
}
impl<'a> ExtractParams<'a> for &'a str {
#[inline]
fn extract(params: &Params<'a>) -> Option<Self> {
params.params.first().map(|p| p.value)
}
}
impl<'a, T: std::str::FromStr> ExtractParams<'a> for (T,) {
fn extract(params: &Params<'a>) -> Option<Self> {
let v = params.params.first()?.value.parse().ok()?;
Some((v,))
}
}
impl<'a, T1: std::str::FromStr, T2: std::str::FromStr> ExtractParams<'a> for (T1, T2) {
fn extract(params: &Params<'a>) -> Option<Self> {
if params.len() < 2 {
return None;
}
let v1 = params.params[0].value.parse().ok()?;
let v2 = params.params[1].value.parse().ok()?;
Some((v1, v2))
}
}
impl<'a, T1: std::str::FromStr, T2: std::str::FromStr, T3: std::str::FromStr> ExtractParams<'a>
for (T1, T2, T3)
{
fn extract(params: &Params<'a>) -> Option<Self> {
if params.len() < 3 {
return None;
}
let v1 = params.params[0].value.parse().ok()?;
let v2 = params.params[1].value.parse().ok()?;
let v3 = params.params[2].value.parse().ok()?;
Some((v1, v2, v3))
}
}
#[derive(Debug, Default)]
pub struct ParamsStats {
match_attempts: AtomicU64,
match_successes: AtomicU64,
params_inline: AtomicU64,
params_heap: AtomicU64,
}
impl ParamsStats {
fn record_match_attempt(&self) {
self.match_attempts.fetch_add(1, Ordering::Relaxed);
}
fn record_match_success(&self) {
self.match_successes.fetch_add(1, Ordering::Relaxed);
}
fn record_param_add(&self, inline: bool) {
if inline {
self.params_inline.fetch_add(1, Ordering::Relaxed);
} else {
self.params_heap.fetch_add(1, Ordering::Relaxed);
}
}
pub fn match_attempts(&self) -> u64 {
self.match_attempts.load(Ordering::Relaxed)
}
pub fn match_successes(&self) -> u64 {
self.match_successes.load(Ordering::Relaxed)
}
pub fn match_ratio(&self) -> f64 {
let attempts = self.match_attempts() as f64;
let successes = self.match_successes() as f64;
if attempts > 0.0 {
successes / attempts
} else {
0.0
}
}
pub fn inline_ratio(&self) -> f64 {
let inline = self.params_inline.load(Ordering::Relaxed) as f64;
let heap = self.params_heap.load(Ordering::Relaxed) as f64;
let total = inline + heap;
if total > 0.0 { inline / total } else { 1.0 }
}
}
static PARAMS_STATS: ParamsStats = ParamsStats {
match_attempts: AtomicU64::new(0),
match_successes: AtomicU64::new(0),
params_inline: AtomicU64::new(0),
params_heap: AtomicU64::new(0),
};
pub fn params_stats() -> &'static ParamsStats {
&PARAMS_STATS
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_borrowed_param() {
let param = BorrowedParam::new("id", "123");
assert_eq!(param.name, "id");
assert_eq!(param.value, "123");
assert_eq!(param.parse::<i32>().unwrap(), 123);
}
#[test]
fn test_params_inline() {
let mut params = Params::new();
params.push("id", "123");
params.push("name", "test");
assert!(params.is_inline());
assert_eq!(params.len(), 2);
assert_eq!(params.get("id"), Some("123"));
assert_eq!(params.get("name"), Some("test"));
}
#[test]
fn test_params_wildcard() {
let mut params = Params::new();
params.push("id", "123");
params.set_wildcard("path/to/file");
assert_eq!(params.wildcard(), Some("path/to/file"));
}
#[test]
fn test_pattern_segment_static() {
let seg = PatternSegment::parse("users");
assert_eq!(seg.segment_type, SegmentType::Static);
assert_eq!(seg.value, "users");
}
#[test]
fn test_pattern_segment_param() {
let seg = PatternSegment::parse(":id");
assert_eq!(seg.segment_type, SegmentType::Param);
assert_eq!(seg.value, "id");
}
#[test]
fn test_pattern_segment_wildcard() {
let seg = PatternSegment::parse("*");
assert_eq!(seg.segment_type, SegmentType::Wildcard);
}
#[test]
fn test_pattern_segment_catch_all() {
let seg = PatternSegment::parse("*path");
assert_eq!(seg.segment_type, SegmentType::CatchAll);
assert_eq!(seg.value, "path");
let seg = PatternSegment::parse("**");
assert_eq!(seg.segment_type, SegmentType::CatchAll);
}
#[test]
fn test_compiled_pattern_static() {
let pattern = CompiledPattern::new("/api/users");
assert!(pattern.is_static);
assert_eq!(pattern.segments.len(), 2);
}
#[test]
fn test_compiled_pattern_with_param() {
let pattern = CompiledPattern::new("/users/:id");
assert!(!pattern.is_static);
assert_eq!(pattern.segments.len(), 2);
}
#[test]
fn test_compiled_pattern_match() {
let pattern = CompiledPattern::new("/users/:id");
let result = pattern.match_path("/users/123");
assert!(result.is_match());
let params = result.params().unwrap();
assert_eq!(params.get("id"), Some("123"));
}
#[test]
fn test_compiled_pattern_catch_all() {
let pattern = CompiledPattern::new("/files/*path");
let result = pattern.match_path("/files/docs/readme.md");
assert!(result.is_match());
let params = result.params().unwrap();
assert_eq!(params.get("path"), Some("docs/readme.md"));
}
#[test]
fn test_zero_alloc_match() {
let params = match_path_zero_alloc("/users/:id", "/users/123").unwrap();
assert_eq!(params.get("id"), Some("123"));
}
#[test]
fn test_zero_alloc_match_multiple() {
let params =
match_path_zero_alloc("/users/:user_id/posts/:post_id", "/users/123/posts/456")
.unwrap();
assert_eq!(params.get("user_id"), Some("123"));
assert_eq!(params.get("post_id"), Some("456"));
}
#[test]
fn test_zero_alloc_no_match() {
let result = match_path_zero_alloc("/users/:id", "/posts/123");
assert!(result.is_none());
}
#[test]
fn test_zero_alloc_catch_all() {
let params = match_path_zero_alloc("/files/*path", "/files/docs/readme.md").unwrap();
assert_eq!(params.get("path"), Some("docs/readme.md"));
assert_eq!(params.wildcard(), Some("docs/readme.md"));
}
#[test]
fn test_extract_params_tuple() {
let mut params = Params::new();
params.push("id", "123");
params.push("name", "test");
let (id,): (i32,) = ExtractParams::extract(¶ms).unwrap();
assert_eq!(id, 123);
}
#[test]
fn test_extract_params_two() {
let mut params = Params::new();
params.push("user_id", "123");
params.push("post_id", "456");
let (user_id, post_id): (i32, i32) = ExtractParams::extract(¶ms).unwrap();
assert_eq!(user_id, 123);
assert_eq!(post_id, 456);
}
#[test]
fn test_params_to_hash_map() {
let mut params = Params::new();
params.push("id", "123");
params.push("name", "test");
let map = params.to_hash_map();
assert_eq!(map.get("id"), Some(&"123".to_string()));
assert_eq!(map.get("name"), Some(&"test".to_string()));
}
#[test]
fn test_catch_all_value_owned_no_leak() {
let pattern = CompiledPattern::new("/files/*path");
for n in 0..1000 {
let path = format!("/files/dir{n}/sub/readme.md");
let params = pattern.match_path(&path).params().unwrap();
assert_eq!(
params.get("path"),
Some(format!("dir{n}/sub/readme.md").as_str())
);
assert_eq!(
params.wildcard(),
Some(format!("dir{n}/sub/readme.md").as_str())
);
}
}
#[test]
fn test_catch_all_owns_beyond_path_lifetime() {
let pattern = CompiledPattern::new("/files/*path");
let owned_map = {
let path = String::from("/files/docs/guide/intro.md");
let params = pattern.match_path(&path).params().unwrap();
params.to_hash_map()
};
assert_eq!(
owned_map.get("path"),
Some(&"docs/guide/intro.md".to_string())
);
assert_eq!(owned_map.get("*"), Some(&"docs/guide/intro.md".to_string()));
}
#[test]
fn test_zero_alloc_catch_all_named_get() {
let params = match_path_zero_alloc("/assets/*rest", "/assets/css/app.css").unwrap();
assert_eq!(params.get("rest"), Some("css/app.css"));
assert_eq!(params.wildcard(), Some("css/app.css"));
}
#[test]
fn test_stats() {
let stats = params_stats();
let _ = stats.match_attempts();
let _ = stats.match_successes();
let _ = stats.match_ratio();
let _ = stats.inline_ratio();
}
}