use std::fmt;
use crate::form::ValueField;
use crate::http::ext::IntoOwned;
use crate::http::uri::{self, Origin, Path};
use crate::route::Segment;
#[derive(Debug, Clone)]
pub struct RouteUri<'a> {
pub(crate) base: Origin<'a>,
pub(crate) unmounted_origin: Origin<'a>,
pub(crate) uri: Origin<'a>,
pub(crate) metadata: Metadata,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Color {
Static = 3,
Partial = 2,
Wild = 1,
}
#[derive(Debug, Clone)]
pub(crate) struct Metadata {
pub uri_segments: Vec<Segment>,
pub base_len: usize,
pub static_query_fields: Vec<(String, String)>,
pub path_color: Color,
pub query_color: Option<Color>,
pub dynamic_trail: bool,
}
type Result<T, E = uri::Error<'static>> = std::result::Result<T, E>;
impl<'a> RouteUri<'a> {
#[track_caller]
pub(crate) fn new(base: &str, uri: &str) -> RouteUri<'static> {
Self::try_new(base, uri).expect("expected valid route URIs")
}
#[doc(hidden)]
pub fn try_new(base: &str, uri: &str) -> Result<RouteUri<'static>> {
let mut base = Origin::parse(base)
.map_err(|e| e.into_owned())?
.into_normalized()
.into_owned();
base.clear_query();
let origin = Origin::parse_route(uri)
.map_err(|e| e.into_owned())?
.into_normalized()
.into_owned();
let route_uri = match origin.path().as_str() {
"/" if !base.has_trailing_slash() => match origin.query() {
Some(query) => format!("{}?{}", base, query),
None => base.to_string(),
},
_ => format!("{}{}", base, origin),
};
let uri = Origin::parse_route(&route_uri)
.map_err(|e| e.into_owned())?
.into_normalized()
.into_owned();
let metadata = Metadata::from(&base, &uri);
Ok(RouteUri {
base,
unmounted_origin: origin,
uri,
metadata,
})
}
pub fn inner(&self) -> &Origin<'a> {
&self.uri
}
#[inline(always)]
pub fn base(&self) -> Path<'_> {
self.base.path()
}
#[inline(always)]
pub fn unmounted(&self) -> &Origin<'a> {
&self.unmounted_origin
}
pub(crate) fn default_rank(&self) -> isize {
let raw_path_weight = self.metadata.path_color as u8;
let raw_query_weight = self.metadata.query_color.map_or(0, |c| c as u8);
let raw_weight = (raw_path_weight << 2) | raw_query_weight;
-((raw_weight as isize) - 3)
}
}
impl Metadata {
fn from(base: &Origin<'_>, uri: &Origin<'_>) -> Self {
let uri_segments = uri
.path()
.raw_segments()
.map(Segment::from)
.collect::<Vec<_>>();
let query_segs = uri
.query()
.map(|q| q.raw_segments().map(Segment::from).collect::<Vec<_>>())
.unwrap_or_default();
let static_query_fields = query_segs
.iter()
.filter(|s| !s.dynamic)
.map(|s| ValueField::parse(&s.value))
.map(|f| (f.name.source().to_string(), f.value.to_string()))
.collect();
let static_path = uri_segments.iter().all(|s| !s.dynamic);
let wild_path = !uri_segments.is_empty() && uri_segments.iter().all(|s| s.dynamic);
let path_color = match (static_path, wild_path) {
(true, _) => Color::Static,
(_, true) => Color::Wild,
(_, _) => Color::Partial,
};
let query_color = (!query_segs.is_empty()).then(|| {
let static_query = query_segs.iter().all(|s| !s.dynamic);
let wild_query = query_segs.iter().all(|s| s.dynamic);
match (static_query, wild_query) {
(true, _) => Color::Static,
(_, true) => Color::Wild,
(_, _) => Color::Partial,
}
});
let dynamic_trail = uri_segments.last().is_some_and(|p| p.dynamic_trail);
let segments = base.path().segments();
let num_empty = segments.clone().filter(|s| s.is_empty()).count();
let base_len = segments.num() - num_empty;
Metadata {
uri_segments,
base_len,
static_query_fields,
path_color,
query_color,
dynamic_trail,
}
}
}
impl<'a> std::ops::Deref for RouteUri<'a> {
type Target = Origin<'a>;
fn deref(&self) -> &Self::Target {
self.inner()
}
}
impl fmt::Display for RouteUri<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.uri.fmt(f)
}
}
impl<'a, 'b> PartialEq<Origin<'b>> for RouteUri<'a> {
fn eq(&self, other: &Origin<'b>) -> bool {
self.inner() == other
}
}
impl PartialEq<str> for RouteUri<'_> {
fn eq(&self, other: &str) -> bool {
self.inner() == other
}
}
impl PartialEq<&str> for RouteUri<'_> {
fn eq(&self, other: &&str) -> bool {
self.inner() == *other
}
}
#[cfg(test)]
mod tests {
macro_rules! assert_uri_equality {
($base:expr, $path:expr => $ebase:expr, $epath:expr, $efull:expr) => {
let uri = super::RouteUri::new($base, $path);
assert_eq!(
uri, $efull,
"complete URI mismatch. expected {}, got {}",
$efull, uri
);
assert_eq!(
uri.base(),
$ebase,
"expected base {}, got {}",
$ebase,
uri.base()
);
assert_eq!(
uri.unmounted(),
$epath,
"expected unmounted {}, got {}",
$epath,
uri.unmounted()
);
};
}
#[test]
fn test_route_uri_composition() {
assert_uri_equality!("/", "/" => "/", "/", "/");
assert_uri_equality!("/", "/foo" => "/", "/foo", "/foo");
assert_uri_equality!("/", "/foo/bar" => "/", "/foo/bar", "/foo/bar");
assert_uri_equality!("/", "/foo/" => "/", "/foo/", "/foo/");
assert_uri_equality!("/", "/foo/bar/" => "/", "/foo/bar/", "/foo/bar/");
assert_uri_equality!("/foo", "/" => "/foo", "/", "/foo");
assert_uri_equality!("/foo", "/bar" => "/foo", "/bar", "/foo/bar");
assert_uri_equality!("/foo", "/bar/" => "/foo", "/bar/", "/foo/bar/");
assert_uri_equality!("/foo", "/?baz" => "/foo", "/?baz", "/foo?baz");
assert_uri_equality!("/foo", "/bar?baz" => "/foo", "/bar?baz", "/foo/bar?baz");
assert_uri_equality!("/foo", "/bar/?baz" => "/foo", "/bar/?baz", "/foo/bar/?baz");
assert_uri_equality!("/foo/", "/" => "/foo/", "/", "/foo/");
assert_uri_equality!("/foo/", "/bar" => "/foo/", "/bar", "/foo/bar");
assert_uri_equality!("/foo/", "/bar/" => "/foo/", "/bar/", "/foo/bar/");
assert_uri_equality!("/foo/", "/?baz" => "/foo/", "/?baz", "/foo/?baz");
assert_uri_equality!("/foo/", "/bar?baz" => "/foo/", "/bar?baz", "/foo/bar?baz");
assert_uri_equality!("/foo/", "/bar/?baz" => "/foo/", "/bar/?baz", "/foo/bar/?baz");
assert_uri_equality!("/foo?baz", "/" => "/foo", "/", "/foo");
assert_uri_equality!("/foo?baz", "/bar" => "/foo", "/bar", "/foo/bar");
assert_uri_equality!("/foo?baz", "/bar/" => "/foo", "/bar/", "/foo/bar/");
assert_uri_equality!("/foo/?baz", "/" => "/foo/", "/", "/foo/");
assert_uri_equality!("/foo/?baz", "/bar" => "/foo/", "/bar", "/foo/bar");
assert_uri_equality!("/foo/?baz", "/bar/" => "/foo/", "/bar/", "/foo/bar/");
}
}