Skip to content

Commit

Permalink
feat(extract::Path): add ErrorKind::DeserializeError to specialize Er…
Browse files Browse the repository at this point in the history
…rorKind::Message

This commit introduces another `extract::path::ErrorKind` variant that captures the
serde error nominally captured through the `serde::de::Error` trait impl on `PathDeserializeError`.
We augment the deserialization error with the captured (key, value), allowing `extract::Path`, and wrapping
extractors, to gain programmatic access to the key name, and attempted deserialized value.

The `PathDeserializationError::custom` is used two places in addition to capture the deserialization error.
These usages should still be unaffected.
  • Loading branch information
vegardgs-ksat committed Apr 25, 2024
1 parent e3bb708 commit b0f51d8
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 2 deletions.
52 changes: 50 additions & 2 deletions axum/src/extract/path/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,21 @@ impl<'de> Deserializer<'de> for PathDeserializer<'de> {
.got(self.url_params.len())
.expected(1));
}
visitor.visit_borrowed_str(&self.url_params[0].1)
let key = &self.url_params[0].0;
let value = &self.url_params[0].1;
visitor
.visit_borrowed_str(value)
.map_err(|e: PathDeserializationError| {
if let ErrorKind::Message(message) = &e.kind {
PathDeserializationError::new(ErrorKind::DeserializeError {
key: key.to_string(),
value: value.as_str().to_owned(),
message: message.to_owned(),
})
} else {
e
}
})
}

fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
Expand Down Expand Up @@ -362,7 +376,19 @@ impl<'de> Deserializer<'de> for ValueDeserializer<'de> {
where
V: Visitor<'de>,
{
visitor.visit_borrowed_str(self.value)
visitor
.visit_borrowed_str(self.value)
.map_err(|e: PathDeserializationError| {
if let (ErrorKind::Message(message), Some(key)) = (&e.kind, self.key.as_ref()) {
PathDeserializationError::new(ErrorKind::DeserializeError {
key: key.key().to_owned(),
value: self.value.as_str().to_owned(),
message: message.to_owned(),
})
} else {
e
}
})
}

fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
Expand Down Expand Up @@ -608,6 +634,15 @@ enum KeyOrIdx<'de> {
Idx { idx: usize, key: &'de str },
}

impl<'de> KeyOrIdx<'de> {
fn key(&self) -> &'de str {
match &self {
Self::Key(key) => key,
Self::Idx { key, .. } => key,
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -928,4 +963,17 @@ mod tests {
}
);
}

#[test]
fn test_deserialize_key_value() {
test_parse_error!(
vec![("id", "123123-123-123123")],
uuid::Uuid,
ErrorKind::DeserializeError {
key: "id".to_owned(),
value: "123123-123-123123".to_owned(),
message: "UUID parsing failed: invalid group count: expected 5, found 3".to_owned(),
}
);
}
}
56 changes: 56 additions & 0 deletions axum/src/extract/path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,16 @@ pub enum ErrorKind {
name: &'static str,
},

/// Failed to deserialize the value with a custom deserialization error.
DeserializeError {
/// The key at which the invalid value was located.
key: String,
/// The value that failed to deserialize.
value: String,
/// The deserializaation failure message.
message: String,
},

/// Catch-all variant for errors that don't fit any other variant.
Message(String),
}
Expand Down Expand Up @@ -347,6 +357,11 @@ impl fmt::Display for ErrorKind {
f,
"Cannot parse value at index {index} with value `{value:?}` to a `{expected_type}`"
),
ErrorKind::DeserializeError {
key,
value,
message,
} => write!(f, "Cannot parse `{key}` with value `{value:?}`: {message}"),
}
}
}
Expand All @@ -371,6 +386,7 @@ impl FailedToDeserializePathParams {
pub fn body_text(&self) -> String {
match self.0.kind {
ErrorKind::Message(_)
| ErrorKind::DeserializeError { .. }
| ErrorKind::InvalidUtf8InPathParam { .. }
| ErrorKind::ParseError { .. }
| ErrorKind::ParseErrorAtIndex { .. }
Expand All @@ -385,6 +401,7 @@ impl FailedToDeserializePathParams {
pub fn status(&self) -> StatusCode {
match self.0.kind {
ErrorKind::Message(_)
| ErrorKind::DeserializeError { .. }
| ErrorKind::InvalidUtf8InPathParam { .. }
| ErrorKind::ParseError { .. }
| ErrorKind::ParseErrorAtIndex { .. }
Expand Down Expand Up @@ -887,4 +904,43 @@ mod tests {
let body = res.text().await;
assert_eq!(body, "a=foo b=bar c=baz");
}

#[crate::test]
async fn deserialize_error_single_value() {
let app = Router::new().route(
"/resources/:res",
get(|res: Path<uuid::Uuid>| async move {
let _res = res;
}),
);

let client = TestClient::new(app);
let res = client.get("/resources/123123-123-123123").await;
let body = res.text().await;
assert_eq!(
body,
r#"Invalid URL: Cannot parse `res` with value `"123123-123-123123"`: UUID parsing failed: invalid group count: expected 5, found 3"#
);
}

#[crate::test]
async fn deserialize_error_multi_value() {
let app = Router::new().route(
"/resources/:res/sub/:sub",
get(
|Path((res, sub)): Path<(uuid::Uuid, uuid::Uuid)>| async move {
let _res = res;
let _sub = sub;
},
),
);

let client = TestClient::new(app);
let res = client.get("/resources/456456-123-456456/sub/123").await;
let body = res.text().await;
assert_eq!(
body,
r#"Invalid URL: Cannot parse `res` with value `"456456-123-456456"`: UUID parsing failed: invalid group count: expected 5, found 3"#
);
}
}

0 comments on commit b0f51d8

Please sign in to comment.