snippe 0.1.0

Async Rust client for the Snippe payments API (Tanzania) — collections, hosted checkout sessions, disbursements, and verified webhooks.
Documentation
//! Internal helpers for parsing the Snippe response envelope.

use serde::de::DeserializeOwned;

use crate::error::{ApiError, Error, ErrorCode};

#[derive(Debug, serde::Deserialize)]
pub(crate) struct SuccessEnvelope<T> {
    #[serde(default)]
    #[allow(dead_code)]
    pub(crate) status: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    pub(crate) code: Option<u16>,
    pub(crate) data: T,
}

#[derive(Debug, Default, serde::Deserialize)]
pub(crate) struct ErrorEnvelope {
    #[serde(default)]
    pub(crate) error_code: Option<String>,
    #[serde(default)]
    pub(crate) message: Option<String>,
}

pub(crate) async fn parse_response<T>(response: reqwest::Response) -> crate::Result<T>
where
    T: DeserializeOwned,
{
    let status = response.status();
    let retry_after = response
        .headers()
        .get("X-Ratelimit-Reset")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u64>().ok());

    let bytes = response.bytes().await.map_err(Error::from)?;

    if status.is_success() {
        tracing::debug!(?status, body_bytes = bytes.len(), "snippe response");
        let envelope: SuccessEnvelope<T> =
            serde_json::from_slice(&bytes).map_err(Error::Decode)?;
        Ok(envelope.data)
    } else {
        let envelope: ErrorEnvelope =
            serde_json::from_slice(&bytes).unwrap_or_default();
        let raw_code = envelope.error_code.unwrap_or_default();
        let error_code = ErrorCode::from_str(&raw_code);
        let message = envelope.message.unwrap_or_else(|| {
            // Fall back to a snippet of the raw body if the envelope didn't decode.
            String::from_utf8_lossy(&bytes).chars().take(500).collect()
        });
        tracing::warn!(?status, error_code = error_code.as_str(), %message, "snippe error response");
        Err(Error::Api(ApiError {
            status: status.as_u16(),
            error_code,
            message,
            retry_after,
        }))
    }
}