1use serde::de::{self, Deserializer, MapAccess, Visitor};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Default, Debug, Clone, Serialize)]
7pub struct Vip {
8 pub vip_type: u8,
11 pub vip_status: u8,
14 pub vip_due_date: u64,
17 pub label: VipLabel,
19 pub nickname_color: String,
21
22 pub vip_pay_type: Option<u8>,
24
25 pub role: Option<u8>,
27
28 pub is_tv_vip: Option<bool>,
30 pub tv_vip_status: Option<u8>,
32 pub tv_vip_pay_type: Option<u8>,
34 pub tv_due_date: Option<u64>,
36
37 pub mid: Option<u64>,
39 pub name: Option<String>,
41}
42
43#[derive(Default, Debug, Clone, Serialize, Deserialize)]
45pub struct VipLabel {
46 #[serde(default)]
48 pub text: String,
49 #[serde(default)]
51 pub label_theme: String,
52 #[serde(default)]
54 pub text_color: String,
55 #[serde(default)]
57 pub bg_style: u32,
58 #[serde(default)]
60 pub bg_color: String,
61}
62
63impl<'de> Deserialize<'de> for Vip {
64 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65 where
66 D: Deserializer<'de>,
67 {
68 struct VipVisitor;
69
70 impl<'de> Visitor<'de> for VipVisitor {
71 type Value = Vip;
72
73 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
74 formatter.write_str("a Vip object")
75 }
76
77 fn visit_map<M>(self, mut map: M) -> Result<Vip, M::Error>
78 where
79 M: MapAccess<'de>,
80 {
81 let mut vip_type = None;
82 let mut vip_status = None;
83 let mut vip_due_date = None;
84 let mut label = None;
85 let mut nickname_color = None;
86 let mut vip_pay_type = None;
87 let mut role = None;
88 let mut is_tv_vip = None;
89 let mut tv_vip_status = None;
90 let mut tv_vip_pay_type = None;
91 let mut tv_due_date = None;
92 let mut mid = None;
93 let mut name = None;
94
95 while let Some(key) = map.next_key::<String>()? {
96 match key.as_str() {
97 "vipType" | "vip_type" | "type" => {
98 if vip_type.is_some() {
99 return Err(de::Error::duplicate_field("vip_type"));
100 }
101 vip_type = Some(next_u8_value(&mut map, "vip_type")?);
102 }
103 "vip_status" | "vipStatus" | "status" => {
104 if vip_status.is_none() {
105 vip_status = Some(next_u8_value(&mut map, "vip_status")?);
106 } else {
107 let _: serde_json::Value = map.next_value()?;
108 }
109 }
110 "vipDueDate" | "due_date" | "vip_due_date" => {
111 if vip_due_date.is_none() {
112 vip_due_date = Some(next_u64_value(&mut map, "vip_due_date")?);
113 } else {
114 let _: serde_json::Value = map.next_value()?;
115 }
116 }
117 "label" => {
118 if label.is_some() {
119 return Err(de::Error::duplicate_field("label"));
120 }
121 label = Some(map.next_value()?);
122 }
123 "nickname_color" => {
124 if nickname_color.is_some() {
125 return Err(de::Error::duplicate_field("nickname_color"));
126 }
127 nickname_color = Some(map.next_value()?);
128 }
129 "vip_pay_type" => {
130 if vip_pay_type.is_some() {
131 return Err(de::Error::duplicate_field("vip_pay_type"));
132 }
133 vip_pay_type = next_optional_u8_value(&mut map, "vip_pay_type")?;
134 }
135 "role" => {
136 if role.is_some() {
137 return Err(de::Error::duplicate_field("role"));
138 }
139 role = next_optional_u8_value(&mut map, "role")?;
140 }
141 "is_tv_vip" => {
142 if is_tv_vip.is_some() {
143 return Err(de::Error::duplicate_field("is_tv_vip"));
144 }
145 is_tv_vip = Some(map.next_value()?);
146 }
147 "tv_vip_status" => {
148 if tv_vip_status.is_some() {
149 return Err(de::Error::duplicate_field("tv_vip_status"));
150 }
151 tv_vip_status = next_optional_u8_value(&mut map, "tv_vip_status")?;
152 }
153 "tv_vip_pay_type" => {
154 if tv_vip_pay_type.is_some() {
155 return Err(de::Error::duplicate_field("tv_vip_pay_type"));
156 }
157 tv_vip_pay_type = next_optional_u8_value(&mut map, "tv_vip_pay_type")?;
158 }
159 "tv_due_date" => {
160 if tv_due_date.is_some() {
161 return Err(de::Error::duplicate_field("tv_due_date"));
162 }
163 tv_due_date = next_optional_u64_value(&mut map, "tv_due_date")?;
164 }
165 "mid" => {
166 if mid.is_some() {
167 return Err(de::Error::duplicate_field("mid"));
168 }
169 mid = next_optional_u64_value(&mut map, "mid")?;
170 }
171 "name" => {
172 if name.is_some() {
173 return Err(de::Error::duplicate_field("name"));
174 }
175 name = Some(map.next_value()?);
176 }
177 _ => {
178 let _: serde_json::Value = map.next_value()?;
180 }
181 }
182 }
183
184 let vip_type = vip_type.ok_or_else(|| de::Error::missing_field("vip_type"))?;
185 let vip_status =
186 vip_status.ok_or_else(|| de::Error::missing_field("vip_status"))?;
187 let vip_due_date = vip_due_date.unwrap_or_default();
188 let label = label.unwrap_or_default();
189 let nickname_color = nickname_color.unwrap_or_default();
190
191 Ok(Vip {
192 vip_type,
193 vip_status,
194 vip_due_date,
195 label,
196 nickname_color,
197 vip_pay_type,
198 role,
199 is_tv_vip,
200 tv_vip_status,
201 tv_vip_pay_type,
202 tv_due_date,
203 mid,
204 name,
205 })
206 }
207 }
208
209 deserializer.deserialize_map(VipVisitor)
210 }
211}
212
213fn next_u8_value<'de, M>(map: &mut M, field: &'static str) -> Result<u8, M::Error>
214where
215 M: MapAccess<'de>,
216{
217 parse_u8_value(map.next_value()?, field)
218}
219
220fn next_u64_value<'de, M>(map: &mut M, field: &'static str) -> Result<u64, M::Error>
221where
222 M: MapAccess<'de>,
223{
224 parse_u64_value(map.next_value()?, field)
225}
226
227fn next_optional_u8_value<'de, M>(map: &mut M, field: &'static str) -> Result<Option<u8>, M::Error>
228where
229 M: MapAccess<'de>,
230{
231 match map.next_value()? {
232 serde_json::Value::Null => Ok(None),
233 value => parse_u8_value(value, field).map(Some),
234 }
235}
236
237fn next_optional_u64_value<'de, M>(
238 map: &mut M,
239 field: &'static str,
240) -> Result<Option<u64>, M::Error>
241where
242 M: MapAccess<'de>,
243{
244 match map.next_value()? {
245 serde_json::Value::Null => Ok(None),
246 value => parse_u64_value(value, field).map(Some),
247 }
248}
249
250fn parse_u8_value<E>(value: serde_json::Value, field: &'static str) -> Result<u8, E>
251where
252 E: de::Error,
253{
254 let raw = parse_u64_value::<E>(value, field)?;
255 u8::try_from(raw).map_err(|_| E::custom(format!("{field} must fit in u8")))
256}
257
258fn parse_u64_value<E>(value: serde_json::Value, field: &'static str) -> Result<u64, E>
259where
260 E: de::Error,
261{
262 match value {
263 serde_json::Value::Number(number) => number
264 .as_u64()
265 .ok_or_else(|| E::custom(format!("{field} must be a non-negative integer"))),
266 serde_json::Value::String(text) => text
267 .parse::<u64>()
268 .map_err(|_| E::custom(format!("{field} must be a numeric string"))),
269 _ => Err(E::custom(format!("{field} must be a string or number"))),
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn vip_deserializes_numeric_fields_from_strings() {
279 let vip: Vip = serde_json::from_str(
280 r##"{
281 "type": "2",
282 "status": "1",
283 "due_date": "1813334400000",
284 "label": {
285 "text": "年度大会员",
286 "label_theme": "annual_vip",
287 "text_color": "#FFFFFF",
288 "bg_style": 1,
289 "bg_color": "#FB7299"
290 },
291 "nickname_color": "#FB7299",
292 "role": "3",
293 "tv_due_date": "0",
294 "tv_vip_pay_type": "0",
295 "tv_vip_status": "0",
296 "vip_pay_type": "0",
297 "mid": "4279370"
298 }"##,
299 )
300 .expect("vip should parse string numeric fields");
301
302 assert_eq!(vip.vip_type, 2);
303 assert_eq!(vip.vip_status, 1);
304 assert_eq!(vip.vip_due_date, 1813334400000);
305 assert_eq!(vip.role, Some(3));
306 assert_eq!(vip.mid, Some(4279370));
307 }
308
309 #[test]
310 fn vip_label_defaults_missing_display_fields() {
311 let label: VipLabel = serde_json::from_str(
312 r##"{
313 "bg_style": 1,
314 "text_color": ""
315 }"##,
316 )
317 .expect("vip label should tolerate omitted display fields");
318
319 assert_eq!(label.text, "");
320 assert_eq!(label.label_theme, "");
321 assert_eq!(label.text_color, "");
322 assert_eq!(label.bg_style, 1);
323 assert_eq!(label.bg_color, "");
324 }
325
326 #[test]
327 fn vip_deserializes_compact_relation_payload() {
328 let vip: Vip = serde_json::from_str(
329 r#"{
330 "vipType": 0,
331 "vipStatus": 0
332 }"#,
333 )
334 .expect("relation-list vip payload should parse");
335
336 assert_eq!(vip.vip_type, 0);
337 assert_eq!(vip.vip_status, 0);
338 assert_eq!(vip.vip_due_date, 0);
339 assert_eq!(vip.nickname_color, "");
340 assert_eq!(vip.label.text, "");
341 }
342}