bpi-rs 0.2.0

Bilibili API client library for Rust
Documentation
use serde::Serialize;
use thiserror::Error;

/// 错误类型分类
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum ErrorCategory {
    /// 权限认证类错误
    Auth,
    /// 请求参数类错误
    Request,
    /// 服务器类错误
    Server,
    /// 业务逻辑类错误
    Business,
    /// 网络类错误
    Network,
    /// 未知错误
    Unknown,
}

#[derive(Debug, Error, Serialize)]
pub enum BpiError {
    /// 网络请求失败
    #[error("网络请求失败: {message}")]
    Network { message: String },

    /// Transport-level request failure.
    #[error("transport request failed: {source}")]
    Transport {
        #[serde(skip)]
        source: reqwest::Error,
    },

    /// HTTP状态码错误
    #[error("HTTP请求失败,状态码: {status}")]
    Http { status: u16 },

    /// HTTP status error.
    #[error("HTTP request failed with status {status}")]
    HttpStatus { status: u16 },

    /// JSON解析失败
    #[error("数据解析失败: {message}")]
    Parse { message: String },

    /// Response decode failure.
    #[error("failed to decode response: {source}")]
    Decode {
        #[serde(skip)]
        source: serde_json::Error,
    },

    /// API返回的业务错误
    #[error("API错误 [{code}]: {message}")]
    Api {
        code: i32,
        message: String,
        category: ErrorCategory,
    },

    /// 验证错误
    #[error("验证失败: {message}")]
    Authentication { message: String },

    /// Authentication or authorization error.
    #[error("authentication failed: {message}")]
    Auth { message: String },

    /// # 参数错误
    #[error("参数错误 [{field}]: {message}")]
    InvalidParameter {
        field: &'static str,
        message: &'static str,
    },

    /// API response succeeded but did not include required payload data.
    #[error("missing response data")]
    MissingData,

    /// Response format is not supported by the current parser.
    #[error("unsupported response: {message}")]
    UnsupportedResponse { message: String },
}

impl BpiError {
    pub fn missing_csrf() -> Self {
        BpiError::InvalidParameter {
            field: "csrf",
            message: "缺少CSRF",
        }
    }

    pub fn missing_data() -> Self {
        BpiError::MissingData
    }

    pub fn auth_required() -> Self {
        BpiError::Auth {
            message: "需要登录".to_string(),
        }
    }
}

/// 生成Error的From实现
impl BpiError {
    /// 根据API错误码创建BpiError
    pub fn from_code(code: i32) -> Self {
        let message = super::code::get_error_message(code);
        let category = super::code::categorize_error(code);

        BpiError::Api {
            code,
            message,
            category,
        }
    }

    // 不在错误码表中的API错误
    pub fn from_code_message(code: i32, message: String) -> Self {
        let category = super::code::categorize_error(code);
        BpiError::Api {
            code,
            message,
            category,
        }
    }

    /// 从API响应创建BpiError
    pub fn from_api_response<T>(resp: crate::response::ApiEnvelope<T>) -> Self {
        if resp.code == 0 {
            return BpiError::Api {
                code: 0,
                message: "API返回成功状态但被当作错误处理".to_string(),
                category: ErrorCategory::Unknown,
            };
        }

        if resp.message.is_empty() || resp.message == "0" {
            Self::from_code(resp.code)
        } else {
            Self::from_code_message(resp.code, resp.message)
        }
    }
}

/// 获取错误属性
impl BpiError {
    /// 获取错误码
    pub fn code(&self) -> Option<i32> {
        match self {
            BpiError::Api { code, .. } => Some(*code),
            _ => None,
        }
    }

    /// 获取 HTTP 状态码
    pub fn http_status(&self) -> Option<u16> {
        match self {
            BpiError::Http { status } | BpiError::HttpStatus { status } => Some(*status),
            _ => None,
        }
    }

    /// 获取错误分类
    pub fn category(&self) -> ErrorCategory {
        match self {
            BpiError::Api { category, .. } => category.clone(),
            BpiError::Network { .. } => ErrorCategory::Network,
            BpiError::Transport { .. } => ErrorCategory::Network,
            BpiError::Http { .. } => ErrorCategory::Network,
            BpiError::HttpStatus { .. } => ErrorCategory::Network,
            BpiError::Parse { .. } => ErrorCategory::Request,
            BpiError::Decode { .. } => ErrorCategory::Request,
            BpiError::InvalidParameter { .. } => ErrorCategory::Request,
            BpiError::Authentication { .. } => ErrorCategory::Auth,
            BpiError::Auth { .. } => ErrorCategory::Auth,
            BpiError::MissingData => ErrorCategory::Request,
            BpiError::UnsupportedResponse { .. } => ErrorCategory::Request,
        }
    }
}

