use std::{
borrow::{Borrow, Cow},
fmt::{Display, Write},
iter::FusedIterator,
mem,
ops::{AddAssign, Deref},
};
use ref_cast::{RefCastCustom, ref_cast_custom};
#[derive(Debug, PartialEq, Eq, Hash, RefCastCustom)]
#[repr(transparent)]
pub struct Path {
inner: str,
}
impl Path {
pub const ROOT: &Path = Path::new("/");
#[must_use]
#[track_caller]
pub const fn new(s: &str) -> &Self {
match Self::from_str(s) {
Ok(path) => path,
Err(err) => panic!("{}", err.message()),
}
}
#[allow(clippy::should_implement_trait)]
pub const fn from_str(s: &str) -> Result<&Self, PathError> {
let s = match s.as_bytes() {
[b'/'] => "",
_ => s,
};
let bytes = s.as_bytes();
let len = bytes.len();
if len == 0 {
return Ok(Self::new_unchecked(s));
}
if bytes[0] != b'/' {
return Err(PathError::MissingLeadingSlash);
}
let mut start = 1;
let mut i = 1;
while i <= len {
if i == len || bytes[i] == b'/' {
let trailing_slash = i == len && start == len && start > 1;
if !trailing_slash && let Err(err) = validate_segment(bytes, start, i) {
return Err(err);
}
start = i + 1;
}
i += 1;
}
Ok(Self::new_unchecked(s))
}
#[ref_cast_custom]
#[must_use]
pub const fn new_unchecked(s: &str) -> &Self;
pub fn segments(&self) -> PathSegments<'_> {
PathSegments::new(self)
}
#[must_use]
pub fn to_matchit_path(&self) -> Cow<'static, str> {
if self.inner.is_empty() {
return Cow::Borrowed("/");
}
let stripped = self
.segments()
.filter(|s| !s.is_group())
.collect::<PathBuf>()
.inner;
if stripped.is_empty() {
return Cow::Borrowed("/");
}
Cow::Owned(stripped)
}
#[must_use]
pub fn starts_with(&self, other: &Path) -> bool {
if self.inner.len() < other.inner.len() {
return false;
}
self.segments().zip(other.segments()).all(|(a, b)| a == b)
}
#[must_use]
pub fn join(&self, other: &Path) -> PathBuf {
let mut buf = self.to_owned();
buf += other;
buf
}
#[must_use]
pub fn matches(&self, url: &str) -> bool {
fn first_segment(rest: &str) -> (&str, Option<&str>) {
match rest.split_once('/') {
Some((head, tail)) => (head, Some(tail)),
None => (rest, None),
}
}
let body = url.strip_prefix('/').unwrap_or(url);
let mut rest = (!body.is_empty()).then_some(body);
for segment in self.segments() {
match segment {
PathSegment::Group(_) => {}
PathSegment::Static("") => return rest == Some(""),
PathSegment::Static(expected) => match rest.map(first_segment) {
Some((head, tail)) if head == expected => rest = tail,
_ => return false,
},
PathSegment::Param(_) => match rest.map(first_segment) {
Some((head, tail)) if !head.is_empty() => rest = tail,
_ => return false,
},
PathSegment::CatchAll(_) => return rest.is_some_and(|rest| !rest.is_empty()),
}
}
rest.is_none()
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.inner
}
#[must_use]
pub fn has_trailing_slash(&self) -> bool {
self.inner.ends_with('/')
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Display for Path {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl ToOwned for Path {
type Owned = PathBuf;
fn to_owned(&self) -> Self::Owned {
PathBuf {
inner: self.inner.to_owned(),
}
}
}
impl<'a> From<&'a Path> for Cow<'a, Path> {
fn from(value: &'a Path) -> Self {
Self::Borrowed(value)
}
}
#[derive(Debug, Clone)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct PathSegments<'path> {
rest: &'path str,
done: bool,
}
impl<'path> PathSegments<'path> {
fn new(path: &'path Path) -> Self {
match path.inner.strip_prefix('/') {
Some(rest) => Self { rest, done: false },
None => Self {
rest: "",
done: true,
},
}
}
fn last_segment(&mut self) -> &'path str {
self.done = true;
mem::take(&mut self.rest)
}
}
impl<'path> Iterator for PathSegments<'path> {
type Item = PathSegment<'path>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
let segment = match self.rest.split_once('/') {
Some((segment, rest)) => {
self.rest = rest;
segment
}
None => self.last_segment(),
};
Some(PathSegment::new_unchecked(segment))
}
}
impl DoubleEndedIterator for PathSegments<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
let segment = match self.rest.rsplit_once('/') {
Some((rest, segment)) => {
self.rest = rest;
segment
}
None => self.last_segment(),
};
Some(PathSegment::new_unchecked(segment))
}
}
impl FusedIterator for PathSegments<'_> {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PathError {
MissingLeadingSlash,
EmptySegment,
MissingClosingBrace,
MissingClosingParen,
UnexpectedBracket,
EmptyName,
InvalidNameStart,
InvalidNameChar,
}
impl PathError {
const fn message(self) -> &'static str {
match self {
Self::MissingLeadingSlash => "invalid path: must be empty or start with `/`",
Self::EmptySegment => "invalid path: empty segment",
Self::MissingClosingBrace => "invalid path: missing closing `}`",
Self::MissingClosingParen => "invalid path: missing closing `)`",
Self::UnexpectedBracket => "invalid path: unexpected bracket in static segment",
Self::EmptyName => "invalid path: segment name must not be empty",
Self::InvalidNameStart => {
"invalid path: segment name must start with a letter or underscore"
}
Self::InvalidNameChar => "invalid path: segment name contains an invalid character",
}
}
}
impl Display for PathError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.message())
}
}
impl std::error::Error for PathError {}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct PathBuf {
inner: String,
}
impl PathBuf {
#[must_use]
pub fn new() -> Self {
PathBuf::default()
}
}
impl Borrow<Path> for PathBuf {
fn borrow(&self) -> &Path {
Path::new_unchecked(&self.inner)
}
}
impl From<PathBuf> for Cow<'static, Path> {
fn from(value: PathBuf) -> Self {
Self::Owned(value)
}
}
impl Deref for PathBuf {
type Target = Path;
fn deref(&self) -> &Self::Target {
Path::new_unchecked(&self.inner)
}
}
impl PathBuf {
fn pop_trailing_slash(&mut self) {
if self.inner.ends_with('/') {
self.inner.pop();
}
}
}
impl AddAssign<PathSegment<'_>> for PathBuf {
fn add_assign(&mut self, rhs: PathSegment<'_>) {
self.pop_trailing_slash();
write!(self.inner, "/{rhs}").unwrap();
}
}
impl AddAssign<&Path> for PathBuf {
fn add_assign(&mut self, rhs: &Path) {
if rhs.is_empty() {
return;
}
self.pop_trailing_slash();
self.inner.push_str(&rhs.inner);
}
}
impl<'a> FromIterator<PathSegment<'a>> for PathBuf {
fn from_iter<T: IntoIterator<Item = PathSegment<'a>>>(iter: T) -> Self {
let mut buf = PathBuf::new();
for segment in iter {
buf += segment;
}
buf
}
}
pub trait IntoPath {
#[track_caller]
fn into_path(self) -> Cow<'static, Path>;
}
impl IntoPath for &'static str {
#[track_caller]
fn into_path(self) -> Cow<'static, Path> {
Cow::Borrowed(Path::new(self))
}
}
impl IntoPath for &'static Path {
fn into_path(self) -> Cow<'static, Path> {
Cow::Borrowed(self)
}
}
impl IntoPath for PathBuf {
fn into_path(self) -> Cow<'static, Path> {
Cow::Owned(self)
}
}
impl IntoPath for Cow<'static, Path> {
fn into_path(self) -> Cow<'static, Path> {
self
}
}
impl Display for PathBuf {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.inner.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PathSegment<'a> {
Static(&'a str),
Group(&'a str),
Param(&'a str),
CatchAll(&'a str),
}
impl<'a> PathSegment<'a> {
#[must_use]
#[track_caller]
pub fn new(s: &'a str) -> Self {
match Self::from_str(s) {
Ok(segment) => segment,
Err(err) => panic!("{}", err.message()),
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &'a str) -> Result<Self, PathError> {
validate_segment(s.as_bytes(), 0, s.len())?;
Ok(Self::new_unchecked(s))
}
#[must_use]
pub fn new_unchecked(s: &'a str) -> Self {
if let Some(inner) = s.strip_prefix('{') {
let inner = inner.strip_suffix('}').unwrap_or(inner);
match inner.strip_prefix('*') {
Some(name) => PathSegment::CatchAll(name),
None => PathSegment::Param(inner),
}
} else if let Some(inner) = s.strip_prefix('(') {
PathSegment::Group(inner.strip_suffix(')').unwrap_or(inner))
} else {
PathSegment::Static(s)
}
}
#[must_use]
pub fn is_static(&self) -> bool {
matches!(self, Self::Static(..))
}
#[must_use]
pub fn is_group(&self) -> bool {
matches!(self, Self::Group(..))
}
#[must_use]
pub fn is_param(&self) -> bool {
matches!(self, Self::Param(..))
}
#[must_use]
pub fn is_catch_all(&self) -> bool {
matches!(self, Self::CatchAll(..))
}
#[must_use]
pub fn as_static(&self) -> Option<&&'a str> {
if let Self::Static(v) = self {
Some(v)
} else {
None
}
}
#[must_use]
pub fn as_group(&self) -> Option<&&'a str> {
if let Self::Group(v) = self {
Some(v)
} else {
None
}
}
#[must_use]
pub fn param_name(&self) -> Option<&'a str> {
match *self {
Self::Param(name) | Self::CatchAll(name) => Some(name),
Self::Static(_) | Self::Group(_) => None,
}
}
#[must_use]
pub fn as_param(&self) -> Option<&&'a str> {
if let Self::Param(v) = self {
Some(v)
} else {
None
}
}
#[must_use]
pub fn as_catch_all(&self) -> Option<&&'a str> {
if let Self::CatchAll(v) = self {
Some(v)
} else {
None
}
}
}
const fn validate_segment(bytes: &[u8], start: usize, end: usize) -> Result<(), PathError> {
if start >= end {
return Err(PathError::EmptySegment);
}
match bytes[start] {
b'{' => {
if bytes[end - 1] != b'}' {
return Err(PathError::MissingClosingBrace);
}
let mut name_start = start + 1;
let name_end = end - 1;
if name_start < name_end && bytes[name_start] == b'*' {
name_start += 1;
}
validate_ident(bytes, name_start, name_end)
}
b'(' => {
if bytes[end - 1] != b')' {
return Err(PathError::MissingClosingParen);
}
validate_ident(bytes, start + 1, end - 1)
}
_ => {
let mut i = start;
while i < end {
match bytes[i] {
b'{' | b'}' | b'(' | b')' => return Err(PathError::UnexpectedBracket),
_ => {}
}
i += 1;
}
Ok(())
}
}
}
const fn validate_ident(bytes: &[u8], start: usize, end: usize) -> Result<(), PathError> {
if start >= end {
return Err(PathError::EmptyName);
}
let first = bytes[start];
if !first.is_ascii_alphabetic() && first != b'_' {
return Err(PathError::InvalidNameStart);
}
let mut i = start + 1;
while i < end {
let ch = bytes[i];
if !ch.is_ascii_alphanumeric() && ch != b'_' {
return Err(PathError::InvalidNameChar);
}
i += 1;
}
Ok(())
}
impl Display for PathSegment<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Static(inner) => f.write_str(inner),
Self::Param(inner) => write!(f, "{{{inner}}}"),
Self::Group(inner) => write!(f, "({inner})"),
Self::CatchAll(inner) => write!(f, "{{*{inner}}}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_root_slash_normalized() {
let path = Path::new("/");
assert_eq!(&path.inner, "");
assert_eq!(path.to_matchit_path(), "/");
assert_eq!(path.segments().count(), 0);
}
#[test]
fn path_segments() {
let path = Path::new("/dashboard/{id}/(auth)");
let segs: Vec<_> = path.segments().collect();
assert_eq!(
segs,
vec![
PathSegment::Static("dashboard"),
PathSegment::Param("id"),
PathSegment::Group("auth"),
]
);
}
#[test]
fn path_single_segment() {
let path = Path::new("/home");
let segs: Vec<_> = path.segments().collect();
assert_eq!(segs, vec![PathSegment::Static("home")]);
}
#[test]
fn path_segments_from_the_back() {
let path = Path::new("/dashboard/{id}/(auth)");
let segs: Vec<_> = path.segments().rev().collect();
assert_eq!(
segs,
vec![
PathSegment::Group("auth"),
PathSegment::Param("id"),
PathSegment::Static("dashboard"),
]
);
}
#[test]
fn path_segments_from_both_ends_meet_in_the_middle() {
let path = Path::new("/a/b/c");
let mut segments = path.segments();
assert_eq!(segments.next(), Some(PathSegment::Static("a")));
assert_eq!(segments.next_back(), Some(PathSegment::Static("c")));
assert_eq!(segments.next(), Some(PathSegment::Static("b")));
assert_eq!(segments.next(), None);
assert_eq!(segments.next_back(), None);
}
#[test]
fn root_path_yields_no_segments_from_either_end() {
let mut segments = Path::new("/").segments();
assert_eq!(segments.next(), None);
assert_eq!(segments.next_back(), None);
}
#[test]
fn trailing_slash_is_an_empty_last_segment() {
let path = Path::new("/users/{id}/");
let segs: Vec<_> = path.segments().collect();
assert_eq!(
segs,
vec![
PathSegment::Static("users"),
PathSegment::Param("id"),
PathSegment::Static(""),
]
);
assert_eq!(path.segments().next_back(), Some(PathSegment::Static("")));
assert_eq!(path.as_str(), "/users/{id}/");
}
#[test]
fn path_to_matchit_strips_groups() {
let path = Path::new("/(auth)/dashboard/{id}");
assert_eq!(path.to_matchit_path(), "/dashboard/{id}");
}
#[test]
fn path_to_matchit_empty() {
let path = Path::new("");
assert_eq!(path.to_matchit_path(), "/");
}
#[test]
fn path_to_matchit_group_only_is_root() {
assert_eq!(Path::new("/(marketing)").to_matchit_path(), "/");
assert_eq!(Path::new("/(a)/(b)").to_matchit_path(), "/");
}
#[test]
fn path_to_matchit_no_groups() {
let path = Path::new("/users/{id}");
assert_eq!(path.to_matchit_path(), "/users/{id}");
}
#[test]
fn path_to_matchit_keeps_trailing_slash() {
assert_eq!(Path::new("/users/").to_matchit_path(), "/users/");
assert_eq!(
Path::new("/(auth)/users/{id}/").to_matchit_path(),
"/users/{id}/"
);
assert_eq!(Path::new("/(marketing)/").to_matchit_path(), "/");
}
#[test]
fn join_appends_segments() {
let joined = Path::new("/settings").join(Path::new("/(admin)/{id}"));
assert_eq!(joined.as_str(), "/settings/(admin)/{id}");
assert_eq!(joined.segments().count(), 3);
}
#[test]
fn join_root_on_either_side_is_identity() {
let path = Path::new("/settings");
assert_eq!(&*path.join(Path::ROOT), path);
assert_eq!(&*Path::ROOT.join(path), path);
assert!(Path::ROOT.join(Path::ROOT).is_empty());
}
#[test]
fn path_buf_add_assign_path() {
let mut buf = PathBuf::new();
buf += Path::new("/users");
buf += PathSegment::Param("id");
buf += Path::new("/posts");
assert_eq!(buf.as_str(), "/users/{id}/posts");
}
#[test]
fn join_drops_a_trailing_slash_when_segments_follow() {
let base = Path::new("/settings/");
assert_eq!(base.join(Path::new("/export")).as_str(), "/settings/export");
assert_eq!(
base.join(Path::new("/export/")).as_str(),
"/settings/export/"
);
let mut buf = base.to_owned();
buf += PathSegment::Param("id");
assert_eq!(buf.as_str(), "/settings/{id}");
}
#[test]
fn join_keeps_a_trailing_slash_when_nothing_follows() {
let path = Path::new("/settings/");
assert_eq!(&*path.join(Path::ROOT), path);
assert_eq!(&*Path::ROOT.join(path), path);
}
#[test]
fn path_starts_with_match() {
let path = Path::new("/users/{id}/posts");
let prefix = Path::new("/users/{id}");
assert!(path.starts_with(prefix));
}
#[test]
fn path_starts_with_no_match() {
let path = Path::new("/users/{id}");
let prefix = Path::new("/posts/{id}");
assert!(!path.starts_with(prefix));
}
#[test]
fn path_starts_with_longer_prefix() {
let path = Path::new("/users");
let prefix = Path::new("/users/{id}/posts");
assert!(!path.starts_with(prefix));
}
#[test]
fn path_starts_with_rejects_partial_segment() {
assert!(!Path::new("/administrator").starts_with(Path::new("/admin")));
}
#[test]
fn path_starts_with_includes_groups() {
let path = Path::new("/(auth)/dashboard");
assert!(path.starts_with(Path::new("/(auth)")));
assert!(!path.starts_with(Path::new("/dashboard")));
}
#[test]
fn path_starts_with_distinguishes_param_names() {
let path = Path::new("/users/{id}/posts");
assert!(path.starts_with(Path::new("/users/{id}")));
assert!(!path.starts_with(Path::new("/users/{user_id}")));
}
#[test]
fn path_starts_with_trailing_slash() {
assert!(Path::new("/users/").starts_with(Path::new("/users")));
assert!(Path::new("/users/").starts_with(Path::new("/users/")));
assert!(!Path::new("/users").starts_with(Path::new("/users/")));
assert!(!Path::new("/users/posts").starts_with(Path::new("/users/")));
}
#[test]
fn path_display() {
let path = Path::new("/users/{id}");
assert_eq!(path.to_string(), "/users/{id}");
}
#[test]
fn matches_static_exact() {
assert!(Path::new("/users/list").matches("/users/list"));
}
#[test]
fn matches_static_mismatch() {
assert!(!Path::new("/users/list").matches("/users/all"));
}
#[test]
fn matches_rejects_partial_segment() {
assert!(!Path::new("/admin").matches("/administrator"));
assert!(!Path::new("/administrator").matches("/admin"));
}
#[test]
fn matches_is_case_sensitive() {
assert!(!Path::new("/admin").matches("/Admin"));
}
#[test]
fn matches_rejects_empty_segments() {
assert!(!Path::new("/admin").matches("//admin"));
assert!(!Path::new("/users/{id}").matches("/users//"));
assert!(!Path::new("/users/{id}/posts").matches("/users//posts"));
}
#[test]
fn matches_treats_percent_encoding_as_opaque() {
assert!(!Path::new("/admin/users").matches("/admin%2Fusers"));
assert!(Path::new("/{page}").matches("/admin%2Fusers"));
}
#[test]
fn matches_param_captures_any_segment() {
let path = Path::new("/users/{id}/posts");
assert!(path.matches("/users/42/posts"));
assert!(path.matches("/users/anything/posts"));
}
#[test]
fn matches_rejects_too_few_segments() {
assert!(!Path::new("/users/{id}/posts").matches("/users/42"));
}
#[test]
fn matches_rejects_trailing_segments() {
assert!(!Path::new("/users/{id}").matches("/users/42/posts"));
}
#[test]
fn matches_ignores_groups() {
assert!(Path::new("/(auth)/dashboard").matches("/dashboard"));
assert!(Path::new("/(a)/{id}/(b)").matches("/42"));
}
#[test]
fn matches_root() {
assert!(Path::new("/").matches("/"));
assert!(!Path::new("/").matches("/anything"));
}
#[test]
fn matches_group_only_path_is_root() {
assert!(Path::new("/(marketing)").matches("/"));
}
#[test]
fn matches_trailing_slash_exactly() {
assert!(!Path::new("/users").matches("/users/"));
assert!(Path::new("/users/").matches("/users/"));
assert!(!Path::new("/users/").matches("/users"));
assert!(!Path::new("/users/").matches("/users//"));
assert!(!Path::new("/users/").matches("/users/posts"));
assert!(Path::new("/users/{id}/").matches("/users/42/"));
assert!(!Path::new("/users/{id}/").matches("/users/42"));
assert!(!Path::new("/users/{id}").matches("/users/"));
}
#[test]
fn matches_root_rejects_doubled_slash() {
assert!(!Path::new("/").matches("//"));
}
#[test]
fn matches_catch_all() {
let path = Path::new("/files/{*rest}");
assert!(path.matches("/files/a"));
assert!(path.matches("/files/a/b/c"));
}
#[test]
fn matches_catch_all_requires_a_segment() {
assert!(!Path::new("/files/{*rest}").matches("/files"));
assert!(!Path::new("/files/{*rest}").matches("/files/"));
}
#[test]
fn matches_catch_all_swallows_empty_segments() {
assert!(Path::new("/files/{*rest}").matches("/files//"));
}
#[test]
fn matches_non_origin_form_urls() {
assert!(!Path::new("/").matches("*"));
assert!(!Path::new("/admin").matches("*"));
assert!(Path::new("/").matches(""));
assert!(!Path::new("/admin").matches(""));
}
#[test]
fn from_str_accepts_valid_paths() {
for input in [
"",
"/",
"/users",
"/users/{id}",
"/users/{id}/posts/{*rest}",
"/(auth)/dashboard/{user_id}",
"/{_private}",
"/users/",
"/users/{id}/",
"/(marketing)/",
] {
assert!(Path::from_str(input).is_ok(), "rejected `{input}`");
}
}
#[test]
fn from_str_reports_errors() {
use PathError::*;
let cases = [
("users", MissingLeadingSlash),
("//", EmptySegment),
("/users//", EmptySegment),
("/users//posts", EmptySegment),
("/foo{bar}", UnexpectedBracket),
("/{id", MissingClosingBrace),
("/(auth", MissingClosingParen),
("/{}", EmptyName),
("/{*}", EmptyName),
("/{0id}", InvalidNameStart),
("/{id-name}", InvalidNameChar),
("/(my-group)", InvalidNameChar),
];
for (input, expected) in cases {
assert_eq!(Path::from_str(input), Err(expected), "for `{input}`");
}
}
#[test]
fn new_validates_in_const_context() {
const PATH: &Path = Path::new("/users/{id}/(auth)");
assert_eq!(PATH.segments().count(), 3);
}
#[test]
#[should_panic(expected = "unexpected bracket")]
fn new_panics_on_invalid() {
let _ = Path::new("/foo{bar}");
}
#[test]
fn pathbuf_new_is_empty() {
let buf = PathBuf::new();
assert_eq!(buf.to_string(), "");
}
#[test]
fn pathbuf_add_assign() {
let mut buf = PathBuf::new();
buf += PathSegment::Static("users");
buf += PathSegment::Param("id");
assert_eq!(buf.to_string(), "/users/{id}");
}
#[test]
fn pathbuf_add_assign_trailing_slash() {
let mut buf = PathBuf::new();
buf += PathSegment::Static("users");
buf += PathSegment::Static("");
assert_eq!(buf.to_string(), "/users/");
assert_eq!(&*buf, Path::new("/users/"));
}
#[test]
fn pathbuf_from_iterator() {
let buf: PathBuf = vec![
PathSegment::Static("api"),
PathSegment::Static("v1"),
PathSegment::Param("resource"),
]
.into_iter()
.collect();
assert_eq!(buf.to_string(), "/api/v1/{resource}");
}
#[test]
fn pathbuf_deref_to_path() {
let mut buf = PathBuf::new();
buf += PathSegment::Static("users");
let path: &Path = &buf;
let segs: Vec<_> = path.segments().collect();
assert_eq!(segs, vec![PathSegment::Static("users")]);
}
#[test]
fn pathbuf_to_owned_roundtrip() {
let path = Path::new("/users/{id}");
let buf = path.to_owned();
assert_eq!(&*buf, path);
}
#[test]
fn static_segment() {
let seg = PathSegment::new("dashboard");
assert!(seg.is_static());
assert_eq!(seg.as_static(), Some(&"dashboard"));
}
#[test]
fn param_segment() {
let seg = PathSegment::new("{id}");
assert!(seg.is_param());
assert_eq!(seg.as_param(), Some(&"id"));
}
#[test]
fn param_with_underscore() {
let seg = PathSegment::new("{user_id}");
assert!(seg.is_param());
assert_eq!(seg.as_param(), Some(&"user_id"));
}
#[test]
fn catch_all_segment() {
let seg = PathSegment::new("{*rest}");
assert!(matches!(seg, PathSegment::CatchAll("rest")));
}
#[test]
fn group_segment() {
let seg = PathSegment::new("(auth)");
assert!(seg.is_group());
assert_eq!(seg.as_group(), Some(&"auth"));
}
#[test]
fn only_param_and_catch_all_segments_capture() {
assert_eq!(PathSegment::new("{id}").param_name(), Some("id"));
assert_eq!(PathSegment::new("{*rest}").param_name(), Some("rest"));
assert_eq!(PathSegment::new("users").param_name(), None);
assert_eq!(PathSegment::new("(auth)").param_name(), None);
}
#[test]
fn display_roundtrip() {
for input in ["dashboard", "{id}", "{*rest}", "(auth)"] {
assert_eq!(PathSegment::new(input).to_string(), input);
}
}
#[test]
#[should_panic(expected = "missing closing `}`")]
fn param_missing_close() {
let _ = PathSegment::new("{id");
}
#[test]
#[should_panic(expected = "missing closing `)`")]
fn group_missing_close() {
let _ = PathSegment::new("(auth");
}
#[test]
#[should_panic(expected = "empty segment")]
fn empty_segment() {
let _ = PathSegment::new("");
}
#[test]
#[should_panic(expected = "unexpected bracket")]
fn static_with_braces() {
let _ = PathSegment::new("foo{bar}");
}
#[test]
#[should_panic(expected = "name must not be empty")]
fn param_empty_name() {
let _ = PathSegment::new("{}");
}
#[test]
#[should_panic(expected = "name must not be empty")]
fn group_empty_name() {
let _ = PathSegment::new("()");
}
#[test]
#[should_panic(expected = "name must not be empty")]
fn catch_all_empty_name() {
let _ = PathSegment::new("{*}");
}
#[test]
#[should_panic(expected = "must start with a letter or underscore")]
fn param_invalid_start() {
let _ = PathSegment::new("{0id}");
}
#[test]
#[should_panic(expected = "contains an invalid character")]
fn param_invalid_char() {
let _ = PathSegment::new("{id-name}");
}
#[test]
#[should_panic(expected = "must start with a letter or underscore")]
fn group_invalid_start() {
let _ = PathSegment::new("(0auth)");
}
#[test]
#[should_panic(expected = "contains an invalid character")]
fn group_invalid_char() {
let _ = PathSegment::new("(my-group)");
}
#[test]
fn underscore_leading_ident() {
let seg = PathSegment::new("{_private}");
assert!(seg.is_param());
assert_eq!(seg.as_param(), Some(&"_private"));
}
}