Skip to main content

bpi_rs/fav/
params.rs

1use crate::ids::{MediaId, Mid};
2use crate::{BpiError, BpiResult};
3
4/// Parameters for `/x/v3/fav/folder/info`.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct FavFolderInfoParams {
7    media_id: MediaId,
8}
9
10impl FavFolderInfoParams {
11    pub fn new(media_id: MediaId) -> Self {
12        Self { media_id }
13    }
14
15    pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
16        vec![("media_id", self.media_id.to_string())]
17    }
18}
19
20/// Parameters for `/x/v3/fav/folder/created/list-all`.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct FavCreatedListParams {
23    up_mid: Mid,
24    typ: Option<u8>,
25    rid: Option<u64>,
26    web_location: String,
27}
28
29impl FavCreatedListParams {
30    pub fn new(up_mid: Mid) -> Self {
31        Self {
32            up_mid,
33            typ: None,
34            rid: None,
35            web_location: "333.1387".to_string(),
36        }
37    }
38
39    pub fn with_type(mut self, typ: u8) -> Self {
40        self.typ = Some(typ);
41        self
42    }
43
44    pub fn with_resource_id(mut self, rid: u64) -> BpiResult<Self> {
45        self.rid = Some(validate_positive_u64("rid", rid)?);
46        Ok(self)
47    }
48
49    pub fn with_web_location(mut self, web_location: impl Into<String>) -> BpiResult<Self> {
50        self.web_location = normalize_non_blank("web_location", web_location.into())?;
51        Ok(self)
52    }
53
54    pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
55        let mut pairs = vec![("up_mid", self.up_mid.to_string())];
56
57        if let Some(typ) = self.typ {
58            pairs.push(("type", typ.to_string()));
59        }
60        if let Some(rid) = self.rid {
61            pairs.push(("rid", rid.to_string()));
62        }
63        pairs.push(("web_location", self.web_location.clone()));
64
65        pairs
66    }
67}
68
69/// Parameters for `/x/v3/fav/folder/collected/list`.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct FavCollectedListParams {
72    up_mid: Mid,
73    page: u32,
74    page_size: u32,
75    platform: String,
76}
77
78impl FavCollectedListParams {
79    pub fn new(up_mid: Mid) -> Self {
80        Self {
81            up_mid,
82            page: 1,
83            page_size: 20,
84            platform: "web".to_string(),
85        }
86    }
87
88    pub fn with_page(mut self, page: u32) -> BpiResult<Self> {
89        self.page = validate_positive_u32("pn", page)?;
90        Ok(self)
91    }
92
93    pub fn with_page_size(mut self, page_size: u32) -> BpiResult<Self> {
94        self.page_size = validate_positive_u32("ps", page_size)?;
95        Ok(self)
96    }
97
98    pub fn with_platform(mut self, platform: impl Into<String>) -> BpiResult<Self> {
99        self.platform = normalize_non_blank("platform", platform.into())?;
100        Ok(self)
101    }
102
103    pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
104        vec![
105            ("up_mid", self.up_mid.to_string()),
106            ("pn", self.page.to_string()),
107            ("ps", self.page_size.to_string()),
108            ("platform", self.platform.clone()),
109        ]
110    }
111}
112
113/// Parameters for `/x/v3/fav/resource/infos`.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct FavResourceInfosParams {
116    resources: String,
117    platform: String,
118}
119
120impl FavResourceInfosParams {
121    pub fn new(resources: impl Into<String>) -> BpiResult<Self> {
122        Ok(Self {
123            resources: normalize_non_blank("resources", resources.into())?,
124            platform: "web".to_string(),
125        })
126    }
127
128    pub fn with_platform(mut self, platform: impl Into<String>) -> BpiResult<Self> {
129        self.platform = normalize_non_blank("platform", platform.into())?;
130        Ok(self)
131    }
132
133    pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
134        vec![
135            ("resources", self.resources.clone()),
136            ("platform", self.platform.clone()),
137        ]
138    }
139}
140
141/// Parameters for `/x/v3/fav/resource/ids`.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct FavResourceIdsParams {
144    media_id: MediaId,
145    platform: String,
146}
147
148impl FavResourceIdsParams {
149    pub fn new(media_id: MediaId) -> Self {
150        Self {
151            media_id,
152            platform: "web".to_string(),
153        }
154    }
155
156    pub fn with_platform(mut self, platform: impl Into<String>) -> BpiResult<Self> {
157        self.platform = normalize_non_blank("platform", platform.into())?;
158        Ok(self)
159    }
160
161    pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
162        vec![
163            ("media_id", self.media_id.to_string()),
164            ("platform", self.platform.clone()),
165        ]
166    }
167}
168
169/// Parameters for creating a favorite folder.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct FavFolderAddParams {
172    title: String,
173    intro: Option<String>,
174    privacy: Option<u8>,
175    cover: Option<String>,
176}
177
178impl FavFolderAddParams {
179    pub fn new(title: impl Into<String>) -> BpiResult<Self> {
180        Ok(Self {
181            title: normalize_non_blank("title", title.into())?,
182            intro: None,
183            privacy: None,
184            cover: None,
185        })
186    }
187
188    pub fn intro(mut self, intro: impl Into<String>) -> BpiResult<Self> {
189        self.intro = Some(normalize_non_blank("intro", intro.into())?);
190        Ok(self)
191    }
192
193    pub fn privacy(mut self, privacy: u8) -> BpiResult<Self> {
194        self.privacy = Some(validate_privacy(privacy)?);
195        Ok(self)
196    }
197
198    pub fn cover(mut self, cover: impl Into<String>) -> BpiResult<Self> {
199        self.cover = Some(normalize_non_blank("cover", cover.into())?);
200        Ok(self)
201    }
202
203    pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
204        let mut pairs = vec![("title", self.title.clone()), ("csrf", csrf.to_string())];
205        push_optional(&mut pairs, "intro", &self.intro);
206        push_optional_value(&mut pairs, "privacy", self.privacy);
207        push_optional(&mut pairs, "cover", &self.cover);
208        pairs
209    }
210}
211
212/// Parameters for editing a favorite folder.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct FavFolderEditParams {
215    media_id: MediaId,
216    title: String,
217    intro: Option<String>,
218    privacy: Option<u8>,
219    cover: Option<String>,
220}
221
222impl FavFolderEditParams {
223    pub fn new(media_id: MediaId, title: impl Into<String>) -> BpiResult<Self> {
224        Ok(Self {
225            media_id,
226            title: normalize_non_blank("title", title.into())?,
227            intro: None,
228            privacy: None,
229            cover: None,
230        })
231    }
232
233    pub fn intro(mut self, intro: impl Into<String>) -> BpiResult<Self> {
234        self.intro = Some(normalize_non_blank("intro", intro.into())?);
235        Ok(self)
236    }
237
238    pub fn privacy(mut self, privacy: u8) -> BpiResult<Self> {
239        self.privacy = Some(validate_privacy(privacy)?);
240        Ok(self)
241    }
242
243    pub fn cover(mut self, cover: impl Into<String>) -> BpiResult<Self> {
244        self.cover = Some(normalize_non_blank("cover", cover.into())?);
245        Ok(self)
246    }
247
248    pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
249        let mut pairs = vec![
250            ("media_id", self.media_id.to_string()),
251            ("title", self.title.clone()),
252            ("csrf", csrf.to_string()),
253        ];
254        push_optional(&mut pairs, "intro", &self.intro);
255        push_optional_value(&mut pairs, "privacy", self.privacy);
256        push_optional(&mut pairs, "cover", &self.cover);
257        pairs
258    }
259}
260
261/// Parameters for deleting favorite folders.
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct FavFolderDeleteParams {
264    media_ids: Vec<MediaId>,
265}
266
267impl FavFolderDeleteParams {
268    pub fn new(media_ids: impl IntoIterator<Item = MediaId>) -> BpiResult<Self> {
269        let media_ids = media_ids.into_iter().collect::<Vec<_>>();
270        if media_ids.is_empty() {
271            return Err(BpiError::invalid_parameter(
272                "media_ids",
273                "at least one media id is required",
274            ));
275        }
276
277        Ok(Self { media_ids })
278    }
279
280    pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
281        vec![
282            (
283                "media_ids",
284                self.media_ids
285                    .iter()
286                    .map(ToString::to_string)
287                    .collect::<Vec<_>>()
288                    .join(","),
289            ),
290            ("csrf", csrf.to_string()),
291        ]
292    }
293}
294
295/// Parameters for copying or moving favorite resources.
296#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct FavResourceTransferParams {
298    src_media_id: MediaId,
299    tar_media_id: MediaId,
300    mid: Mid,
301    resources: String,
302}
303
304impl FavResourceTransferParams {
305    pub fn new(
306        src_media_id: MediaId,
307        tar_media_id: MediaId,
308        mid: Mid,
309        resources: impl Into<String>,
310    ) -> BpiResult<Self> {
311        Ok(Self {
312            src_media_id,
313            tar_media_id,
314            mid,
315            resources: normalize_non_blank("resources", resources.into())?,
316        })
317    }
318
319    pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
320        vec![
321            ("src_media_id", self.src_media_id.to_string()),
322            ("tar_media_id", self.tar_media_id.to_string()),
323            ("mid", self.mid.to_string()),
324            ("resources", self.resources.clone()),
325            ("platform", "web".to_string()),
326            ("csrf", csrf.to_string()),
327        ]
328    }
329}
330
331/// Parameters for deleting favorite resources in batches.
332#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct FavResourceBatchDeleteParams {
334    media_id: MediaId,
335    resources: String,
336}
337
338impl FavResourceBatchDeleteParams {
339    pub fn new(media_id: MediaId, resources: impl Into<String>) -> BpiResult<Self> {
340        Ok(Self {
341            media_id,
342            resources: normalize_non_blank("resources", resources.into())?,
343        })
344    }
345
346    pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
347        vec![
348            ("media_id", self.media_id.to_string()),
349            ("resources", self.resources.clone()),
350            ("platform", "web".to_string()),
351            ("csrf", csrf.to_string()),
352        ]
353    }
354}
355
356/// Parameters for cleaning invalid resources from a favorite folder.
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub struct FavResourceCleanParams {
359    media_id: MediaId,
360}
361
362impl FavResourceCleanParams {
363    pub fn new(media_id: MediaId) -> Self {
364        Self { media_id }
365    }
366
367    pub(crate) fn form_pairs(&self, csrf: &str) -> Vec<(&'static str, String)> {
368        vec![
369            ("media_id", self.media_id.to_string()),
370            ("csrf", csrf.to_string()),
371        ]
372    }
373}
374
375fn validate_positive_u32(field: &'static str, value: u32) -> BpiResult<u32> {
376    if value == 0 {
377        return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
378    }
379
380    Ok(value)
381}
382
383fn validate_positive_u64(field: &'static str, value: u64) -> BpiResult<u64> {
384    if value == 0 {
385        return Err(BpiError::invalid_parameter(field, "value must be non-zero"));
386    }
387
388    Ok(value)
389}
390
391fn normalize_non_blank(field: &'static str, value: String) -> BpiResult<String> {
392    let value = value.trim().to_string();
393    if value.is_empty() {
394        return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
395    }
396
397    Ok(value)
398}
399
400fn validate_privacy(value: u8) -> BpiResult<u8> {
401    if matches!(value, 0 | 1) {
402        return Ok(value);
403    }
404
405    Err(BpiError::invalid_parameter(
406        "privacy",
407        "value must be 0 or 1",
408    ))
409}
410
411fn push_optional(
412    pairs: &mut Vec<(&'static str, String)>,
413    field: &'static str,
414    value: &Option<String>,
415) {
416    if let Some(value) = value {
417        pairs.push((field, value.clone()));
418    }
419}
420
421fn push_optional_value<T>(
422    pairs: &mut Vec<(&'static str, String)>,
423    field: &'static str,
424    value: Option<T>,
425) where
426    T: ToString,
427{
428    if let Some(value) = value {
429        pairs.push((field, value.to_string()));
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn fav_folder_info_params_serializes_media_id() -> BpiResult<()> {
439        let params = FavFolderInfoParams::new(MediaId::new(1052622027)?);
440
441        assert_eq!(
442            params.query_pairs(),
443            vec![("media_id", "1052622027".to_string())]
444        );
445        Ok(())
446    }
447
448    #[test]
449    fn fav_created_list_params_serializes_defaults() -> BpiResult<()> {
450        let params = FavCreatedListParams::new(Mid::new(7792521)?);
451
452        assert_eq!(
453            params.query_pairs(),
454            vec![
455                ("up_mid", "7792521".to_string()),
456                ("web_location", "333.1387".to_string())
457            ]
458        );
459        Ok(())
460    }
461
462    #[test]
463    fn fav_created_list_params_serializes_optional_filters() -> BpiResult<()> {
464        let params = FavCreatedListParams::new(Mid::new(7792521)?)
465            .with_type(2)
466            .with_resource_id(170001)?
467            .with_web_location("333.999")?;
468
469        assert_eq!(
470            params.query_pairs(),
471            vec![
472                ("up_mid", "7792521".to_string()),
473                ("type", "2".to_string()),
474                ("rid", "170001".to_string()),
475                ("web_location", "333.999".to_string())
476            ]
477        );
478        Ok(())
479    }
480
481    #[test]
482    fn fav_created_list_params_rejects_zero_resource_id() -> BpiResult<()> {
483        let err = FavCreatedListParams::new(Mid::new(7792521)?)
484            .with_resource_id(0)
485            .unwrap_err();
486
487        assert!(matches!(
488            err,
489            BpiError::InvalidParameter { field: "rid", .. }
490        ));
491        Ok(())
492    }
493
494    #[test]
495    fn fav_collected_list_params_serializes_defaults() -> BpiResult<()> {
496        let params = FavCollectedListParams::new(Mid::new(7792521)?);
497
498        assert_eq!(
499            params.query_pairs(),
500            vec![
501                ("up_mid", "7792521".to_string()),
502                ("pn", "1".to_string()),
503                ("ps", "20".to_string()),
504                ("platform", "web".to_string())
505            ]
506        );
507        Ok(())
508    }
509
510    #[test]
511    fn fav_collected_list_params_serializes_pagination() -> BpiResult<()> {
512        let params = FavCollectedListParams::new(Mid::new(7792521)?)
513            .with_page(2)?
514            .with_page_size(30)?;
515
516        assert_eq!(
517            params.query_pairs(),
518            vec![
519                ("up_mid", "7792521".to_string()),
520                ("pn", "2".to_string()),
521                ("ps", "30".to_string()),
522                ("platform", "web".to_string())
523            ]
524        );
525        Ok(())
526    }
527
528    #[test]
529    fn fav_collected_list_params_rejects_zero_page() -> BpiResult<()> {
530        let err = FavCollectedListParams::new(Mid::new(7792521)?)
531            .with_page(0)
532            .unwrap_err();
533
534        assert!(matches!(
535            err,
536            BpiError::InvalidParameter { field: "pn", .. }
537        ));
538        Ok(())
539    }
540
541    #[test]
542    fn fav_resource_infos_params_serializes_defaults() -> BpiResult<()> {
543        let params = FavResourceInfosParams::new("371494037:2")?;
544
545        assert_eq!(
546            params.query_pairs(),
547            vec![
548                ("resources", "371494037:2".to_string()),
549                ("platform", "web".to_string())
550            ]
551        );
552        Ok(())
553    }
554
555    #[test]
556    fn fav_resource_infos_params_rejects_blank_resources() {
557        let err = FavResourceInfosParams::new("  ").unwrap_err();
558
559        assert!(matches!(
560            err,
561            BpiError::InvalidParameter {
562                field: "resources",
563                ..
564            }
565        ));
566    }
567
568    #[test]
569    fn fav_resource_ids_params_serializes_defaults() -> BpiResult<()> {
570        let params = FavResourceIdsParams::new(MediaId::new(1052622027)?);
571
572        assert_eq!(
573            params.query_pairs(),
574            vec![
575                ("media_id", "1052622027".to_string()),
576                ("platform", "web".to_string())
577            ]
578        );
579        Ok(())
580    }
581
582    #[test]
583    fn fav_folder_add_params_rejects_blank_title() {
584        let err = FavFolderAddParams::new(" ").unwrap_err();
585
586        assert!(matches!(
587            err,
588            BpiError::InvalidParameter { field: "title", .. }
589        ));
590    }
591
592    #[test]
593    fn fav_folder_edit_params_rejects_invalid_privacy() -> BpiResult<()> {
594        let err = FavFolderEditParams::new(MediaId::new(1052622027)?, "folder")?
595            .privacy(2)
596            .unwrap_err();
597
598        assert!(matches!(
599            err,
600            BpiError::InvalidParameter {
601                field: "privacy",
602                ..
603            }
604        ));
605        Ok(())
606    }
607
608    #[test]
609    fn fav_folder_delete_params_requires_media_ids() {
610        let err = FavFolderDeleteParams::new(Vec::<MediaId>::new()).unwrap_err();
611
612        assert!(matches!(
613            err,
614            BpiError::InvalidParameter {
615                field: "media_ids",
616                ..
617            }
618        ));
619    }
620
621    #[test]
622    fn fav_resource_transfer_params_rejects_blank_resources() -> BpiResult<()> {
623        let err =
624            FavResourceTransferParams::new(MediaId::new(1)?, MediaId::new(2)?, Mid::new(3)?, " ")
625                .unwrap_err();
626
627        assert!(matches!(
628            err,
629            BpiError::InvalidParameter {
630                field: "resources",
631                ..
632            }
633        ));
634        Ok(())
635    }
636}