Skip to main content

bpi_rs/video_ranking/
params.rs

1use std::str::FromStr;
2
3use crate::{BpiError, BpiResult};
4
5/// Parameters for `/x/web-interface/popular`.
6#[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/// Parameters for `/x/web-interface/popular/series/one`.
42#[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/// Ranking type accepted by `/x/web-interface/ranking/v2`.
60#[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/// Parameters for `/x/web-interface/ranking/v2`.
94#[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/// Parameters for `/x/web-interface/dynamic/region`.
130#[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/// Parameters for `/x/web-interface/dynamic/tag`.
164#[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/// Parameters for `/x/web-interface/newlist`.
203#[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/// Ordering accepted by `/x/web-interface/newlist_rank`.
247#[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/// Parameters for `/x/web-interface/newlist_rank`.
281#[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}