Skip to main content

bpi_rs/
response.rs

1use crate::err::error::BpiError;
2use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
3
4/// bpi-rs 操作的 crate 级结果类型。
5pub type BpiResult<T> = Result<T, BpiError>;
6
7/// 大多数 Web API endpoint 使用的标准 Bilibili JSON envelope。
8#[derive(Debug, Serialize, Clone)]
9pub struct ApiEnvelope<T> {
10    /// API 返回码。`0` 表示成功。
11    pub code: i32,
12
13    /// 成功 endpoint 返回的 payload。
14    pub data: Option<T>,
15
16    /// API 消息。Bilibili 成功时常返回 `"0"`。
17    pub message: String,
18
19    /// 部分 endpoint 返回的可选状态标记。
20    pub status: bool,
21}
22
23impl<'de, T> Deserialize<'de> for ApiEnvelope<T>
24where
25    T: Deserialize<'de>,
26{
27    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
28    where
29        D: Deserializer<'de>,
30    {
31        let raw = RawEnvelope::<T>::deserialize(deserializer)?;
32        Ok(Self {
33            code: raw.code.or(raw.errno).unwrap_or_default(),
34            data: raw.data,
35            message: raw.message.or(raw.msg).or(raw.show_msg).unwrap_or_default(),
36            status: raw.status,
37        })
38    }
39}
40
41impl<T> ApiEnvelope<T> {
42    /// 从字节解析 JSON envelope。
43    pub fn from_slice(bytes: &[u8]) -> BpiResult<Self>
44    where
45        T: DeserializeOwned,
46    {
47        serde_json::from_slice(bytes).map_err(BpiError::from)
48    }
49
50    /// 如果此 envelope 表示成功的 API 响应,则返回它。
51    pub fn ensure_success(self) -> BpiResult<Self> {
52        if self.code == 0 {
53            return Ok(self);
54        }
55
56        if self.message.is_empty() || self.message == "0" {
57            Err(BpiError::from_code(self.code))
58        } else {
59            Err(BpiError::from_code_message(self.code, self.message))
60        }
61    }
62
63    /// 从成功响应中提取必需 payload。
64    pub fn into_payload(self) -> BpiResult<T> {
65        self.ensure_success()?.data.ok_or(BpiError::MissingData)
66    }
67
68    /// 从成功响应中提取可选 payload。
69    pub fn into_optional_payload(self) -> BpiResult<Option<T>> {
70        Ok(self.ensure_success()?.data)
71    }
72
73    /// 不检查响应码,直接提取必需 payload。
74    ///
75    /// 这是为 payload 包含业务状态的 endpoint 保留的,例如 QR 轮询。
76    pub fn into_data(self) -> Result<T, BpiError> {
77        self.data.ok_or(BpiError::missing_data())
78    }
79}
80
81/// 仍暴露完整 Bilibili envelope 的模块使用的兼容别名。
82pub type BpiResponse<T> = ApiEnvelope<T>;
83
84#[derive(Debug, Deserialize)]
85#[serde(bound(deserialize = "T: Deserialize<'de>"))]
86struct RawEnvelope<T> {
87    #[serde(default)]
88    code: Option<i32>,
89    #[serde(default)]
90    errno: Option<i32>,
91    #[serde(default, alias = "result")]
92    data: Option<T>,
93    #[serde(default)]
94    message: Option<String>,
95    #[serde(default)]
96    msg: Option<String>,
97    #[serde(default, rename = "showMsg")]
98    show_msg: Option<String>,
99    #[serde(default)]
100    status: bool,
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[derive(Debug, Deserialize, PartialEq, Eq)]
108    struct FixturePayload {
109        title: String,
110        aid: u64,
111    }
112
113    fn fixture(name: &str) -> &'static [u8] {
114        match name {
115            "success" => include_bytes!("../tests/fixtures/envelope/success.json"),
116            "result-alias" => include_bytes!("../tests/fixtures/envelope/result-alias.json"),
117            "api-error" => include_bytes!("../tests/fixtures/envelope/api-error.json"),
118            "missing-data" => include_bytes!("../tests/fixtures/envelope/missing-data.json"),
119            "no-payload" => include_bytes!("../tests/fixtures/envelope/no-payload.json"),
120            _ => unreachable!("unknown fixture"),
121        }
122    }
123
124    #[test]
125    fn api_envelope_extracts_data_payload() -> Result<(), BpiError> {
126        let payload =
127            ApiEnvelope::<FixturePayload>::from_slice(fixture("success"))?.into_payload()?;
128
129        assert_eq!(payload.title, "fixture video");
130        Ok(())
131    }
132
133    #[test]
134    fn api_envelope_extracts_result_alias_payload() -> Result<(), BpiError> {
135        let payload =
136            ApiEnvelope::<FixturePayload>::from_slice(fixture("result-alias"))?.into_payload()?;
137
138        assert_eq!(payload.aid, 170002);
139        Ok(())
140    }
141
142    #[test]
143    fn api_envelope_returns_missing_data_for_required_payload() {
144        let err = ApiEnvelope::<FixturePayload>::from_slice(fixture("missing-data"))
145            .and_then(ApiEnvelope::into_payload)
146            .unwrap_err();
147
148        assert!(matches!(err, BpiError::MissingData));
149    }
150
151    #[test]
152    fn api_envelope_allows_optional_payload() -> Result<(), BpiError> {
153        let payload = ApiEnvelope::<FixturePayload>::from_slice(fixture("no-payload"))?
154            .into_optional_payload()?;
155
156        assert!(payload.is_none());
157        Ok(())
158    }
159
160    #[test]
161    fn api_envelope_converts_api_error() {
162        let err = ApiEnvelope::<FixturePayload>::from_slice(fixture("api-error"))
163            .and_then(ApiEnvelope::ensure_success)
164            .unwrap_err();
165
166        assert!(matches!(err, BpiError::Api { code: -101, .. }));
167        assert_eq!(err.code(), Some(-101));
168    }
169
170    #[test]
171    fn api_envelope_treats_null_message_as_empty() -> Result<(), BpiError> {
172        let payload = ApiEnvelope::<LoginCoinFixture>::from_slice(
173            br#"{ "code": 0, "message": null, "data": { "money": 0.0 } }"#,
174        )?
175        .into_payload()?;
176
177        assert_eq!(payload.money, 0.0);
178        Ok(())
179    }
180
181    #[test]
182    fn api_envelope_maps_errno_login_error() {
183        let err = ApiEnvelope::<serde_json::Value>::from_slice(
184            br#"{ "errno": 800501007, "msg": "user not login", "showMsg": "user not login" }"#,
185        )
186        .and_then(ApiEnvelope::ensure_success)
187        .unwrap_err();
188
189        assert!(err.requires_login());
190        assert_eq!(err.code(), Some(800501007));
191    }
192
193    #[test]
194    fn api_envelope_returns_decode_error_for_invalid_json() {
195        let err = ApiEnvelope::<FixturePayload>::from_slice(b"{not json").unwrap_err();
196
197        assert!(matches!(err, BpiError::Decode { .. }));
198    }
199
200    #[derive(Debug, Deserialize)]
201    struct LoginCoinFixture {
202        money: f64,
203    }
204}