Skip to content

Commit

Permalink
Support deserializing paths to sequences for multi-component (eg. tai…
Browse files Browse the repository at this point in the history
…l) matches
  • Loading branch information
Maarten Deprez authored and maartendeprez committed Jul 24, 2024
1 parent 270a6a3 commit 4cf9189
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 3 deletions.
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, ...)
- 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 @@ -537,6 +589,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 @@ -632,6 +694,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

0 comments on commit 4cf9189

Please sign in to comment.