aioduct 0.1.10

Async-native HTTP client built directly on hyper 1.x — no hyper-util, no legacy
Documentation
use std::collections::HashMap;

/// RFC 9457 Problem Details for HTTP APIs.
///
/// Represents the standard `application/problem+json` error format.
#[derive(Debug, Clone, serde::Deserialize)]
pub struct ProblemDetails {
    /// A URI reference that identifies the problem type.
    #[serde(rename = "type", default)]
    pub problem_type: Option<String>,
    /// A short, human-readable summary of the problem.
    #[serde(default)]
    pub title: Option<String>,
    /// The HTTP status code generated by the origin server.
    #[serde(default)]
    pub status: Option<u16>,
    /// A human-readable explanation specific to this occurrence.
    #[serde(default)]
    pub detail: Option<String>,
    /// A URI reference that identifies this specific occurrence.
    #[serde(default)]
    pub instance: Option<String>,
    /// Extension members not covered by the standard fields.
    #[serde(flatten)]
    pub extensions: HashMap<String, serde_json::Value>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deserialize_full() {
        let json = r#"{
            "type": "https://example.com/probs/out-of-credit",
            "title": "You do not have enough credit.",
            "status": 403,
            "detail": "Your current balance is 30, but that costs 50.",
            "instance": "/account/12345/msgs/abc"
        }"#;
        let pd: ProblemDetails = serde_json::from_str(json).unwrap();
        assert_eq!(
            pd.problem_type.as_deref(),
            Some("https://example.com/probs/out-of-credit")
        );
        assert_eq!(pd.title.as_deref(), Some("You do not have enough credit."));
        assert_eq!(pd.status, Some(403));
        assert_eq!(
            pd.detail.as_deref(),
            Some("Your current balance is 30, but that costs 50.")
        );
        assert_eq!(pd.instance.as_deref(), Some("/account/12345/msgs/abc"));
        assert!(pd.extensions.is_empty());
    }

    #[test]
    fn deserialize_minimal() {
        let json = r#"{"title": "Not Found"}"#;
        let pd: ProblemDetails = serde_json::from_str(json).unwrap();
        assert_eq!(pd.title.as_deref(), Some("Not Found"));
        assert!(pd.problem_type.is_none());
        assert!(pd.status.is_none());
        assert!(pd.detail.is_none());
        assert!(pd.instance.is_none());
    }

    #[test]
    fn deserialize_with_extensions() {
        let json = r#"{
            "type": "https://example.com/probs/rate-limit",
            "title": "Rate limit exceeded",
            "status": 429,
            "retry_after": 60,
            "limit": 100
        }"#;
        let pd: ProblemDetails = serde_json::from_str(json).unwrap();
        assert_eq!(pd.status, Some(429));
        assert_eq!(
            pd.extensions.get("retry_after").and_then(|v| v.as_u64()),
            Some(60)
        );
        assert_eq!(
            pd.extensions.get("limit").and_then(|v| v.as_u64()),
            Some(100)
        );
    }

    #[test]
    fn deserialize_empty() {
        let json = "{}";
        let pd: ProblemDetails = serde_json::from_str(json).unwrap();
        assert!(pd.problem_type.is_none());
        assert!(pd.title.is_none());
    }
}