From 4f0912d1c730b167bc45872090326d93ffc9fd24 Mon Sep 17 00:00:00 2001 From: nitn3lav <77448526+nitn3lav@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:53:23 +0100 Subject: [PATCH] PathDeserializer: use `deserialize_str` for `deserialize_any` (#2881) * PathDeserializer: use `deserialize_str` for `deserialize_any` * fix `deserialize_any` for `seq` and `map` * add tests for `deserialize_any` * parse numeric values as well --------- Co-authored-by: Rob Ede Co-authored-by: Yuki Okushi --- actix-router/CHANGES.md | 3 + actix-router/src/de.rs | 154 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 3 deletions(-) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 781bdefe5..950880dc8 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -3,6 +3,9 @@ ## Unreleased - Minimum supported Rust version (MSRV) is now 1.88. +- Support `deserialize_any` in `PathDeserializer` (enables derived `#[serde(untagged)]` enums in path segments). [#2881] + +[#2881]: https://github.com/actix/actix-web/pull/2881 ## 0.5.3 diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs index 2f50619f8..f06911b34 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -27,6 +27,9 @@ macro_rules! unsupported_type { macro_rules! parse_single_value { ($trait_fn:ident) => { + parse_single_value!($trait_fn, $trait_fn); + }; + ($trait_fn:ident, $visit_fn:ident) => { fn $trait_fn(self, visitor: V) -> Result where V: Visitor<'de>, @@ -43,7 +46,7 @@ macro_rules! parse_single_value { Value { value: &self.path[0], } - .$trait_fn(visitor) + .$visit_fn(visitor) } } }; @@ -205,11 +208,11 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T> }) } - unsupported_type!(deserialize_any, "'any'"); unsupported_type!(deserialize_option, "Option"); unsupported_type!(deserialize_identifier, "identifier"); unsupported_type!(deserialize_ignored_any, "ignored_any"); + parse_single_value!(deserialize_any); parse_single_value!(deserialize_bool); parse_single_value!(deserialize_i8); parse_single_value!(deserialize_i16); @@ -427,7 +430,39 @@ impl<'de> Deserializer<'de> for Value<'de> { Err(de::value::Error::custom("unsupported type: tuple struct")) } - unsupported_type!(deserialize_any, "any"); + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + let decoded = FULL_QUOTER + .with(|q| q.requote_str_lossy(self.value)) + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed(self.value)); + + let s = decoded.as_ref(); + // We have to do it manually here on behalf of serde. + if let Ok(v) = s.parse::() { + if let Ok(v) = u32::try_from(v) { + return visitor.visit_u32(v); + } + + return visitor.visit_u64(v); + } + + if let Ok(v) = s.parse::() { + if let Ok(v) = i32::try_from(v) { + return visitor.visit_i32(v); + } + + return visitor.visit_i64(v); + } + + match decoded { + Cow::Borrowed(value) => visitor.visit_borrowed_str(value), + Cow::Owned(value) => visitor.visit_string(value), + } + } + unsupported_type!(deserialize_seq, "seq"); unsupported_type!(deserialize_map, "map"); unsupported_type!(deserialize_identifier, "identifier"); @@ -704,6 +739,119 @@ mod tests { assert_eq!(vals.value, "/"); } + #[test] + fn deserialize_path_decode_any() { + #[derive(Debug, PartialEq)] + pub enum AnyEnumCustom { + String(String), + Int(u32), + Other, + } + + impl<'de> Deserialize<'de> for AnyEnumCustom { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Vis; + impl<'de> Visitor<'de> for Vis { + type Value = AnyEnumCustom; + + fn expecting<'a>(&self, f: &mut std::fmt::Formatter<'a>) -> std::fmt::Result { + write!(f, "my thing") + } + + fn visit_u32(self, v: u32) -> Result + where + E: serde::de::Error, + { + Ok(AnyEnumCustom::Int(v)) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + match u32::try_from(v) { + Ok(v) => Ok(AnyEnumCustom::Int(v)), + Err(_) => Ok(AnyEnumCustom::String(format!("some str: {v}"))), + } + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + match u32::try_from(v) { + Ok(v) => Ok(AnyEnumCustom::Int(v)), + Err(_) => Ok(AnyEnumCustom::String(format!("some str: {v}"))), + } + } + + fn visit_str(self, v: &str) -> Result { + v.parse().map(AnyEnumCustom::Int).or_else(|_| { + Ok(match v { + "other" => AnyEnumCustom::Other, + _ => AnyEnumCustom::String(format!("some str: {v}")), + }) + }) + } + } + + deserializer.deserialize_any(Vis) + } + } + + #[derive(Debug, Deserialize, PartialEq)] + #[serde(untagged)] + pub enum AnyEnumDerive { + String(String), + Int(u32), + Other, + } + + // single + let rdef = ResourceDef::new("/{key}"); + + let mut path = Path::new("/%25"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let segment: AnyEnumCustom = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(segment, AnyEnumCustom::String("some str: %".to_string())); + + let mut path = Path::new("/%25"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let segment: AnyEnumDerive = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(segment, AnyEnumDerive::String("%".to_string())); + + // seq + let rdef = ResourceDef::new("/{key}/{value}"); + + let mut path = Path::new("/other/123"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let segment: (AnyEnumCustom, AnyEnumDerive) = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(segment.0, AnyEnumCustom::Other); + assert_eq!(segment.1, AnyEnumDerive::Int(123)); + + // map + #[derive(Deserialize)] + struct Vals { + key: AnyEnumCustom, + value: AnyEnumDerive, + } + + let rdef = ResourceDef::new("/{key}/{value}"); + + let mut path = Path::new("/123/%2F"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let vals: Vals = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(vals.key, AnyEnumCustom::Int(123)); + assert_eq!(vals.value, AnyEnumDerive::String("/".to_string())); + } + #[test] fn deserialize_borrowed() { #[derive(Debug, Deserialize)]