/// 错误创建函数
impl BpiError {
    /// 创建网络错误
    pub fn network(message: impl Into<String>) -> Self {
        BpiError::Network {
            message: message.into(),
        }
    }

    /// 创建HTTP错误
    pub fn http(status: u16) -> Self {
        BpiError::HttpStatus { status }
    }

    /// 创建解析错误
    pub fn parse(message: impl Into<String>) -> Self {
        BpiError::Parse {
            message: message.into(),
        }
    }

    /// 创建参数错误
    pub fn invalid_parameter(field: &'static str, message: &'static str) -> Self {
        BpiError::InvalidParameter { field, message }
    }

    pub fn auth(message: impl Into<String>) -> Self {
        BpiError::Auth {
            message: message.into(),
        }
    }

    /// Creates an unsupported response error.
    pub fn unsupported_response(message: impl Into<String>) -> Self {
        BpiError::UnsupportedResponse {
            message: message.into(),
        }
    }
}

/// 错误判断
impl BpiError {
    /// 判断是否需要用户登录
    pub fn requires_login(&self) -> bool {
        matches!(self.code(), Some(-101) | Some(-401) | Some(800501007))
            || matches!(self.http_status(), Some(401))
    }

    /// 判断是否为权限问题
    pub fn is_permission_error(&self) -> bool {
        matches!(self.category(), ErrorCategory::Auth)
            || matches!(self.code(), Some(-403) | Some(-4))
            || matches!(self.http_status(), Some(403))
    }

    /// 判断是否需要VIP权限
    pub fn requires_vip(&self) -> bool {
        matches!(self.code(), Some(-106) | Some(-650))
    }

    /// 判断是否为风控拦截
    pub fn is_risk_control(&self) -> bool {
        matches!(self.code(), Some(-352) | Some(-412)) || matches!(self.http_status(), Some(412))
    }

    /// 判断是否为业务逻辑错误
    pub fn is_business_error(&self) -> bool {
        matches!(self.category(), ErrorCategory::Business)
    }

    /// 获取可写入合同的稳定语义错误标签
    pub fn semantic_error(&self) -> Option<&'static str> {
        if self.requires_login() {
            Some("requires_login")
        } else if self.requires_vip() {
            Some("requires_vip")
        } else if self.is_risk_control() {
            Some("risk_control")
        } else if self.is_permission_error() {
            Some("permission_denied")
        } else if self.is_business_error() {
            Some("business_error")
        } else {
            None
        }
    }
}

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

    #[test]
    fn http_status_returns_status_for_legacy_and_current_http_variants() {
        assert_eq!(BpiError::Http { status: 412 }.http_status(), Some(412));
        assert_eq!(BpiError::http(403).http_status(), Some(403));
    }

    #[test]
    fn requires_login_recognizes_api_and_http_unauthorized_errors() {
        assert!(BpiError::from_code(-101).requires_login());
        assert!(BpiError::from_code(800501007).requires_login());
        assert!(BpiError::http(401).requires_login());
    }

    #[test]
    fn is_permission_error_recognizes_api_and_http_forbidden_errors() {
        assert!(BpiError::from_code(-403).is_permission_error());
        assert!(BpiError::http(403).is_permission_error());
    }

    #[test]
    fn is_risk_control_recognizes_api_and_http_risk_blocks() {
        assert!(BpiError::from_code(-352).is_risk_control());
        assert!(BpiError::from_code(-412).is_risk_control());
        assert!(BpiError::http(412).is_risk_control());
    }

    #[test]
    fn semantic_error_returns_stable_contract_labels() {
        assert_eq!(
            BpiError::from_code(-101).semantic_error(),
            Some("requires_login")
        );
        assert_eq!(
            BpiError::from_code(-106).semantic_error(),
            Some("requires_vip")
        );
        assert_eq!(
            BpiError::from_code(-352).semantic_error(),
            Some("risk_control")
        );
        assert_eq!(
            BpiError::from_code(-403).semantic_error(),
            Some("permission_denied")
        );
    }
}