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> {
78 self.data.ok_or(BpiError::missing_data())
79 }
80}
81
82pub type BpiResponse<T> = ApiEnvelope<T>;
84
85#[derive(Debug, Deserialize)]
86#[serde(bound(deserialize = "T: Deserialize<'de>"))]
87struct RawEnvelope<T> {
88 #[serde(default)]
89 code: Option<i32>,
90 #[serde(default)]
91 errno: Option<i32>,
92 #[serde(default, alias = "result")]
93 data: Option<T>,
94 #[serde(default)]
95 message: Option<String>,
96 #[serde(default)]
97 msg: Option<String>,
98 #[serde(default, rename = "showMsg")]
99 show_msg: Option<String>,
100 #[serde(default)]
101 status: bool,
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[derive(Debug, Deserialize, PartialEq, Eq)]
109 struct FixturePayload {
110 title: String,
111 aid: u64,
112 }
113
114 fn fixture(name: &str) -> &'static [u8] {
115 match name {
116 "success" => include_bytes!("../tests/fixtures/envelope/success.json"),
117 "result-alias" => include_bytes!("../tests/fixtures/envelope/result-alias.json"),
118 "api-error" => include_bytes!("../tests/fixtures/envelope/api-error.json"),
119 "missing-data" => include_bytes!("../tests/fixtures/envelope/missing-data.json"),
120 "no-payload" => include_bytes!("../tests/fixtures/envelope/no-payload.json"),
121 _ => unreachable!("unknown fixture"),
122 }
123 }
124
125 #[test]
126 fn api_envelope_extracts_data_payload() -> Result<(), BpiError> {
127 let payload =
128 ApiEnvelope::<FixturePayload>::from_slice(fixture("success"))?.into_payload()?;
129
130 assert_eq!(payload.title, "fixture video");
131 Ok(())
132 }
133
134 #[test]
135 fn api_envelope_extracts_result_alias_payload() -> Result<(), BpiError> {
136 let payload =
137 ApiEnvelope::<FixturePayload>::from_slice(fixture("result-alias"))?.into_payload()?;
138
139 assert_eq!(payload.aid, 170002);
140 Ok(())
141 }
142
143 #[test]
144 fn api_envelope_returns_missing_data_for_required_payload() {
145 let err = ApiEnvelope::<FixturePayload>::from_slice(fixture("missing-data"))
146 .and_then(ApiEnvelope::into_payload)
147 .unwrap_err();
148
149 assert!(matches!(err, BpiError::MissingData));
150 }
151
152 #[test]
153 fn api_envelope_allows_optional_payload() -> Result<(), BpiError> {
154 let payload = ApiEnvelope::<FixturePayload>::from_slice(fixture("no-payload"))?
155 .into_optional_payload()?;
156
157 assert!(payload.is_none());
158 Ok(())
159 }
160
161 #[test]
162 fn api_envelope_converts_api_error() {
163 let err = ApiEnvelope::<FixturePayload>::from_slice(fixture("api-error"))
164 .and_then(ApiEnvelope::ensure_success)
165 .unwrap_err();
166
167 assert!(matches!(err, BpiError::Api { code: -101, .. }));
168 assert_eq!(err.code(), Some(-101));
169 }
170
171 #[test]
172 fn api_envelope_treats_null_message_as_empty() -> Result<(), BpiError> {
173 let payload = ApiEnvelope::<LoginCoinFixture>::from_slice(
174 br#"{ "code": 0, "message": null, "data": { "money": 0.0 } }"#,
175 )?
176 .into_payload()?;
177
178 assert_eq!(payload.money, 0.0);
179 Ok(())
180 }
181
182 #[test]
183 fn api_envelope_maps_errno_login_error() {
184 let err = ApiEnvelope::<serde_json::Value>::from_slice(
185 br#"{ "errno": 800501007, "msg": "user not login", "showMsg": "user not login" }"#,
186 )
187 .and_then(ApiEnvelope::ensure_success)
188 .unwrap_err();
189
190 assert!(err.requires_login());
191 assert_eq!(err.code(), Some(800501007));
192 }
193
194 #[test]
195 fn api_envelope_returns_decode_error_for_invalid_json() {
196 let err = ApiEnvelope::<FixturePayload>::from_slice(b"{not json").unwrap_err();
197
198 assert!(matches!(err, BpiError::Decode { .. }));
199 }
200
201 #[derive(Debug, Deserialize)]
202 struct LoginCoinFixture {
203 money: f64,
204 }
205}