use std::{iter::FusedIterator, slice};
use percent_encoding::percent_decode_str;
use topcoat_core::context::{Cx, request_context};
use crate::{Path, PathSegment, PathSegments, endpoint};
pub trait PathParam {
type Output<'cx>;
#[doc(hidden)]
#[track_caller]
fn path_param(cx: &Cx, _: PathParamSealed) -> Self::Output<'_>;
}
#[inline]
#[must_use]
#[track_caller]
pub fn path_param<T: PathParam + ?Sized>(cx: &Cx) -> T::Output<'_> {
T::path_param(cx, PathParamSealed::new())
}
#[inline]
#[track_caller]
pub fn raw_path_params(cx: &Cx) -> RawPathParamsIter<'_> {
let params = request_context::<RawPathParams>(cx);
RawPathParamsIter {
segments: ParamSegments::new(endpoint(cx).path()),
values: params.values.iter(),
catch_all: ¶ms.catch_all,
}
}
#[doc(hidden)]
#[must_use]
#[track_caller]
pub fn path_param_segment<'cx>(cx: &'cx Cx, name: &str) -> &'cx str {
match find(cx, name) {
Some(RawPathParamValue::Segment(value)) => value,
Some(RawPathParamValue::CatchAll { .. }) => {
panic!("path parameter \"{name}\" captured multiple segments")
}
None => panic!("path parameter \"{name}\" was not found in request path"),
}
}
#[doc(hidden)]
#[track_caller]
pub fn path_param_segments<'cx>(cx: &'cx Cx, name: &str) -> CatchAllSegments<'cx> {
match find(cx, name) {
Some(RawPathParamValue::CatchAll { segments, .. }) => segments,
Some(RawPathParamValue::Segment(_)) => {
panic!("path parameter \"{name}\" captured one segment")
}
None => panic!("path parameter \"{name}\" was not found in request path"),
}
}
#[track_caller]
fn find<'cx>(cx: &'cx Cx, name: &str) -> Option<RawPathParamValue<'cx>> {
raw_path_params(cx).find_map(|(param, value)| (param == name).then_some(value))
}
#[derive(Debug, Clone)]
pub enum RawPathParamValue<'params> {
Segment(&'params str),
CatchAll {
tail: &'params str,
segments: CatchAllSegments<'params>,
},
}
impl<'params> RawPathParamValue<'params> {
#[must_use]
pub fn as_str(&self) -> &'params str {
match self {
Self::Segment(value) => value,
Self::CatchAll { tail, .. } => tail,
}
}
}
#[derive(Debug, Clone)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct CatchAllSegments<'params> {
inner: slice::Iter<'params, Box<str>>,
}
impl<'params> CatchAllSegments<'params> {
fn new(segments: &'params [Box<str>]) -> Self {
Self {
inner: segments.iter(),
}
}
}
impl<'params> Iterator for CatchAllSegments<'params> {
type Item = &'params str;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(Box::as_ref)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl DoubleEndedIterator for CatchAllSegments<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back().map(Box::as_ref)
}
}
impl ExactSizeIterator for CatchAllSegments<'_> {}
impl FusedIterator for CatchAllSegments<'_> {}
#[derive(Debug, Clone, Default)]
pub struct RawPathParams {
values: Vec<Box<str>>,
catch_all: Box<[Box<str>]>,
}
impl RawPathParams {
pub(crate) fn from_match<'values>(
path: &Path,
values: impl IntoIterator<Item = &'values str>,
) -> Self {
let mut values = values.into_iter();
let mut params = Self::default();
for (segment, value) in ParamSegments::new(path).zip(values.by_ref()) {
match segment {
PathSegment::CatchAll(_) => {
params.catch_all = value.split('/').map(decode).collect();
params.values.push(Box::from(value));
}
_ => params.values.push(decode(value)),
}
}
debug_assert!(
values.next().is_none(),
"the match captured more values than `{path}` declares parameters"
);
params
}
}
#[derive(Debug, Clone)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct RawPathParamsIter<'params> {
segments: ParamSegments<'params>,
values: slice::Iter<'params, Box<str>>,
catch_all: &'params [Box<str>],
}
impl<'params> RawPathParamsIter<'params> {
fn pair(
&self,
segment: &PathSegment<'params>,
value: &'params str,
) -> Option<(&'params str, RawPathParamValue<'params>)> {
let name = segment.param_name()?;
let value = match segment {
PathSegment::CatchAll(_) => RawPathParamValue::CatchAll {
tail: value,
segments: CatchAllSegments::new(self.catch_all),
},
_ => RawPathParamValue::Segment(value),
};
Some((name, value))
}
}
impl<'params> Iterator for RawPathParamsIter<'params> {
type Item = (&'params str, RawPathParamValue<'params>);
fn next(&mut self) -> Option<Self::Item> {
let segment = self.segments.next()?;
let value = self.values.next()?;
self.pair(&segment, value)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.values.size_hint()
}
}
impl DoubleEndedIterator for RawPathParamsIter<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
let segment = self.segments.next_back()?;
let value = self.values.next_back()?;
self.pair(&segment, value)
}
}
impl ExactSizeIterator for RawPathParamsIter<'_> {}
impl FusedIterator for RawPathParamsIter<'_> {}
#[derive(Debug, Clone)]
struct ParamSegments<'path>(PathSegments<'path>);
impl<'path> ParamSegments<'path> {
fn new(path: &'path Path) -> Self {
Self(path.segments())
}
}
impl<'path> Iterator for ParamSegments<'path> {
type Item = PathSegment<'path>;
fn next(&mut self) -> Option<Self::Item> {
self.0.find(|segment| segment.param_name().is_some())
}
}
impl DoubleEndedIterator for ParamSegments<'_> {
fn next_back(&mut self) -> Option<Self::Item> {
self.0.rfind(|segment| segment.param_name().is_some())
}
}
impl FusedIterator for ParamSegments<'_> {}
fn decode(value: &str) -> Box<str> {
percent_decode_str(value)
.decode_utf8_lossy()
.into_owned()
.into_boxed_str()
}
#[doc(hidden)]
#[derive(Debug)]
pub struct PathParamSealed(());
impl PathParamSealed {
pub(crate) fn new() -> Self {
Self(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::router::test_matched_cx;
fn matched<'values>(path: &str, values: impl IntoIterator<Item = &'values str>) -> Cx {
let path = Path::new(path);
let params = RawPathParams::from_match(path, values);
test_matched_cx(path).with(params)
}
fn pairs(cx: &Cx) -> Vec<(&str, &str)> {
raw_path_params(cx)
.map(|(name, value)| (name, value.as_str()))
.collect()
}
#[test]
fn decodes_a_single_segment() {
let cx = matched("/users/{id}", ["a%20b"]);
assert_eq!(path_param_segment(&cx, "id"), "a b");
assert_eq!(pairs(&cx), [("id", "a b")]);
}
#[test]
fn keeps_a_catch_all_tail_raw() {
let cx = matched("/docs/{*path}", ["guides/getting%2Fstarted"]);
assert_eq!(pairs(&cx), [("path", "guides/getting%2Fstarted")]);
assert_eq!(
path_param_segments(&cx, "path").collect::<Vec<_>>(),
["guides", "getting/started"]
);
}
#[test]
fn reads_a_single_segment_catch_all() {
let cx = matched("/docs/{*path}", ["readme.md"]);
assert_eq!(
path_param_segments(&cx, "path").collect::<Vec<_>>(),
["readme.md"]
);
}
#[test]
fn iteration_carries_both_views_of_a_catch_all() {
let cx = matched("/docs/{*path}", ["a/b%2Fc"]);
let (name, value) = raw_path_params(&cx).next().unwrap();
let RawPathParamValue::CatchAll { tail, segments } = value else {
panic!("a catch-all segment captures a catch-all value");
};
assert_eq!(name, "path");
assert_eq!(tail, "a/b%2Fc");
assert_eq!(segments.collect::<Vec<_>>(), ["a", "b/c"]);
}
#[test]
fn only_capturing_segments_are_named() {
let cx = matched("/users/(auth)/{id}/docs/{*path}", ["42", "a/b"]);
assert_eq!(pairs(&cx), [("id", "42"), ("path", "a/b")]);
}
#[test]
fn iterates_from_either_end() {
let cx = matched("/users/{id}/docs/{*path}", ["42", "a/b"]);
let params = raw_path_params(&cx);
assert_eq!(params.len(), 2);
assert_eq!(
params
.rev()
.map(|(name, value)| (name, value.as_str()))
.collect::<Vec<_>>(),
[("path", "a/b"), ("id", "42")]
);
}
#[test]
fn a_path_without_parameters_captures_nothing() {
let cx = matched("/users", []);
assert_eq!(raw_path_params(&cx).count(), 0);
assert!(find(&cx, "id").is_none());
}
#[test]
fn missing_parameter_is_absent() {
let cx = matched("/users/{id}", ["42"]);
assert!(find(&cx, "slug").is_none());
}
#[test]
#[should_panic(expected = "was not found in request path")]
fn reading_a_missing_parameter_panics() {
let cx = matched("/users/{id}", ["42"]);
let _ = path_param_segment(&cx, "slug");
}
#[test]
#[should_panic(expected = "captured multiple segments")]
fn reading_a_catch_all_as_one_segment_panics() {
let cx = matched("/docs/{*path}", ["a/b"]);
let _ = path_param_segment(&cx, "path");
}
#[test]
#[should_panic(expected = "captured one segment")]
fn reading_one_segment_as_a_catch_all_panics() {
let cx = matched("/users/{id}", ["42"]);
let _ = path_param_segments(&cx, "id");
}
#[test]
fn segments_count_without_consuming() {
let cx = matched("/docs/{*path}", ["a/b/c"]);
let segments = path_param_segments(&cx, "path");
assert_eq!(segments.len(), 3);
assert_eq!(segments.rev().collect::<Vec<_>>(), ["c", "b", "a"]);
}
}