From 9c453c07860233335d9a34bcf73119e8fff492e4 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 10 Feb 2026 18:33:49 +0900 Subject: [PATCH] parse numeric values as well --- actix-router/CHANGES.md | 3 ++ actix-router/src/de.rs | 63 ++++++++++++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 11 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 a651f35ad..f06911b34 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -212,7 +212,7 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T> unsupported_type!(deserialize_identifier, "identifier"); unsupported_type!(deserialize_ignored_any, "ignored_any"); - parse_single_value!(deserialize_any, deserialize_str); + parse_single_value!(deserialize_any); parse_single_value!(deserialize_bool); parse_single_value!(deserialize_i8); parse_single_value!(deserialize_i16); @@ -430,11 +430,37 @@ impl<'de> Deserializer<'de> for Value<'de> { Err(de::value::Error::custom("unsupported type: tuple struct")) } - fn deserialize_any(self, v: V) -> Result + fn deserialize_any(self, visitor: V) -> Result where V: Visitor<'de>, { - self.deserialize_str(v) + 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"); @@ -731,10 +757,7 @@ mod tests { impl<'de> Visitor<'de> for Vis { type Value = AnyEnumCustom; - fn expecting<'a>( - &self, - f: &mut std::fmt::Formatter<'a>, - ) -> std::fmt::Result { + fn expecting<'a>(&self, f: &mut std::fmt::Formatter<'a>) -> std::fmt::Result { write!(f, "my thing") } @@ -745,6 +768,26 @@ mod tests { 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 { @@ -788,11 +831,9 @@ mod tests { 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(); + let segment: (AnyEnumCustom, AnyEnumDerive) = serde::Deserialize::deserialize(de).unwrap(); assert_eq!(segment.0, AnyEnumCustom::Other); - // Deserializes to `String` instead of `Int` because deserializing defaults to `str` and serde doesn't automatically implement parsing numbers from `&str`s - assert_eq!(segment.1, AnyEnumDerive::String("123".to_string())); + assert_eq!(segment.1, AnyEnumDerive::Int(123)); // map #[derive(Deserialize)]