1use crate::ids::Mid;
2use crate::{BpiError, BpiResult};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum UserCardPhoto {
7 Include,
9 Exclude,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct UserCardParams {
16 mid: Mid,
17 photo: Option<UserCardPhoto>,
18}
19
20impl UserCardParams {
21 pub fn new(mid: Mid) -> Self {
23 Self { mid, photo: None }
24 }
25
26 pub fn with_photo(mut self, photo: UserCardPhoto) -> Self {
28 self.photo = Some(photo);
29 self
30 }
31
32 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
33 let mut pairs = vec![("mid", self.mid.to_string())];
34
35 if let Some(photo) = self.photo {
36 let value = match photo {
37 UserCardPhoto::Include => "true",
38 UserCardPhoto::Exclude => "false",
39 };
40 pairs.push(("photo", value.to_string()));
41 }
42
43 pairs
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct UserCardsParams {
50 mids: Vec<Mid>,
51}
52
53impl UserCardsParams {
54 pub fn new<I>(mids: I) -> BpiResult<Self>
56 where
57 I: IntoIterator<Item = Mid>,
58 {
59 let mids = mids.into_iter().collect::<Vec<_>>();
60
61 if mids.is_empty() {
62 return Err(BpiError::invalid_parameter(
63 "uids",
64 "at least one user id is required",
65 ));
66 }
67
68 Ok(Self { mids })
69 }
70
71 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
72 vec![(
73 "uids",
74 self.mids
75 .iter()
76 .map(ToString::to_string)
77 .collect::<Vec<_>>()
78 .join(","),
79 )]
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct UserInfosParams {
86 mids: Vec<Mid>,
87}
88
89impl UserInfosParams {
90 pub fn new<I>(mids: I) -> BpiResult<Self>
92 where
93 I: IntoIterator<Item = Mid>,
94 {
95 let mids = mids.into_iter().collect::<Vec<_>>();
96
97 if mids.is_empty() {
98 return Err(BpiError::invalid_parameter(
99 "uids",
100 "at least one user id is required",
101 ));
102 }
103
104 Ok(Self { mids })
105 }
106
107 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
108 vec![(
109 "uids",
110 self.mids
111 .iter()
112 .map(ToString::to_string)
113 .collect::<Vec<_>>()
114 .join(","),
115 )]
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct UserSpaceParams {
122 mid: Mid,
123}
124
125impl UserSpaceParams {
126 pub fn new(mid: Mid) -> Self {
128 Self { mid }
129 }
130
131 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
132 vec![("mid", self.mid.to_string())]
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct UserSpaceNoticeParams {
139 mid: Mid,
140}
141
142impl UserSpaceNoticeParams {
143 pub fn new(mid: Mid) -> Self {
145 Self { mid }
146 }
147
148 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
149 vec![("mid", self.mid.to_string())]
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum UserBangumiFollowKind {
156 Bangumi,
158 Cinema,
160}
161
162impl UserBangumiFollowKind {
163 fn as_query_value(self) -> &'static str {
164 match self {
165 Self::Bangumi => "1",
166 Self::Cinema => "2",
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct UserBangumiFollowListParams {
174 mid: Mid,
175 kind: UserBangumiFollowKind,
176 page: u32,
177 page_size: u32,
178}
179
180impl UserBangumiFollowListParams {
181 pub fn new(mid: Mid) -> Self {
183 Self {
184 mid,
185 kind: UserBangumiFollowKind::Bangumi,
186 page: 1,
187 page_size: 15,
188 }
189 }
190
191 pub fn with_kind(mut self, kind: UserBangumiFollowKind) -> Self {
193 self.kind = kind;
194 self
195 }
196
197 pub fn with_page(mut self, page: u32) -> Self {
199 self.page = page;
200 self
201 }
202
203 pub fn with_page_size(mut self, page_size: u32) -> BpiResult<Self> {
205 if !(1..=30).contains(&page_size) {
206 return Err(BpiError::invalid_parameter(
207 "page_size",
208 "page size must be between 1 and 30",
209 ));
210 }
211
212 self.page_size = page_size;
213 Ok(self)
214 }
215
216 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
217 vec![
218 ("vmid", self.mid.to_string()),
219 ("type", self.kind.as_query_value().to_string()),
220 ("pn", self.page.to_string()),
221 ("ps", self.page_size.to_string()),
222 ]
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub struct UserRelationStatParams {
229 mid: Mid,
230}
231
232impl UserRelationStatParams {
233 pub fn new(mid: Mid) -> Self {
235 Self { mid }
236 }
237
238 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
239 vec![("vmid", self.mid.to_string())]
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct UserFollowingsParams {
246 mid: Mid,
247 order_type: Option<String>,
248 page_size: Option<u32>,
249 page: Option<u32>,
250}
251
252impl UserFollowingsParams {
253 pub fn new(mid: Mid) -> Self {
255 Self {
256 mid,
257 order_type: None,
258 page_size: None,
259 page: None,
260 }
261 }
262
263 pub fn with_order_type(mut self, order_type: impl Into<String>) -> Self {
265 let order_type = order_type.into();
266 if !order_type.trim().is_empty() {
267 self.order_type = Some(order_type);
268 }
269 self
270 }
271
272 pub fn with_page_size(mut self, page_size: u32) -> Self {
274 self.page_size = Some(page_size);
275 self
276 }
277
278 pub fn with_page(mut self, page: u32) -> Self {
280 self.page = Some(page);
281 self
282 }
283
284 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
285 let mut pairs = vec![("vmid", self.mid.to_string())];
286
287 if let Some(order_type) = &self.order_type {
288 pairs.push(("order_type", order_type.to_string()));
289 }
290 if let Some(page_size) = self.page_size {
291 pairs.push(("ps", page_size.to_string()));
292 }
293 if let Some(page) = self.page {
294 pairs.push(("pn", page.to_string()));
295 }
296
297 pairs
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
303pub struct UserFollowersParams {
304 mid: Mid,
305 page_size: Option<u32>,
306 page: Option<u32>,
307 offset: Option<String>,
308 last_access_ts: Option<u64>,
309 from: Option<String>,
310}
311
312impl UserFollowersParams {
313 pub fn new(mid: Mid) -> Self {
315 Self {
316 mid,
317 page_size: None,
318 page: None,
319 offset: None,
320 last_access_ts: None,
321 from: None,
322 }
323 }
324
325 pub fn with_page_size(mut self, page_size: u32) -> Self {
327 self.page_size = Some(page_size);
328 self
329 }
330
331 pub fn with_page(mut self, page: u32) -> Self {
333 self.page = Some(page);
334 self
335 }
336
337 pub fn with_offset(mut self, offset: impl Into<String>) -> Self {
339 let offset = offset.into();
340 if !offset.trim().is_empty() {
341 self.offset = Some(offset);
342 }
343 self
344 }
345
346 pub fn with_last_access_ts(mut self, last_access_ts: u64) -> Self {
348 self.last_access_ts = Some(last_access_ts);
349 self
350 }
351
352 pub fn with_from(mut self, from: impl Into<String>) -> Self {
354 let from = from.into();
355 if !from.trim().is_empty() {
356 self.from = Some(from);
357 }
358 self
359 }
360
361 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
362 let mut pairs = vec![("vmid", self.mid.to_string())];
363
364 if let Some(page_size) = self.page_size {
365 pairs.push(("ps", page_size.to_string()));
366 }
367 if let Some(page) = self.page {
368 pairs.push(("pn", page.to_string()));
369 }
370 if let Some(offset) = &self.offset {
371 pairs.push(("offset", offset.to_string()));
372 }
373 if let Some(last_access_ts) = self.last_access_ts {
374 pairs.push(("last_access_ts", last_access_ts.to_string()));
375 }
376 if let Some(from) = &self.from {
377 pairs.push(("from", from.to_string()));
378 }
379
380 pairs
381 }
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
386pub struct UserMedalWallParams {
387 target_id: Mid,
388}
389
390impl UserMedalWallParams {
391 pub fn new(target_id: Mid) -> Self {
393 Self { target_id }
394 }
395
396 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
397 vec![("target_id", self.target_id.to_string())]
398 }
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
403pub struct UserUpStatParams {
404 mid: Mid,
405}
406
407impl UserUpStatParams {
408 pub fn new(mid: Mid) -> Self {
410 Self { mid }
411 }
412
413 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
414 vec![("mid", self.mid.to_string())]
415 }
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub struct UserNavStatParams {
421 mid: Mid,
422}
423
424impl UserNavStatParams {
425 pub fn new(mid: Mid) -> Self {
427 Self { mid }
428 }
429
430 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
431 vec![("mid", self.mid.to_string())]
432 }
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub struct UserAlbumCountParams {
438 mid: Mid,
439}
440
441impl UserAlbumCountParams {
442 pub fn new(mid: Mid) -> Self {
444 Self { mid }
445 }
446
447 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
448 vec![("uid", self.mid.to_string())]
449 }
450}
451
452#[derive(Debug, Clone, PartialEq, Eq)]
454pub struct UserNameToUidParams {
455 names: Vec<String>,
456}
457
458impl UserNameToUidParams {
459 pub fn new<I, S>(names: I) -> BpiResult<Self>
461 where
462 I: IntoIterator<Item = S>,
463 S: Into<String>,
464 {
465 let names = names
466 .into_iter()
467 .map(Into::into)
468 .map(|name| name.trim().to_string())
469 .collect::<Vec<_>>();
470
471 if names.is_empty() {
472 return Err(BpiError::invalid_parameter(
473 "names",
474 "at least one name is required",
475 ));
476 }
477
478 if names.iter().any(|name| name.is_empty()) {
479 return Err(BpiError::invalid_parameter(
480 "names",
481 "names cannot contain blank values",
482 ));
483 }
484
485 Ok(Self { names })
486 }
487
488 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
489 vec![("names", self.names.join(","))]
490 }
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq)]
495pub enum UserUploadedVideoOrder {
496 Pubdate,
498 Click,
500 Stow,
502}
503
504impl UserUploadedVideoOrder {
505 fn as_query_value(self) -> &'static str {
506 match self {
507 Self::Pubdate => "pubdate",
508 Self::Click => "click",
509 Self::Stow => "stow",
510 }
511 }
512}
513
514impl TryFrom<&str> for UserUploadedVideoOrder {
515 type Error = BpiError;
516
517 fn try_from(value: &str) -> BpiResult<Self> {
518 match value.trim() {
519 "pubdate" => Ok(Self::Pubdate),
520 "click" => Ok(Self::Click),
521 "stow" => Ok(Self::Stow),
522 _ => Err(BpiError::invalid_parameter(
523 "order",
524 "uploaded video order must be pubdate, click, or stow",
525 )),
526 }
527 }
528}
529
530#[derive(Debug, Clone, PartialEq, Eq)]
532pub struct UserUploadedVideosParams {
533 mid: Mid,
534 order: UserUploadedVideoOrder,
535 tid: u64,
536 keyword: Option<String>,
537 page: u32,
538 page_size: u32,
539}
540
541impl UserUploadedVideosParams {
542 pub fn new(mid: Mid) -> Self {
544 Self {
545 mid,
546 order: UserUploadedVideoOrder::Pubdate,
547 tid: 0,
548 keyword: None,
549 page: 1,
550 page_size: 30,
551 }
552 }
553
554 pub fn with_order(mut self, order: UserUploadedVideoOrder) -> Self {
556 self.order = order;
557 self
558 }
559
560 pub fn with_tid(mut self, tid: u64) -> Self {
562 self.tid = tid;
563 self
564 }
565
566 pub fn with_keyword(mut self, keyword: impl Into<String>) -> Self {
568 let keyword = keyword.into().trim().to_string();
569 if !keyword.is_empty() {
570 self.keyword = Some(keyword);
571 }
572 self
573 }
574
575 pub fn with_page(mut self, page: u32) -> BpiResult<Self> {
577 if page == 0 {
578 return Err(BpiError::invalid_parameter(
579 "page",
580 "page number must be at least 1",
581 ));
582 }
583
584 self.page = page;
585 Ok(self)
586 }
587
588 pub fn with_page_size(mut self, page_size: u32) -> BpiResult<Self> {
590 if page_size == 0 {
591 return Err(BpiError::invalid_parameter(
592 "page_size",
593 "page size must be at least 1",
594 ));
595 }
596
597 self.page_size = page_size;
598 Ok(self)
599 }
600
601 pub(crate) fn query_pairs(&self) -> Vec<(&'static str, String)> {
602 let mut pairs = vec![
603 ("mid", self.mid.to_string()),
604 ("order", self.order.as_query_value().to_string()),
605 ("tid", self.tid.to_string()),
606 ("pn", self.page.to_string()),
607 ("ps", self.page_size.to_string()),
608 ];
609
610 if let Some(keyword) = &self.keyword {
611 pairs.push(("keyword", keyword.to_string()));
612 }
613
614 pairs
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn user_card_params_serializes_mid_query() -> Result<(), BpiError> {
624 let params = UserCardParams::new(Mid::new(1001)?);
625
626 assert_eq!(params.query_pairs(), vec![("mid", "1001".to_string())]);
627 Ok(())
628 }
629
630 #[test]
631 fn user_card_params_serializes_include_photo_query() -> Result<(), BpiError> {
632 let params = UserCardParams::new(Mid::new(1001)?).with_photo(UserCardPhoto::Include);
633
634 assert_eq!(
635 params.query_pairs(),
636 vec![("mid", "1001".to_string()), ("photo", "true".to_string())]
637 );
638 Ok(())
639 }
640
641 #[test]
642 fn user_card_params_serializes_exclude_photo_query() -> Result<(), BpiError> {
643 let params = UserCardParams::new(Mid::new(1001)?).with_photo(UserCardPhoto::Exclude);
644
645 assert_eq!(
646 params.query_pairs(),
647 vec![("mid", "1001".to_string()), ("photo", "false".to_string())]
648 );
649 Ok(())
650 }
651
652 #[test]
653 fn user_cards_params_serializes_uids_query() -> Result<(), BpiError> {
654 let params = UserCardsParams::new([Mid::new(1001)?, Mid::new(1002)?])?;
655
656 assert_eq!(
657 params.query_pairs(),
658 vec![("uids", "1001,1002".to_string())]
659 );
660 Ok(())
661 }
662
663 #[test]
664 fn user_cards_params_rejects_empty_uid_list() {
665 let err = UserCardsParams::new([]).unwrap_err();
666
667 assert!(matches!(
668 err,
669 BpiError::InvalidParameter { field: "uids", .. }
670 ));
671 }
672
673 #[test]
674 fn user_infos_params_serializes_uids_query() -> Result<(), BpiError> {
675 let params = UserInfosParams::new([Mid::new(1001)?, Mid::new(1002)?])?;
676
677 assert_eq!(
678 params.query_pairs(),
679 vec![("uids", "1001,1002".to_string())]
680 );
681 Ok(())
682 }
683
684 #[test]
685 fn user_infos_params_rejects_empty_uid_list() {
686 let err = UserInfosParams::new([]).unwrap_err();
687
688 assert!(matches!(
689 err,
690 BpiError::InvalidParameter { field: "uids", .. }
691 ));
692 }
693
694 #[test]
695 fn user_space_params_serializes_mid_query() -> Result<(), BpiError> {
696 let params = UserSpaceParams::new(Mid::new(1001)?);
697
698 assert_eq!(params.query_pairs(), vec![("mid", "1001".to_string())]);
699 Ok(())
700 }
701
702 #[test]
703 fn user_space_notice_params_serializes_mid_query() -> Result<(), BpiError> {
704 let params = UserSpaceNoticeParams::new(Mid::new(1001)?);
705
706 assert_eq!(params.query_pairs(), vec![("mid", "1001".to_string())]);
707 Ok(())
708 }
709
710 #[test]
711 fn user_bangumi_follow_list_params_serializes_default_query() -> Result<(), BpiError> {
712 let params = UserBangumiFollowListParams::new(Mid::new(1001)?);
713
714 assert_eq!(
715 params.query_pairs(),
716 vec![
717 ("vmid", "1001".to_string()),
718 ("type", "1".to_string()),
719 ("pn", "1".to_string()),
720 ("ps", "15".to_string()),
721 ]
722 );
723 Ok(())
724 }
725
726 #[test]
727 fn user_bangumi_follow_list_params_serializes_optional_filters() -> Result<(), BpiError> {
728 let params = UserBangumiFollowListParams::new(Mid::new(1001)?)
729 .with_kind(UserBangumiFollowKind::Cinema)
730 .with_page(2)
731 .with_page_size(30)?;
732
733 assert_eq!(
734 params.query_pairs(),
735 vec![
736 ("vmid", "1001".to_string()),
737 ("type", "2".to_string()),
738 ("pn", "2".to_string()),
739 ("ps", "30".to_string()),
740 ]
741 );
742 Ok(())
743 }
744
745 #[test]
746 fn user_bangumi_follow_list_params_rejects_zero_page_size() -> Result<(), BpiError> {
747 let err = UserBangumiFollowListParams::new(Mid::new(1001)?)
748 .with_page_size(0)
749 .unwrap_err();
750
751 assert!(matches!(
752 err,
753 BpiError::InvalidParameter {
754 field: "page_size",
755 ..
756 }
757 ));
758 Ok(())
759 }
760
761 #[test]
762 fn user_bangumi_follow_list_params_rejects_large_page_size() -> Result<(), BpiError> {
763 let err = UserBangumiFollowListParams::new(Mid::new(1001)?)
764 .with_page_size(31)
765 .unwrap_err();
766
767 assert!(matches!(
768 err,
769 BpiError::InvalidParameter {
770 field: "page_size",
771 ..
772 }
773 ));
774 Ok(())
775 }
776
777 #[test]
778 fn user_uploaded_videos_params_serializes_default_query() -> Result<(), BpiError> {
779 let params = UserUploadedVideosParams::new(Mid::new(1001)?);
780
781 assert_eq!(
782 params.query_pairs(),
783 vec![
784 ("mid", "1001".to_string()),
785 ("order", "pubdate".to_string()),
786 ("tid", "0".to_string()),
787 ("pn", "1".to_string()),
788 ("ps", "30".to_string()),
789 ]
790 );
791 Ok(())
792 }
793
794 #[test]
795 fn user_uploaded_video_order_parses_supported_values() -> Result<(), BpiError> {
796 assert_eq!(
797 UserUploadedVideoOrder::try_from("pubdate")?,
798 UserUploadedVideoOrder::Pubdate
799 );
800 assert_eq!(
801 UserUploadedVideoOrder::try_from("click")?,
802 UserUploadedVideoOrder::Click
803 );
804 assert_eq!(
805 UserUploadedVideoOrder::try_from("stow")?,
806 UserUploadedVideoOrder::Stow
807 );
808 Ok(())
809 }
810
811 #[test]
812 fn user_uploaded_video_order_rejects_unknown_value() {
813 let err = UserUploadedVideoOrder::try_from("invalid").unwrap_err();
814
815 assert!(matches!(
816 err,
817 BpiError::InvalidParameter { field: "order", .. }
818 ));
819 }
820
821 #[test]
822 fn user_uploaded_videos_params_serializes_optional_filters() -> Result<(), BpiError> {
823 let params = UserUploadedVideosParams::new(Mid::new(1001)?)
824 .with_order(UserUploadedVideoOrder::Click)
825 .with_tid(33)
826 .with_keyword("rust")
827 .with_page(2)?
828 .with_page_size(20)?;
829
830 assert_eq!(
831 params.query_pairs(),
832 vec![
833 ("mid", "1001".to_string()),
834 ("order", "click".to_string()),
835 ("tid", "33".to_string()),
836 ("pn", "2".to_string()),
837 ("ps", "20".to_string()),
838 ("keyword", "rust".to_string()),
839 ]
840 );
841 Ok(())
842 }
843
844 #[test]
845 fn user_uploaded_videos_params_trims_keyword() -> Result<(), BpiError> {
846 let params = UserUploadedVideosParams::new(Mid::new(1001)?).with_keyword(" rust ");
847
848 assert_eq!(
849 params.query_pairs(),
850 vec![
851 ("mid", "1001".to_string()),
852 ("order", "pubdate".to_string()),
853 ("tid", "0".to_string()),
854 ("pn", "1".to_string()),
855 ("ps", "30".to_string()),
856 ("keyword", "rust".to_string()),
857 ]
858 );
859 Ok(())
860 }
861
862 #[test]
863 fn user_uploaded_videos_params_ignores_blank_keyword() -> Result<(), BpiError> {
864 let params = UserUploadedVideosParams::new(Mid::new(1001)?).with_keyword(" ");
865
866 assert_eq!(
867 params.query_pairs(),
868 vec![
869 ("mid", "1001".to_string()),
870 ("order", "pubdate".to_string()),
871 ("tid", "0".to_string()),
872 ("pn", "1".to_string()),
873 ("ps", "30".to_string()),
874 ]
875 );
876 Ok(())
877 }
878
879 #[test]
880 fn user_uploaded_videos_params_rejects_zero_page() -> Result<(), BpiError> {
881 let err = UserUploadedVideosParams::new(Mid::new(1001)?)
882 .with_page(0)
883 .unwrap_err();
884
885 assert!(matches!(
886 err,
887 BpiError::InvalidParameter { field: "page", .. }
888 ));
889 Ok(())
890 }
891
892 #[test]
893 fn user_uploaded_videos_params_rejects_zero_page_size() -> Result<(), BpiError> {
894 let err = UserUploadedVideosParams::new(Mid::new(1001)?)
895 .with_page_size(0)
896 .unwrap_err();
897
898 assert!(matches!(
899 err,
900 BpiError::InvalidParameter {
901 field: "page_size",
902 ..
903 }
904 ));
905 Ok(())
906 }
907
908 #[test]
909 fn user_relation_stat_params_serializes_mid_query() -> Result<(), BpiError> {
910 let params = UserRelationStatParams::new(Mid::new(1001)?);
911
912 assert_eq!(params.query_pairs(), vec![("vmid", "1001".to_string())]);
913 Ok(())
914 }
915
916 #[test]
917 fn user_followings_params_serializes_default_query() -> Result<(), BpiError> {
918 let params = UserFollowingsParams::new(Mid::new(1001)?);
919
920 assert_eq!(params.query_pairs(), vec![("vmid", "1001".to_string())]);
921 Ok(())
922 }
923
924 #[test]
925 fn user_followings_params_serializes_optional_filters() -> Result<(), BpiError> {
926 let params = UserFollowingsParams::new(Mid::new(1001)?)
927 .with_order_type("attention")
928 .with_page_size(20)
929 .with_page(2);
930
931 assert_eq!(
932 params.query_pairs(),
933 vec![
934 ("vmid", "1001".to_string()),
935 ("order_type", "attention".to_string()),
936 ("ps", "20".to_string()),
937 ("pn", "2".to_string()),
938 ]
939 );
940 Ok(())
941 }
942
943 #[test]
944 fn user_followers_params_serializes_default_query() -> Result<(), BpiError> {
945 let params = UserFollowersParams::new(Mid::new(1001)?);
946
947 assert_eq!(params.query_pairs(), vec![("vmid", "1001".to_string())]);
948 Ok(())
949 }
950
951 #[test]
952 fn user_followers_params_serializes_optional_filters() -> Result<(), BpiError> {
953 let params = UserFollowersParams::new(Mid::new(1001)?)
954 .with_page_size(20)
955 .with_page(2)
956 .with_offset("next-offset")
957 .with_last_access_ts(1_700_000_000)
958 .with_from("main");
959
960 assert_eq!(
961 params.query_pairs(),
962 vec![
963 ("vmid", "1001".to_string()),
964 ("ps", "20".to_string()),
965 ("pn", "2".to_string()),
966 ("offset", "next-offset".to_string()),
967 ("last_access_ts", "1700000000".to_string()),
968 ("from", "main".to_string()),
969 ]
970 );
971 Ok(())
972 }
973
974 #[test]
975 fn user_medal_wall_params_serializes_target_id_query() -> Result<(), BpiError> {
976 let params = UserMedalWallParams::new(Mid::new(1001)?);
977
978 assert_eq!(
979 params.query_pairs(),
980 vec![("target_id", "1001".to_string())]
981 );
982 Ok(())
983 }
984
985 #[test]
986 fn user_up_stat_params_serializes_mid_query() -> Result<(), BpiError> {
987 let params = UserUpStatParams::new(Mid::new(1001)?);
988
989 assert_eq!(params.query_pairs(), vec![("mid", "1001".to_string())]);
990 Ok(())
991 }
992
993 #[test]
994 fn user_nav_stat_params_serializes_mid_query() -> Result<(), BpiError> {
995 let params = UserNavStatParams::new(Mid::new(1001)?);
996
997 assert_eq!(params.query_pairs(), vec![("mid", "1001".to_string())]);
998 Ok(())
999 }
1000
1001 #[test]
1002 fn user_album_count_params_serializes_uid_query() -> Result<(), BpiError> {
1003 let params = UserAlbumCountParams::new(Mid::new(1001)?);
1004
1005 assert_eq!(params.query_pairs(), vec![("uid", "1001".to_string())]);
1006 Ok(())
1007 }
1008
1009 #[test]
1010 fn user_name_to_uid_params_serializes_joined_names_query() -> Result<(), BpiError> {
1011 let params = UserNameToUidParams::new(["fixture_user", "another_user"])?;
1012
1013 assert_eq!(
1014 params.query_pairs(),
1015 vec![("names", "fixture_user,another_user".to_string())]
1016 );
1017 Ok(())
1018 }
1019
1020 #[test]
1021 fn user_name_to_uid_params_rejects_empty_names() {
1022 let err = UserNameToUidParams::new(Vec::<&str>::new()).unwrap_err();
1023
1024 assert!(matches!(
1025 err,
1026 BpiError::InvalidParameter { field: "names", .. }
1027 ));
1028 }
1029
1030 #[test]
1031 fn user_name_to_uid_params_rejects_blank_name() {
1032 let err = UserNameToUidParams::new(["fixture_user", " "]).unwrap_err();
1033
1034 assert!(matches!(
1035 err,
1036 BpiError::InvalidParameter { field: "names", .. }
1037 ));
1038 }
1039}