1use crate::ids::{MediaId, Mid};
2use crate::{BpiError, BpiResult};
3
4#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}