1use crate::{BpiError, BpiResult};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct MessageUnreadCountParams {
6 build: String,
7 mobi_app: String,
8}
9
10impl Default for MessageUnreadCountParams {
11 fn default() -> Self {
12 Self {
13 build: "0".to_string(),
14 mobi_app: "web".to_string(),
15 }
16 }
17}
18
19impl MessageUnreadCountParams {
20 pub fn new() -> Self {
21 Self::default()
22 }
23
24 pub fn with_build(mut self, build: impl Into<String>) -> BpiResult<Self> {
25 self.build = normalize_non_blank("build", build.into())?;
26 Ok(self)
27 }
28
29 pub fn with_mobi_app(mut self, mobi_app: impl Into<String>) -> BpiResult<Self> {
30 self.mobi_app = normalize_non_blank("mobi_app", mobi_app.into())?;
31 Ok(self)
32 }
33
34 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
35 vec![
36 ("build", self.build.clone()),
37 ("mobi_app", self.mobi_app.clone()),
38 ]
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct MessageReplyFeedParams {
45 start_id: Option<u64>,
46 start_time: Option<u64>,
47 build: String,
48 mobi_app: String,
49 platform: String,
50 web_location: String,
51}
52
53impl Default for MessageReplyFeedParams {
54 fn default() -> Self {
55 Self {
56 start_id: None,
57 start_time: None,
58 build: "0".to_string(),
59 mobi_app: "web".to_string(),
60 platform: "web".to_string(),
61 web_location: String::new(),
62 }
63 }
64}
65
66impl MessageReplyFeedParams {
67 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn with_start_id(mut self, start_id: u64) -> BpiResult<Self> {
73 self.start_id = Some(validate_positive_u64("id", start_id)?);
74 Ok(self)
75 }
76
77 pub fn with_start_time(mut self, start_time: u64) -> BpiResult<Self> {
79 self.start_time = Some(validate_positive_u64("reply_time", start_time)?);
80 Ok(self)
81 }
82
83 pub fn with_web_location(mut self, web_location: impl Into<String>) -> Self {
85 self.web_location = web_location.into();
86 self
87 }
88
89 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
90 let mut pairs = vec![
91 ("build", self.build.clone()),
92 ("mobi_app", self.mobi_app.clone()),
93 ("platform", self.platform.clone()),
94 ("web_location", self.web_location.clone()),
95 ];
96
97 if let Some(start_id) = self.start_id {
98 pairs.push(("id", start_id.to_string()));
99 }
100 if let Some(start_time) = self.start_time {
101 pairs.push(("reply_time", start_time.to_string()));
102 }
103
104 pairs
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum SingleUnreadType {
111 All,
112 Follow,
113 Unfollow,
114 Blocked,
115 Custom(u32),
116}
117
118impl SingleUnreadType {
119 fn as_query_value(self) -> String {
120 match self {
121 Self::All => "0".to_string(),
122 Self::Follow => "1".to_string(),
123 Self::Unfollow => "2".to_string(),
124 Self::Blocked => "3".to_string(),
125 Self::Custom(value) => value.to_string(),
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct MessageSingleUnreadParams {
133 unread_type: SingleUnreadType,
134 show_unfollow_list: bool,
135 show_dustbin: bool,
136 build: String,
137 mobi_app: String,
138}
139
140impl Default for MessageSingleUnreadParams {
141 fn default() -> Self {
142 Self {
143 unread_type: SingleUnreadType::All,
144 show_unfollow_list: false,
145 show_dustbin: false,
146 build: "0".to_string(),
147 mobi_app: "web".to_string(),
148 }
149 }
150}
151
152impl MessageSingleUnreadParams {
153 pub fn new() -> Self {
154 Self::default()
155 }
156
157 pub fn with_unread_type(mut self, unread_type: SingleUnreadType) -> Self {
158 if unread_type == SingleUnreadType::Blocked {
159 self.show_dustbin = true;
160 }
161 self.unread_type = unread_type;
162 self
163 }
164
165 pub fn show_unfollow_list(mut self, show: bool) -> Self {
166 self.show_unfollow_list = show;
167 self
168 }
169
170 pub fn show_dustbin(mut self, show: bool) -> Self {
171 self.show_dustbin = show;
172 self
173 }
174
175 pub fn with_custom_unread_type(mut self, unread_type: u32) -> BpiResult<Self> {
176 self.unread_type =
177 SingleUnreadType::Custom(validate_positive_u32("unread_type", unread_type)?);
178 Ok(self)
179 }
180
181 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
182 vec![
183 ("build", self.build.clone()),
184 ("mobi_app", self.mobi_app.clone()),
185 ("unread_type", self.unread_type.as_query_value()),
186 (
187 "show_unfollow_list",
188 bool_flag(self.show_unfollow_list).to_string(),
189 ),
190 ("show_dustbin", bool_flag(self.show_dustbin).to_string()),
191 ]
192 }
193}
194
195fn bool_flag(value: bool) -> &'static str {
196 if value { "1" } else { "0" }
197}
198
199fn validate_positive_u32(field: &'static str, value: u32) -> BpiResult<u32> {
200 if value == 0 {
201 return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
202 }
203
204 Ok(value)
205}
206
207fn validate_positive_u64(field: &'static str, value: u64) -> BpiResult<u64> {
208 if value == 0 {
209 return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
210 }
211
212 Ok(value)
213}
214
215fn normalize_non_blank(field: &'static str, value: String) -> BpiResult<String> {
216 let value = value.trim();
217 if value.is_empty() {
218 return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
219 }
220
221 Ok(value.to_string())
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn unread_count_params_serializes_defaults() {
230 let params = MessageUnreadCountParams::new();
231
232 assert_eq!(
233 params.query_pairs(),
234 vec![("build", "0".to_string()), ("mobi_app", "web".to_string())]
235 );
236 }
237
238 #[test]
239 fn unread_count_params_serializes_custom_client() -> BpiResult<()> {
240 let params = MessageUnreadCountParams::new()
241 .with_build("123")?
242 .with_mobi_app("android")?;
243
244 assert_eq!(
245 params.query_pairs(),
246 vec![
247 ("build", "123".to_string()),
248 ("mobi_app", "android".to_string()),
249 ]
250 );
251 Ok(())
252 }
253
254 #[test]
255 fn unread_count_params_rejects_blank_mobi_app() {
256 let err = MessageUnreadCountParams::new()
257 .with_mobi_app(" ")
258 .unwrap_err();
259
260 assert!(matches!(
261 err,
262 BpiError::InvalidParameter {
263 field: "mobi_app",
264 ..
265 }
266 ));
267 }
268
269 #[test]
270 fn reply_feed_params_serializes_defaults() {
271 let params = MessageReplyFeedParams::new();
272
273 assert_eq!(
274 params.query_pairs(),
275 vec![
276 ("build", "0".to_string()),
277 ("mobi_app", "web".to_string()),
278 ("platform", "web".to_string()),
279 ("web_location", String::new()),
280 ]
281 );
282 }
283
284 #[test]
285 fn reply_feed_params_serializes_cursor() -> BpiResult<()> {
286 let params = MessageReplyFeedParams::new()
287 .with_start_id(1001)?
288 .with_start_time(1_700_000_000)?;
289
290 assert_eq!(
291 params.query_pairs(),
292 vec![
293 ("build", "0".to_string()),
294 ("mobi_app", "web".to_string()),
295 ("platform", "web".to_string()),
296 ("web_location", String::new()),
297 ("id", "1001".to_string()),
298 ("reply_time", "1700000000".to_string()),
299 ]
300 );
301 Ok(())
302 }
303
304 #[test]
305 fn reply_feed_params_rejects_zero_start_id() {
306 let err = MessageReplyFeedParams::new().with_start_id(0).unwrap_err();
307
308 assert!(matches!(
309 err,
310 BpiError::InvalidParameter { field: "id", .. }
311 ));
312 }
313
314 #[test]
315 fn single_unread_params_serializes_defaults() {
316 let params = MessageSingleUnreadParams::new();
317
318 assert_eq!(
319 params.query_pairs(),
320 vec![
321 ("build", "0".to_string()),
322 ("mobi_app", "web".to_string()),
323 ("unread_type", "0".to_string()),
324 ("show_unfollow_list", "0".to_string()),
325 ("show_dustbin", "0".to_string()),
326 ]
327 );
328 }
329
330 #[test]
331 fn single_unread_params_serializes_flags() {
332 let params = MessageSingleUnreadParams::new()
333 .with_unread_type(SingleUnreadType::Follow)
334 .show_unfollow_list(true)
335 .show_dustbin(true);
336
337 assert_eq!(
338 params.query_pairs(),
339 vec![
340 ("build", "0".to_string()),
341 ("mobi_app", "web".to_string()),
342 ("unread_type", "1".to_string()),
343 ("show_unfollow_list", "1".to_string()),
344 ("show_dustbin", "1".to_string()),
345 ]
346 );
347 }
348
349 #[test]
350 fn single_unread_params_enables_dustbin_for_blocked_type() {
351 let params = MessageSingleUnreadParams::new().with_unread_type(SingleUnreadType::Blocked);
352
353 assert_eq!(params.query_pairs()[4], ("show_dustbin", "1".to_string()));
354 }
355
356 #[test]
357 fn single_unread_params_rejects_zero_custom_type() {
358 let err = MessageSingleUnreadParams::new()
359 .with_custom_unread_type(0)
360 .unwrap_err();
361
362 assert!(matches!(
363 err,
364 BpiError::InvalidParameter {
365 field: "unread_type",
366 ..
367 }
368 ));
369 }
370}