Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support deserializing paths to sequences for multi-component (eg. tail) matches #3432

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions actix-router/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
## 0.5.3

- Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size.
- Add support for extracting multi-component path params into a sequence (Vec, tuple, ...)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to unreleased section

- Minimum supported Rust version (MSRV) is now 1.72.

## 0.5.2
Expand Down
101 changes: 98 additions & 3 deletions actix-router/src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,11 +396,25 @@ impl<'de> Deserializer<'de> for Value<'de> {
visitor.visit_newtype_struct(self)
}

fn deserialize_tuple<V>(self, _: usize, _: V) -> Result<V::Value, Self::Error>
fn deserialize_tuple<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
let value_seq = ValueSeq::new(self.value);
if len == value_seq.len() {
visitor.visit_seq(value_seq)
} else {
Err(de::value::Error::custom(
"path and tuple lengths don't match",
))
}
}

fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(de::value::Error::custom("unsupported type: tuple"))
visitor.visit_seq(ValueSeq::new(self.value))
}

fn deserialize_struct<V>(
Expand Down Expand Up @@ -428,7 +442,6 @@ impl<'de> Deserializer<'de> for Value<'de> {
}

unsupported_type!(deserialize_any, "any");
unsupported_type!(deserialize_seq, "seq");
unsupported_type!(deserialize_map, "map");
unsupported_type!(deserialize_identifier, "identifier");
}
Expand Down Expand Up @@ -498,6 +511,45 @@ impl<'de> de::VariantAccess<'de> for UnitVariant {
}
}

struct ValueSeq<'de> {
value: &'de str,
elems: std::str::Split<'de, char>,
}

impl<'de> ValueSeq<'de> {
fn new(value: &'de str) -> Self {
Self {
value,
elems: value.split('/'),
}
}

fn len(&self) -> usize {
self.value.split('/').filter(|s| !s.is_empty()).count()
}
}

impl<'de> de::SeqAccess<'de> for ValueSeq<'de> {
type Error = de::value::Error;

fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
where
T: de::DeserializeSeed<'de>,
{
for elem in &mut self.elems {
if !elem.is_empty() {
return seed.deserialize(Value { value: elem }).map(Some);
}
}

Ok(None)
}

fn size_hint(&self) -> Option<usize> {
Some(self.len())
}
}

#[cfg(test)]
mod tests {
use serde::Deserialize;
Expand Down Expand Up @@ -532,6 +584,16 @@ mod tests {
val: TestEnum,
}

#[derive(Debug, Deserialize)]
struct TestSeq1 {
tail: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct TestSeq2 {
tail: (String, String, String),
}

#[test]
fn test_request_extract() {
let mut router = Router::<()>::build();
Expand Down Expand Up @@ -627,6 +689,39 @@ mod tests {
assert!(format!("{:?}", i).contains("unknown variant"));
}

#[test]
fn test_extract_seq() {
let mut router = Router::<()>::build();
router.path("/path/to/{tail}*", ());
let router = router.finish();

let mut path = Path::new("/path/to/tail/with/slash%2fes");
assert!(router.recognize(&mut path).is_some());

let i: (String,) = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap();
assert_eq!(i.0, String::from("tail/with/slash/es"));

let i: TestSeq1 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap();
assert_eq!(
i.tail,
vec![
String::from("tail"),
String::from("with"),
String::from("slash/es")
]
);

let i: TestSeq2 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap();
assert_eq!(
i.tail,
(
String::from("tail"),
String::from("with"),
String::from("slash/es")
)
);
}

#[test]
fn test_extract_errors() {
let mut router = Router::<()>::build();
Expand Down
20 changes: 20 additions & 0 deletions actix-web/src/types/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ use crate::{
/// format!("Welcome {}!", info.name)
/// }
/// ```
///
/// Segments matching multiple path components can be deserialized
/// into a Vec<_> to percent-decode the components individually. Empty
/// path components are ignored.
///
/// ```
/// use actix_web::{get, web};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Tail {
/// tail: Vec<String>,
/// }
///
/// // extract `Tail` from a path using serde
/// #[get("/path/to/{tail}*")]
/// async fn index(info: web::Path<Tail>) -> String {
/// format!("Navigating to {}!", info.tail.join(" :: "))
/// }
/// ```
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From)]
pub struct Path<T>(T);

Expand Down
Loading