From feee43094ea9d8d9503c3d28cdea9004de4ab0a3 Mon Sep 17 00:00:00 2001 From: Maarten Deprez Date: Thu, 15 Aug 2024 16:54:51 +0200 Subject: [PATCH] Support deserializing paths to sequences for multi-component (eg. tail) matches --- actix-router/CHANGES.md | 1 + actix-router/src/de.rs | 101 ++++++++++++++++++++++++++++++++++-- actix-web/src/types/path.rs | 20 +++++++ 3 files changed, 119 insertions(+), 3 deletions(-) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 6305b45c..fe3d0c96 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -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, ...) - Minimum supported Rust version (MSRV) is now 1.72. ## 0.5.2 diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs index 2f50619f..7c66fd46 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -396,11 +396,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( @@ -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"); } @@ -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(&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; @@ -532,6 +584,16 @@ mod tests { val: TestEnum, } + #[derive(Debug, Deserialize)] + struct TestSeq1 { + tail: Vec, + } + + #[derive(Debug, Deserialize)] + struct TestSeq2 { + tail: (String, String, String), + } + #[test] fn test_request_extract() { let mut router = Router::<()>::build(); @@ -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(); diff --git a/actix-web/src/types/path.rs b/actix-web/src/types/path.rs index d6cf186f..624e4d33 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);