diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 30101e3ee..355f13fc8 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -2,6 +2,10 @@ ## Unreleased +- Add support for extracting multi-component path params into a sequence (Vec, tuple, ...). [#3432] + +[#3432]: https://github.com/actix/actix-web/pull/3432 + ## 0.5.4 - Minimum supported Rust version (MSRV) is now 1.88. diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs index f06911b34..d255704fe 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -399,11 +399,25 @@ impl<'de> Deserializer<'de> for Value<'de> { visitor.visit_newtype_struct(self) } - fn deserialize_tuple(self, _: usize, _: V) -> Result + fn deserialize_tuple(self, len: usize, visitor: V) -> Result where V: Visitor<'de>, { - Err(de::value::Error::custom("unsupported type: tuple")) + 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(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_seq(ValueSeq::new(self.value)) } fn deserialize_struct( @@ -421,13 +435,13 @@ impl<'de> Deserializer<'de> for Value<'de> { fn deserialize_tuple_struct( self, _: &'static str, - _: usize, - _: V, + len: usize, + visitor: V, ) -> Result where V: Visitor<'de>, { - Err(de::value::Error::custom("unsupported type: tuple struct")) + self.deserialize_tuple(len, visitor) } fn deserialize_any(self, visitor: V) -> Result @@ -463,7 +477,6 @@ impl<'de> Deserializer<'de> for Value<'de> { } } - unsupported_type!(deserialize_seq, "seq"); unsupported_type!(deserialize_map, "map"); unsupported_type!(deserialize_identifier, "identifier"); } @@ -533,6 +546,43 @@ impl<'de> de::VariantAccess<'de> for UnitVariant { } } +struct ValueSeq<'de> { + elems: std::str::Split<'de, char>, +} + +impl<'de> ValueSeq<'de> { + fn new(value: &'de str) -> Self { + Self { + elems: value.split('/'), + } + } + + fn len(&self) -> usize { + self.elems.clone().filter(|s| !s.is_empty()).count() + } +} + +impl<'de> de::SeqAccess<'de> for ValueSeq<'de> { + type Error = de::value::Error; + + fn next_element_seed(&mut self, seed: T) -> Result, 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 { + Some(self.len()) + } +} + #[cfg(test)] mod tests { use serde::Deserialize; @@ -567,6 +617,24 @@ mod tests { val: TestEnum, } + #[derive(Debug, Deserialize)] + struct TestSeq1 { + tail: Vec, + } + + #[derive(Debug, Deserialize)] + struct TestSeq2 { + tail: (String, String, String), + } + + #[derive(Debug, Deserialize)] + struct TestSeq3 { + tail: TestTupleStruct, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct TestTupleStruct(String, String, String); + #[test] fn test_request_extract() { let mut router = Router::<()>::build(); @@ -662,6 +730,62 @@ 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") + ) + ); + + let i: TestSeq3 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!( + i.tail, + TestTupleStruct( + String::from("tail"), + String::from("with"), + String::from("slash/es") + ) + ); + } + + #[test] + fn test_value_seq_size_hint_counts_remaining_elements() { + use serde::de::SeqAccess as _; + + let mut seq = ValueSeq::new("tail/with/slash"); + + assert_eq!(seq.size_hint(), Some(3)); + + let elem = seq.next_element::().unwrap(); + assert_eq!(elem.as_deref(), Some("tail")); + assert_eq!(seq.size_hint(), Some(2)); + } + #[test] fn test_extract_errors() { let mut router = Router::<()>::build(); diff --git a/actix-web/src/types/path.rs b/actix-web/src/types/path.rs index 5f22568cc..8bae70755 100644 --- a/actix-web/src/types/path.rs +++ b/actix-web/src/types/path.rs @@ -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, +/// } +/// +/// // extract `Tail` from a path using serde +/// #[get("/path/to/{tail}*")] +/// async fn index(info: web::Path) -> String { +/// format!("Navigating to {}!", info.tail.join(" :: ")) +/// } +/// ``` #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From)] pub struct Path(T);