unistore-http 0.1.0

HTTP client capability for UniStore
Documentation
//! HTTP 响应包装
//!
//! 职责:
//! - 封装 HTTP 响应数据
//! - 提供便捷的响应体解析方法
//! - 支持流式读取

use super::error::HttpError;
use serde::de::DeserializeOwned;

/// HTTP 响应
#[derive(Debug)]
pub struct Response {
    /// HTTP 状态码
    status: u16,

    /// 响应头
    headers: Vec<(String, String)>,

    /// 响应体(字节)
    body: Vec<u8>,

    /// 请求 URL
    url: String,
}

impl Response {
    /// 从原始数据创建响应
    ///
    /// 注:此方法主要用于测试和内部构造
    #[cfg_attr(not(test), allow(dead_code))]
    pub(crate) fn new(status: u16, headers: Vec<(String, String)>, body: Vec<u8>, url: String) -> Self {
        Self {
            status,
            headers,
            body,
            url,
        }
    }

    /// 获取 HTTP 状态码
    pub fn status(&self) -> u16 {
        self.status
    }

    /// 检查响应是否成功 (2xx)
    pub fn is_success(&self) -> bool {
        (200..300).contains(&self.status)
    }

    /// 检查是否为信息响应 (1xx)
    pub fn is_informational(&self) -> bool {
        (100..200).contains(&self.status)
    }

    /// 检查是否为重定向 (3xx)
    pub fn is_redirection(&self) -> bool {
        (300..400).contains(&self.status)
    }

    /// 检查是否为客户端错误 (4xx)
    pub fn is_client_error(&self) -> bool {
        (400..500).contains(&self.status)
    }

    /// 检查是否为服务器错误 (5xx)
    pub fn is_server_error(&self) -> bool {
        (500..600).contains(&self.status)
    }

    /// 获取请求 URL
    pub fn url(&self) -> &str {
        &self.url
    }

    /// 获取响应头
    pub fn headers(&self) -> &[(String, String)] {
        &self.headers
    }

    /// 获取指定响应头的值
    pub fn header(&self, name: &str) -> Option<&str> {
        let name_lower = name.to_lowercase();
        self.headers
            .iter()
            .find(|(k, _)| k.to_lowercase() == name_lower)
            .map(|(_, v)| v.as_str())
    }

    /// 获取 Content-Type
    pub fn content_type(&self) -> Option<&str> {
        self.header("content-type")
    }

    /// 获取 Content-Length
    pub fn content_length(&self) -> Option<usize> {
        self.header("content-length")
            .and_then(|v| v.parse().ok())
    }

    /// 获取响应体字节
    pub fn bytes(&self) -> &[u8] {
        &self.body
    }

    /// 消费响应,获取响应体字节
    pub fn into_bytes(self) -> Vec<u8> {
        self.body
    }

    /// 将响应体解析为文本
    pub fn text(&self) -> Result<String, HttpError> {
        String::from_utf8(self.body.clone())
            .map_err(|e| HttpError::ResponseBody(format!("UTF-8 解码失败: {}", e)))
    }

    /// 消费响应,将响应体解析为文本
    pub fn into_text(self) -> Result<String, HttpError> {
        String::from_utf8(self.body)
            .map_err(|e| HttpError::ResponseBody(format!("UTF-8 解码失败: {}", e)))
    }

    /// 将响应体解析为 JSON
    pub fn json<T: DeserializeOwned>(&self) -> Result<T, HttpError> {
        serde_json::from_slice(&self.body)
            .map_err(|e| HttpError::JsonDeserialize(e.to_string()))
    }

    /// 消费响应,将响应体解析为 JSON
    pub fn into_json<T: DeserializeOwned>(self) -> Result<T, HttpError> {
        serde_json::from_slice(&self.body)
            .map_err(|e| HttpError::JsonDeserialize(e.to_string()))
    }

    /// 确保响应成功,否则返回错误
    pub fn error_for_status(self) -> Result<Self, HttpError> {
        if self.is_success() {
            Ok(self)
        } else if self.is_client_error() {
            Err(HttpError::ClientError(
                self.status,
                self.text().unwrap_or_else(|_| "Unknown error".into()),
            ))
        } else if self.is_server_error() {
            Err(HttpError::ServerError(self.status))
        } else {
            Ok(self)
        }
    }

    /// 从 reqwest Response 创建
    pub(crate) async fn from_reqwest(resp: reqwest::Response) -> Result<Self, HttpError> {
        let status = resp.status().as_u16();
        let url = resp.url().to_string();

        let headers: Vec<(String, String)> = resp
            .headers()
            .iter()
            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
            .collect();

        let body = resp.bytes().await?.to_vec();

        Ok(Self {
            status,
            headers,
            body,
            url,
        })
    }
}

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

    fn mock_response(status: u16, body: &str) -> Response {
        Response::new(
            status,
            vec![
                ("content-type".into(), "application/json".into()),
                ("content-length".into(), body.len().to_string()),
            ],
            body.as_bytes().to_vec(),
            "http://example.com".into(),
        )
    }

    #[test]
    fn test_status_checks() {
        assert!(mock_response(200, "").is_success());
        assert!(mock_response(201, "").is_success());
        assert!(mock_response(204, "").is_success());
        assert!(!mock_response(400, "").is_success());
        assert!(!mock_response(500, "").is_success());

        assert!(mock_response(100, "").is_informational());
        assert!(mock_response(301, "").is_redirection());
        assert!(mock_response(404, "").is_client_error());
        assert!(mock_response(503, "").is_server_error());
    }

    #[test]
    fn test_headers() {
        let resp = mock_response(200, "test");
        assert_eq!(resp.content_type(), Some("application/json"));
        assert_eq!(resp.content_length(), Some(4));
        assert_eq!(resp.header("Content-Type"), Some("application/json")); // case-insensitive
    }

    #[test]
    fn test_body_parsing() {
        let resp = mock_response(200, "hello");
        assert_eq!(resp.text().unwrap(), "hello");
        assert_eq!(resp.bytes(), b"hello");
    }

    #[test]
    fn test_json_parsing() {
        let resp = mock_response(200, r#"{"name":"test"}"#);
        let data: serde_json::Value = resp.json().unwrap();
        assert_eq!(data["name"], "test");
    }

    #[test]
    fn test_error_for_status() {
        let ok = mock_response(200, "ok");
        assert!(ok.error_for_status().is_ok());

        let client_err = mock_response(404, "Not Found");
        assert!(client_err.error_for_status().is_err());

        let server_err = mock_response(500, "Internal Server Error");
        assert!(server_err.error_for_status().is_err());
    }
}