1use std::str::FromStr;
2
3use crate::{BpiError, BpiResult};
4
5#[derive(Debug, Clone, PartialEq, Eq, Default)]
7pub struct VideoPopularListParams {
8 page: Option<u32>,
9 page_size: Option<u32>,
10}
11
12impl VideoPopularListParams {
13 pub fn new() -> Self {
14 Self::default()
15 }
16
17 pub fn with_page(mut self, page: u32) -> BpiResult<Self> {
18 self.page = Some(validate_positive("pn", page)?);
19 Ok(self)
20 }
21
22 pub fn with_page_size(mut self, page_size: u32) -> BpiResult<Self> {
23 self.page_size = Some(validate_positive("ps", page_size)?);
24 Ok(self)
25 }
26
27 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
28 let mut pairs = Vec::new();
29
30 if let Some(page) = self.page {
31 pairs.push(("pn", page.to_string()));
32 }
33 if let Some(page_size) = self.page_size {
34 pairs.push(("ps", page_size.to_string()));
35 }
36
37 pairs
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct PopularSeriesOneParams {
44 number: u32,
45}
46
47impl PopularSeriesOneParams {
48 pub fn new(number: u32) -> BpiResult<Self> {
49 Ok(Self {
50 number: validate_positive("number", number)?,
51 })
52 }
53
54 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
55 vec![("number", self.number.to_string())]
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum VideoRankingType {
62 All,
63 Rookie,
64 Origin,
65}
66
67impl VideoRankingType {
68 pub fn as_str(self) -> &'static str {
69 match self {
70 Self::All => "all",
71 Self::Rookie => "rookie",
72 Self::Origin => "origin",
73 }
74 }
75}
76
77impl FromStr for VideoRankingType {
78 type Err = BpiError;
79
80 fn from_str(value: &str) -> Result<Self, Self::Err> {
81 match value {
82 "all" => Ok(Self::All),
83 "rookie" => Ok(Self::Rookie),
84 "origin" => Ok(Self::Origin),
85 _ => Err(BpiError::invalid_parameter(
86 "type",
87 "supported ranking types are all, rookie, and origin",
88 )),
89 }
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Default)]
95pub struct VideoRankingListParams {
96 rid: Option<u32>,
97 ranking_type: Option<VideoRankingType>,
98}
99
100impl VideoRankingListParams {
101 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn with_rid(mut self, rid: u32) -> BpiResult<Self> {
106 self.rid = Some(validate_positive("rid", rid)?);
107 Ok(self)
108 }
109
110 pub fn with_type(mut self, ranking_type: VideoRankingType) -> Self {
111 self.ranking_type = Some(ranking_type);
112 self
113 }
114
115 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
116 let mut pairs = Vec::new();
117
118 if let Some(rid) = self.rid {
119 pairs.push(("rid", rid.to_string()));
120 }
121 if let Some(ranking_type) = self.ranking_type {
122 pairs.push(("type", ranking_type.as_str().to_string()));
123 }
124
125 pairs
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct VideoRegionDynamicParams {
132 rid: u32,
133 page: Option<u32>,
134 page_size: Option<u32>,
135}
136
137impl VideoRegionDynamicParams {
138 pub fn new(rid: u32) -> BpiResult<Self> {
139 Ok(Self {
140 rid: validate_positive("rid", rid)?,
141 page: None,
142 page_size: None,
143 })
144 }
145
146 pub fn with_page(mut self, page: u32) -> BpiResult<Self> {
147 self.page = Some(validate_positive("pn", page)?);
148 Ok(self)
149 }
150
151 pub fn with_page_size(mut self, page_size: u32) -> BpiResult<Self> {
152 self.page_size = Some(validate_positive("ps", page_size)?);
153 Ok(self)
154 }
155
156 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
157 let mut pairs = vec![("rid", self.rid.to_string())];
158 append_optional_pagination(&mut pairs, self.page, self.page_size);
159 pairs
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct VideoRegionTagDynamicParams {
166 rid: u32,
167 tag_id: u64,
168 page: Option<u32>,
169 page_size: Option<u32>,
170}
171
172impl VideoRegionTagDynamicParams {
173 pub fn new(rid: u32, tag_id: u64) -> BpiResult<Self> {
174 Ok(Self {
175 rid: validate_positive("rid", rid)?,
176 tag_id: validate_positive_u64("tag_id", tag_id)?,
177 page: None,
178 page_size: None,
179 })
180 }
181
182 pub fn with_page(mut self, page: u32) -> BpiResult<Self> {
183 self.page = Some(validate_positive("pn", page)?);
184 Ok(self)
185 }
186
187 pub fn with_page_size(mut self, page_size: u32) -> BpiResult<Self> {
188 self.page_size = Some(validate_positive("ps", page_size)?);
189 Ok(self)
190 }
191
192 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
193 let mut pairs = vec![
194 ("rid", self.rid.to_string()),
195 ("tag_id", self.tag_id.to_string()),
196 ];
197 append_optional_pagination(&mut pairs, self.page, self.page_size);
198 pairs
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct VideoRegionNewListParams {
205 rid: u32,
206 page: Option<u32>,
207 page_size: Option<u32>,
208 typ: Option<u32>,
209}
210
211impl VideoRegionNewListParams {
212 pub fn new(rid: u32) -> BpiResult<Self> {
213 Ok(Self {
214 rid: validate_positive("rid", rid)?,
215 page: None,
216 page_size: None,
217 typ: None,
218 })
219 }
220
221 pub fn with_page(mut self, page: u32) -> BpiResult<Self> {
222 self.page = Some(validate_positive("pn", page)?);
223 Ok(self)
224 }
225
226 pub fn with_page_size(mut self, page_size: u32) -> BpiResult<Self> {
227 self.page_size = Some(validate_positive("ps", page_size)?);
228 Ok(self)
229 }
230
231 pub fn with_type(mut self, typ: u32) -> BpiResult<Self> {
232 self.typ = Some(validate_positive("type", typ)?);
233 Ok(self)
234 }
235
236 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
237 let mut pairs = vec![("rid", self.rid.to_string())];
238 append_optional_pagination(&mut pairs, self.page, self.page_size);
239 if let Some(typ) = self.typ {
240 pairs.push(("type", typ.to_string()));
241 }
242 pairs
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum VideoNewListRankOrder {
249 Click,
250 Scores,
251 Pubdate,
252}
253
254impl VideoNewListRankOrder {
255 pub fn as_str(self) -> &'static str {
256 match self {
257 Self::Click => "click",
258 Self::Scores => "scores",
259 Self::Pubdate => "pubdate",
260 }
261 }
262}
263
264impl FromStr for VideoNewListRankOrder {
265 type Err = BpiError;
266
267 fn from_str(value: &str) -> Result<Self, Self::Err> {
268 match value {
269 "click" => Ok(Self::Click),
270 "scores" => Ok(Self::Scores),
271 "pubdate" => Ok(Self::Pubdate),
272 _ => Err(BpiError::invalid_parameter(
273 "order",
274 "supported newlist rank orders are click, scores, and pubdate",
275 )),
276 }
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct VideoRegionNewListRankParams {
283 cate_id: u32,
284 order: Option<VideoNewListRankOrder>,
285 page: Option<u32>,
286 page_size: u32,
287 time_from: String,
288 time_to: String,
289}
290
291impl VideoRegionNewListRankParams {
292 pub fn new(
293 cate_id: u32,
294 page_size: u32,
295 time_from: impl Into<String>,
296 time_to: impl Into<String>,
297 ) -> BpiResult<Self> {
298 let time_from = time_from.into();
299 let time_to = time_to.into();
300 validate_non_blank("time_from", &time_from)?;
301 validate_non_blank("time_to", &time_to)?;
302
303 Ok(Self {
304 cate_id: validate_positive("cate_id", cate_id)?,
305 order: None,
306 page: None,
307 page_size: validate_positive("pagesize", page_size)?,
308 time_from,
309 time_to,
310 })
311 }
312
313 pub fn with_order(mut self, order: VideoNewListRankOrder) -> Self {
314 self.order = Some(order);
315 self
316 }
317
318 pub fn with_page(mut self, page: u32) -> BpiResult<Self> {
319 self.page = Some(validate_positive("page", page)?);
320 Ok(self)
321 }
322
323 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
324 let mut pairs = vec![
325 ("search_type", "video".to_string()),
326 ("view_type", "hot_rank".to_string()),
327 ("cate_id", self.cate_id.to_string()),
328 ("pagesize", self.page_size.to_string()),
329 ("time_from", self.time_from.clone()),
330 ("time_to", self.time_to.clone()),
331 ];
332
333 if let Some(order) = self.order {
334 pairs.push(("order", order.as_str().to_string()));
335 }
336 if let Some(page) = self.page {
337 pairs.push(("page", page.to_string()));
338 }
339
340 pairs
341 }
342}
343
344fn append_optional_pagination(
345 pairs: &mut Vec<(&'static str, String)>,
346 page: Option<u32>,
347 page_size: Option<u32>,
348) {
349 if let Some(page) = page {
350 pairs.push(("pn", page.to_string()));
351 }
352 if let Some(page_size) = page_size {
353 pairs.push(("ps", page_size.to_string()));
354 }
355}
356
357fn validate_positive(field: &'static str, value: u32) -> BpiResult<u32> {
358 if value == 0 {
359 return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
360 }
361
362 Ok(value)
363}
364
365fn validate_positive_u64(field: &'static str, value: u64) -> BpiResult<u64> {
366 if value == 0 {
367 return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
368 }
369
370 Ok(value)
371}
372
373fn validate_non_blank(field: &'static str, value: &str) -> BpiResult<()> {
374 if value.trim().is_empty() {
375 return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
376 }
377
378 Ok(())
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn popular_list_params_serializes_empty_defaults() {
387 let params = VideoPopularListParams::new();
388
389 assert!(params.query_pairs().is_empty());
390 }
391
392 #[test]
393 fn popular_list_params_serializes_pagination() -> BpiResult<()> {
394 let params = VideoPopularListParams::new()
395 .with_page(1)?
396 .with_page_size(2)?;
397
398 assert_eq!(
399 params.query_pairs(),
400 vec![("pn", "1".to_string()), ("ps", "2".to_string())]
401 );
402 Ok(())
403 }
404
405 #[test]
406 fn popular_series_one_params_rejects_zero_number() {
407 let err = PopularSeriesOneParams::new(0).unwrap_err();
408
409 assert!(matches!(
410 err,
411 BpiError::InvalidParameter {
412 field: "number",
413 ..
414 }
415 ));
416 }
417
418 #[test]
419 fn ranking_type_parses_supported_values() -> BpiResult<()> {
420 assert_eq!("all".parse::<VideoRankingType>()?, VideoRankingType::All);
421 assert_eq!(
422 "rookie".parse::<VideoRankingType>()?,
423 VideoRankingType::Rookie
424 );
425 assert_eq!(
426 "origin".parse::<VideoRankingType>()?,
427 VideoRankingType::Origin
428 );
429 Ok(())
430 }
431
432 #[test]
433 fn ranking_list_params_serializes_filters() -> BpiResult<()> {
434 let params = VideoRankingListParams::new()
435 .with_rid(21)?
436 .with_type(VideoRankingType::Rookie);
437
438 assert_eq!(
439 params.query_pairs(),
440 vec![("rid", "21".to_string()), ("type", "rookie".to_string())]
441 );
442 Ok(())
443 }
444
445 #[test]
446 fn region_dynamic_params_serializes_pagination() -> BpiResult<()> {
447 let params = VideoRegionDynamicParams::new(21)?
448 .with_page(1)?
449 .with_page_size(2)?;
450
451 assert_eq!(
452 params.query_pairs(),
453 vec![
454 ("rid", "21".to_string()),
455 ("pn", "1".to_string()),
456 ("ps", "2".to_string())
457 ]
458 );
459 Ok(())
460 }
461
462 #[test]
463 fn region_tag_dynamic_params_serializes_required_values() -> BpiResult<()> {
464 let params = VideoRegionTagDynamicParams::new(136, 10026108)?;
465
466 assert_eq!(
467 params.query_pairs(),
468 vec![
469 ("rid", "136".to_string()),
470 ("tag_id", "10026108".to_string())
471 ]
472 );
473 Ok(())
474 }
475
476 #[test]
477 fn region_newlist_params_serializes_type_filter() -> BpiResult<()> {
478 let params = VideoRegionNewListParams::new(231)?.with_type(1)?;
479
480 assert_eq!(
481 params.query_pairs(),
482 vec![("rid", "231".to_string()), ("type", "1".to_string())]
483 );
484 Ok(())
485 }
486
487 #[test]
488 fn newlist_rank_params_serializes_required_and_optional_values() -> BpiResult<()> {
489 let params = VideoRegionNewListRankParams::new(231, 2, "20260701", "20260703")?
490 .with_order(VideoNewListRankOrder::Click)
491 .with_page(1)?;
492
493 assert_eq!(
494 params.query_pairs(),
495 vec![
496 ("search_type", "video".to_string()),
497 ("view_type", "hot_rank".to_string()),
498 ("cate_id", "231".to_string()),
499 ("pagesize", "2".to_string()),
500 ("time_from", "20260701".to_string()),
501 ("time_to", "20260703".to_string()),
502 ("order", "click".to_string()),
503 ("page", "1".to_string()),
504 ]
505 );
506 Ok(())
507 }
508
509 #[test]
510 fn newlist_rank_params_rejects_blank_time() {
511 let err = VideoRegionNewListRankParams::new(231, 2, " ", "20260703").unwrap_err();
512
513 assert!(matches!(
514 err,
515 BpiError::InvalidParameter {
516 field: "time_from",
517 ..
518 }
519 ));
520 }
521}