1use crate::err::error::BpiError;
2use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
3
4pub type BpiResult<T> = Result<T, BpiError>;
6
7#[derive(Debug, Serialize, Clone)]
9pub struct ApiEnvelope<T> {
10 pub code: i32,
12
13 pub data: Option<T>,
15
16 pub message: String,
18
19 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 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 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 pub fn into_payload(self) -> BpiResult<T> {
65 self.ensure_success()?.data.ok_or(BpiError::MissingData)
66 }
67
68 pub fn into_optional_payload(self) -> BpiResult<Option<T>> {
70 Ok(self.ensure_success()?.data)
71 }
72
73 pub fn into_data(self) -> Result<T, BpiError> {
77 self.data.ok_or(BpiError::missing_data())
78 }
79}
80
81pub 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}