use crate::{Captures, ReverseMatch, Segment};
use smartstring::alias::String as SmartString;
use std::{
cmp::Ordering,
convert::TryFrom,
fmt::{self, Debug, Display, Formatter},
iter,
str::FromStr,
};
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct RouteSpec {
source: Option<SmartString>,
segments: Vec<Segment>,
}
impl Display for RouteSpec {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("/")?;
for segment in &self.segments {
match segment {
Segment::Slash => f.write_str("/")?,
Segment::Dot => f.write_str(".")?,
Segment::Exact(s) => f.write_str(s)?,
Segment::Param(p) => f.write_fmt(format_args!(":{}", p))?,
Segment::Wildcard => f.write_str("*")?,
};
}
Ok(())
}
}
impl RouteSpec {
fn dots(&self) -> usize {
self.segments
.iter()
.filter(|c| matches!(c, Segment::Dot))
.count()
}
pub fn source(&self) -> Option<&str> {
self.source.as_deref()
}
pub fn segments(&self) -> &[Segment] {
self.segments.as_slice()
}
#[inline]
fn inner_match<'path>(
&self,
mut path: &'path str,
captures: &mut Vec<&'path str>,
) -> Option<&'path str> {
let mut peek = self.segments.iter().peekable();
while let Some(segment) = peek.next() {
path = match segment {
Segment::Exact(e) => {
if path.starts_with(&**e) {
&path[e.len()..]
} else {
return None;
}
}
Segment::Param(_) => {
if path.is_empty() {
return None;
}
match peek.peek() {
None | Some(Segment::Slash) => {
#[cfg(feature = "memchr")]
let capture = memchr::memchr(b'/', path.as_bytes())
.map(|index| &path[..index])
.unwrap_or(path);
#[cfg(not(feature = "memchr"))]
let capture = path.split('/').next()?;
captures.push(capture);
&path[capture.len()..]
}
Some(Segment::Dot) => {
#[cfg(feature = "memchr")]
let index = memchr::memchr2(b'.', b'/', path.as_bytes())?;
#[cfg(not(feature = "memchr"))]
let index = path.find(|c| c == '.' || c == '/')?;
if path.chars().nth(index) == Some('.') {
captures.push(&path[..index]);
&path[index..] } else {
return None;
}
}
_ => panic!(
"param must be followed by a dot, a slash, or the end of the route"
),
}
}
Segment::Wildcard => match peek.peek() {
Some(_) => panic!(concat!(
"wildcard must currently be the terminal segment, ",
"please file an issue if you have a use case for a mid-route *"
)),
None => {
captures.push(path);
""
}
},
Segment::Slash => match (path.chars().next(), peek.peek()) {
(Some('/'), Some(_)) => &path[1..],
(None, None) => path,
(None, Some(Segment::Wildcard)) => path,
_ => return None,
},
Segment::Dot => match path.chars().next() {
Some('.') => &path[1..],
_ => return None,
},
}
}
Some(path)
}
#[inline]
pub fn matches<'path>(&self, path: &'path str) -> Option<Vec<&'path str>> {
let mut p = path.trim_start_matches('/').trim_end_matches('/');
let mut captures = vec![];
p = self.inner_match(p, &mut captures)?;
if p.is_empty() || p == "/" {
Some(captures)
} else {
None
}
}
pub fn template<'route, 'keys, 'captures, 'values>(
&'route self,
captures: &'captures Captures<'keys, 'values>,
) -> Option<ReverseMatch<'keys, 'values, 'captures, 'route>> {
ReverseMatch::new(captures, self)
}
}
impl FromStr for RouteSpec {
type Err = String;
fn from_str(source: &str) -> Result<Self, Self::Err> {
let mut last_index = 0;
let source_trimmed = source.trim_start_matches('/').trim_end_matches('/');
#[cfg(feature = "memchr")]
let index_iter = memchr::memchr2_iter(b'.', b'/', source_trimmed.as_bytes());
#[cfg(not(feature = "memchr"))]
let index_iter = source_trimmed
.match_indices(|c| c == '.' || c == '/')
.map(|(i, _)| i);
let segments = index_iter
.chain(iter::once_with(|| source_trimmed.len()))
.try_fold(vec![], |mut acc, index| {
let first_char = if last_index == 0 {
None
} else {
source_trimmed.chars().nth(last_index - 1)
};
let section = &source_trimmed[last_index..index];
last_index = index + 1;
let segment = match (section.chars().next(), section.len()) {
(Some('*'), 1) => Some(Segment::Wildcard),
(Some('*'), _) => {
return Err(format!(
concat!(
"since there can only be one wildcard,",
" it doesn't need a name. replace `{}` with `*`"
),
section
));
}
(Some(':'), 1) => {
return Err(String::from("params must be named"));
}
(Some(':'), _) => Some(Segment::Param(SmartString::from(§ion[1..]))),
(None, 0) => None,
(_, _) => Some(Segment::Exact(SmartString::from(section))),
};
if first_char == Some('.') {
if let Some(Segment::Exact(s)) = acc.last_mut() {
s.push('.');
} else {
acc.push(Segment::Dot);
}
}
if let Some(segment) = segment {
if first_char == Some('/') {
acc.push(Segment::Slash);
}
acc.push(segment);
}
Ok(acc)
})?;
Ok(Self {
source: Some(SmartString::from(source)),
segments,
})
}
}
impl TryFrom<&str> for RouteSpec {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
impl TryFrom<String> for RouteSpec {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<Vec<Segment>> for RouteSpec {
fn from(segments: Vec<Segment>) -> Self {
Self {
segments,
source: None,
}
}
}
impl PartialOrd for RouteSpec {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RouteSpec {
fn cmp(&self, other: &Self) -> Ordering {
self.segments
.iter()
.zip(&other.segments)
.map(|(mine, theirs)| mine.cmp(theirs))
.chain(iter::once_with(|| self.dots().cmp(&other.dots())))
.chain(iter::once_with(|| {
other.segments.len().cmp(&self.segments.len())
}))
.find(|c| *c != Ordering::Equal)
.unwrap_or(Ordering::Less)
.reverse()
}
}