1#[cfg(all(not(feature = "std"), feature = "alloc"))]
23use alloc::{string::String, vec::Vec};
24#[cfg(feature = "serde")]
25use serde::{Deserialize, Serialize};
26#[cfg(feature = "validator")]
27use validator::Validate;
28
29#[cfg(any(feature = "std", feature = "alloc"))]
50#[derive(Debug, Clone, PartialEq)]
51#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
52#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
53#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
54#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
55#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
56pub struct PaginatedResponse<T> {
57 pub items: Vec<T>,
59 pub total_count: u64,
61 pub has_more: bool,
63 pub limit: u64,
65 pub offset: u64,
67}
68
69#[cfg(any(feature = "std", feature = "alloc"))]
70impl<T> PaginatedResponse<T> {
71 #[must_use]
91 pub fn new(items: Vec<T>, total_count: u64, params: &PaginationParams) -> Self {
92 let limit = params.limit();
93 let offset = params.offset();
94 let has_more = offset + (items.len() as u64) < total_count;
95 Self {
96 items,
97 total_count,
98 has_more,
99 limit,
100 offset,
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
123#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
124#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema, utoipa::IntoParams))]
125#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
126#[cfg_attr(feature = "validator", derive(Validate))]
127#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
128pub struct PaginationParams {
129 #[cfg_attr(feature = "serde", serde(default))]
131 #[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
132 #[cfg_attr(
133 feature = "proptest",
134 proptest(strategy = "proptest::option::of(1u64..=100u64)")
135 )]
136 pub limit: Option<u64>,
137 #[cfg_attr(feature = "serde", serde(default))]
139 pub offset: Option<u64>,
140}
141
142impl Default for PaginationParams {
143 fn default() -> Self {
144 Self {
145 limit: Some(20),
146 offset: Some(0),
147 }
148 }
149}
150
151#[cfg(any(feature = "std", feature = "alloc"))]
152impl PaginationParams {
153 pub fn new(limit: u64, offset: u64) -> Result<Self, crate::error::ValidationError> {
170 if !(1..=100).contains(&limit) {
171 return Err(crate::error::ValidationError {
172 field: "/limit".into(),
173 message: "must be between 1 and 100".into(),
174 rule: Some("range".into()),
175 });
176 }
177 Ok(Self {
178 limit: Some(limit),
179 offset: Some(offset),
180 })
181 }
182}
183
184impl PaginationParams {
185 #[must_use]
199 pub fn limit(&self) -> u64 {
200 self.limit.unwrap_or(20)
201 }
202
203 #[must_use]
217 pub fn offset(&self) -> u64 {
218 self.offset.unwrap_or(0)
219 }
220}
221
222#[cfg(any(feature = "std", feature = "alloc"))]
236#[derive(Debug, Clone, PartialEq)]
237#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
238#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
239#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
240#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
241#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
242pub struct CursorPaginatedResponse<T> {
243 pub data: Vec<T>,
245 pub pagination: CursorPagination,
247}
248
249#[cfg(any(feature = "std", feature = "alloc"))]
250impl<T> CursorPaginatedResponse<T> {
251 #[must_use]
266 pub fn new(data: Vec<T>, pagination: CursorPagination) -> Self {
267 Self { data, pagination }
268 }
269}
270
271#[cfg(any(feature = "std", feature = "alloc"))]
277#[derive(Debug, Clone, PartialEq, Eq)]
278#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
279#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
280#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
281#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
282#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
283pub struct CursorPagination {
284 pub has_more: bool,
286 #[cfg_attr(
288 feature = "serde",
289 serde(default, skip_serializing_if = "Option::is_none")
290 )]
291 pub next_cursor: Option<String>,
292}
293
294#[cfg(any(feature = "std", feature = "alloc"))]
295impl CursorPagination {
296 #[must_use]
308 pub fn more(cursor: impl Into<String>) -> Self {
309 Self {
310 has_more: true,
311 next_cursor: Some(cursor.into()),
312 }
313 }
314
315 #[must_use]
327 pub fn last_page() -> Self {
328 Self {
329 has_more: false,
330 next_cursor: None,
331 }
332 }
333}
334
335#[cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))]
336#[allow(clippy::unnecessary_wraps)]
337fn default_cursor_limit() -> Option<u64> {
338 Some(20)
339}
340
341#[cfg(any(feature = "std", feature = "alloc"))]
348#[derive(Debug, Clone, PartialEq, Eq)]
349#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
350#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema, utoipa::IntoParams))]
351#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
352#[cfg_attr(feature = "validator", derive(Validate))]
353#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
354pub struct CursorPaginationParams {
355 #[cfg_attr(feature = "serde", serde(default = "default_cursor_limit"))]
357 #[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
358 #[cfg_attr(
359 feature = "proptest",
360 proptest(strategy = "proptest::option::of(1u64..=100u64)")
361 )]
362 pub limit: Option<u64>,
363 #[cfg_attr(
365 feature = "serde",
366 serde(default, skip_serializing_if = "Option::is_none")
367 )]
368 pub after: Option<String>,
369}
370
371#[cfg(any(feature = "std", feature = "alloc"))]
372impl Default for CursorPaginationParams {
373 fn default() -> Self {
374 Self {
375 limit: Some(20),
376 after: None,
377 }
378 }
379}
380
381#[cfg(any(feature = "std", feature = "alloc"))]
382impl CursorPaginationParams {
383 pub fn new(limit: u64, after: Option<String>) -> Result<Self, crate::error::ValidationError> {
399 if !(1..=100).contains(&limit) {
400 return Err(crate::error::ValidationError {
401 field: "/limit".into(),
402 message: "must be between 1 and 100".into(),
403 rule: Some("range".into()),
404 });
405 }
406 Ok(Self {
407 limit: Some(limit),
408 after,
409 })
410 }
411
412 #[must_use]
426 pub fn limit(&self) -> u64 {
427 self.limit.unwrap_or(20)
428 }
429
430 #[must_use]
432 pub fn after(&self) -> Option<&str> {
433 self.after.as_deref()
434 }
435}
436
437#[cfg(any(feature = "std", feature = "alloc"))]
455#[derive(Debug, Clone, PartialEq, Eq)]
456#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
457#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
458#[cfg_attr(feature = "schemars", schemars(bound = "K: schemars::JsonSchema"))]
459#[cfg_attr(feature = "validator", derive(Validate))]
460pub struct KeysetPaginationParams<K> {
461 #[cfg_attr(
463 feature = "serde",
464 serde(default, skip_serializing_if = "Option::is_none")
465 )]
466 pub after: Option<K>,
467 #[cfg_attr(
469 feature = "serde",
470 serde(default, skip_serializing_if = "Option::is_none")
471 )]
472 pub before: Option<K>,
473 #[cfg_attr(feature = "serde", serde(default = "default_keyset_limit"))]
475 #[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
476 pub limit: Option<u64>,
477}
478
479#[cfg(any(feature = "std", feature = "alloc"))]
480impl<K> Default for KeysetPaginationParams<K> {
481 fn default() -> Self {
482 Self {
483 after: None,
484 before: None,
485 limit: Some(20),
486 }
487 }
488}
489
490#[cfg(any(feature = "std", feature = "alloc"))]
491impl<K> KeysetPaginationParams<K> {
492 pub fn new(
508 limit: u64,
509 after: Option<K>,
510 before: Option<K>,
511 ) -> Result<Self, crate::error::ValidationError> {
512 if !(1..=100).contains(&limit) {
513 return Err(crate::error::ValidationError {
514 field: "/limit".into(),
515 message: "must be between 1 and 100".into(),
516 rule: Some("range".into()),
517 });
518 }
519 Ok(Self {
520 after,
521 before,
522 limit: Some(limit),
523 })
524 }
525
526 #[must_use]
537 pub fn limit(&self) -> u64 {
538 self.limit.unwrap_or(20)
539 }
540}
541
542#[cfg(all(feature = "serde", any(feature = "std", feature = "alloc")))]
543#[allow(clippy::unnecessary_wraps)]
544fn default_keyset_limit() -> Option<u64> {
545 Some(20)
546}
547
548#[cfg(any(feature = "std", feature = "alloc"))]
556#[derive(Debug, Clone, PartialEq)]
557#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
558#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
559#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
560pub struct KeysetPaginatedResponse<T> {
561 pub items: Vec<T>,
563 pub has_next: bool,
565 pub has_prev: bool,
567 #[cfg_attr(
571 feature = "serde",
572 serde(default, skip_serializing_if = "Option::is_none")
573 )]
574 pub prev_cursor: Option<String>,
575 #[cfg_attr(
579 feature = "serde",
580 serde(default, skip_serializing_if = "Option::is_none")
581 )]
582 pub next_cursor: Option<String>,
583}
584
585#[cfg(any(feature = "std", feature = "alloc"))]
586impl<T> KeysetPaginatedResponse<T> {
587 #[must_use]
605 pub fn new(
606 items: Vec<T>,
607 has_next: bool,
608 has_prev: bool,
609 prev_cursor: Option<String>,
610 next_cursor: Option<String>,
611 ) -> Self {
612 Self {
613 items,
614 has_next,
615 has_prev,
616 prev_cursor,
617 next_cursor,
618 }
619 }
620
621 #[must_use]
637 pub fn first_page(items: Vec<T>, has_next: bool, next_cursor: Option<String>) -> Self {
638 Self::new(items, has_next, false, None, next_cursor)
639 }
640}
641
642#[cfg(feature = "axum")]
647#[allow(clippy::result_large_err)]
648mod axum_extractors {
649 use super::{CursorPaginationParams, PaginationParams};
650 use crate::error::ApiError;
651 use axum::extract::{FromRequestParts, Query};
652 use axum::http::request::Parts;
653 #[cfg(feature = "validator")]
654 use validator::Validate;
655
656 impl<S: Send + Sync> FromRequestParts<S> for PaginationParams {
657 type Rejection = ApiError;
658
659 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
660 let Query(params) = Query::<Self>::from_request_parts(parts, state)
661 .await
662 .map_err(|e| ApiError::bad_request(e.to_string()))?;
663 #[cfg(feature = "validator")]
664 params
665 .validate()
666 .map_err(|e| ApiError::bad_request(e.to_string()))?;
667 Ok(params)
668 }
669 }
670
671 impl<S: Send + Sync> FromRequestParts<S> for CursorPaginationParams {
672 type Rejection = ApiError;
673
674 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
675 let Query(params) = Query::<Self>::from_request_parts(parts, state)
676 .await
677 .map_err(|e| ApiError::bad_request(e.to_string()))?;
678 #[cfg(feature = "validator")]
679 params
680 .validate()
681 .map_err(|e| ApiError::bad_request(e.to_string()))?;
682 Ok(params)
683 }
684 }
685}
686
687#[cfg(feature = "arbitrary")]
692impl<'a> arbitrary::Arbitrary<'a> for PaginationParams {
693 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
694 use arbitrary::Arbitrary;
695 let limit = if bool::arbitrary(u)? {
697 Some(u.int_in_range(1u64..=100)?)
698 } else {
699 None
700 };
701 Ok(Self {
702 limit,
703 offset: Arbitrary::arbitrary(u)?,
704 })
705 }
706}
707
708#[cfg(all(feature = "arbitrary", any(feature = "std", feature = "alloc")))]
709impl<'a> arbitrary::Arbitrary<'a> for CursorPaginationParams {
710 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
711 use arbitrary::Arbitrary;
712 let limit = if bool::arbitrary(u)? {
713 Some(u.int_in_range(1u64..=100)?)
714 } else {
715 None
716 };
717 Ok(Self {
718 limit,
719 after: Arbitrary::arbitrary(u)?,
720 })
721 }
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727
728 #[test]
733 fn paginated_response_has_more_true() {
734 let params = PaginationParams::default();
735 let resp = PaginatedResponse::new(vec![1i32; 20], 25, ¶ms);
736 assert!(resp.has_more);
737 assert_eq!(resp.total_count, 25);
738 assert_eq!(resp.limit, 20);
739 assert_eq!(resp.offset, 0);
740 }
741
742 #[test]
743 fn paginated_response_has_more_false() {
744 let params = PaginationParams::default();
745 let resp = PaginatedResponse::new(vec![1i32; 5], 5, ¶ms);
746 assert!(!resp.has_more);
747 }
748
749 #[test]
750 fn paginated_response_exact_last_page_boundary() {
751 let params = PaginationParams {
753 limit: Some(20),
754 offset: Some(20),
755 };
756 let resp = PaginatedResponse::new(vec![1i32; 5], 25, ¶ms);
757 assert!(!resp.has_more);
758 }
759
760 #[test]
761 fn paginated_response_second_page_has_more() {
762 let params = PaginationParams {
763 limit: Some(10),
764 offset: Some(10),
765 };
766 let resp = PaginatedResponse::new(vec![1i32; 10], 50, ¶ms);
767 assert!(resp.has_more);
768 }
769
770 #[test]
775 fn pagination_params_defaults() {
776 let p = PaginationParams::default();
777 assert_eq!(p.limit(), 20);
778 assert_eq!(p.offset(), 0);
779 }
780
781 #[test]
782 fn pagination_params_none_falls_back_to_defaults() {
783 let p = PaginationParams {
784 limit: None,
785 offset: None,
786 };
787 assert_eq!(p.limit(), 20);
788 assert_eq!(p.offset(), 0);
789 }
790
791 #[test]
792 fn pagination_params_custom_values() {
793 let p = PaginationParams {
794 limit: Some(50),
795 offset: Some(100),
796 };
797 assert_eq!(p.limit(), 50);
798 assert_eq!(p.offset(), 100);
799 }
800
801 #[cfg(feature = "validator")]
806 #[test]
807 fn pagination_params_validate_min_limit() {
808 use validator::Validate;
809 let p = PaginationParams {
810 limit: Some(0),
811 offset: Some(0),
812 };
813 assert!(p.validate().is_err());
814 }
815
816 #[cfg(feature = "validator")]
817 #[test]
818 fn pagination_params_validate_max_limit() {
819 use validator::Validate;
820 let p = PaginationParams {
821 limit: Some(101),
822 offset: Some(0),
823 };
824 assert!(p.validate().is_err());
825 }
826
827 #[cfg(feature = "validator")]
828 #[test]
829 fn pagination_params_validate_boundary_values() {
830 use validator::Validate;
831 let min = PaginationParams {
832 limit: Some(1),
833 offset: Some(0),
834 };
835 assert!(min.validate().is_ok());
836 let max = PaginationParams {
837 limit: Some(100),
838 offset: Some(0),
839 };
840 assert!(max.validate().is_ok());
841 }
842
843 #[cfg(feature = "validator")]
844 #[test]
845 fn pagination_params_validate_none_limit_uses_default() {
846 use validator::Validate;
847 let p = PaginationParams {
849 limit: None,
850 offset: None,
851 };
852 assert!(p.validate().is_ok());
853 }
854
855 #[test]
860 fn pagination_params_new_valid() {
861 let p = PaginationParams::new(1, 0).unwrap();
862 assert_eq!(p.limit(), 1);
863 assert_eq!(p.offset(), 0);
864
865 let p = PaginationParams::new(100, 500).unwrap();
866 assert_eq!(p.limit(), 100);
867 assert_eq!(p.offset(), 500);
868 }
869
870 #[test]
871 fn pagination_params_new_limit_zero_fails() {
872 let err = PaginationParams::new(0, 0).unwrap_err();
873 assert_eq!(err.field, "/limit");
874 assert_eq!(err.rule.as_deref(), Some("range"));
875 }
876
877 #[test]
878 fn pagination_params_new_limit_101_fails() {
879 let err = PaginationParams::new(101, 0).unwrap_err();
880 assert_eq!(err.field, "/limit");
881 }
882
883 #[test]
888 fn cursor_pagination_params_new_valid() {
889 let p = CursorPaginationParams::new(1, None).unwrap();
890 assert_eq!(p.limit(), 1);
891
892 let p = CursorPaginationParams::new(100, Some("tok".to_string())).unwrap();
893 assert_eq!(p.limit(), 100);
894 assert_eq!(p.after(), Some("tok"));
895 }
896
897 #[test]
898 fn cursor_pagination_params_new_limit_zero_fails() {
899 assert!(CursorPaginationParams::new(0, None).is_err());
900 }
901
902 #[test]
903 fn cursor_pagination_params_new_limit_101_fails() {
904 let err = CursorPaginationParams::new(101, None).unwrap_err();
905 assert_eq!(err.field, "/limit");
906 }
907
908 #[test]
913 fn keyset_pagination_params_new_valid() {
914 let p = KeysetPaginationParams::<u64>::new(10, Some(5), None).unwrap();
915 assert_eq!(p.limit(), 10);
916 assert_eq!(p.after, Some(5));
917 assert!(p.before.is_none());
918 }
919
920 #[test]
921 fn keyset_pagination_params_new_limit_zero_fails() {
922 assert!(KeysetPaginationParams::<u64>::new(0, None, None).is_err());
923 }
924
925 #[test]
926 fn keyset_pagination_params_new_limit_101_fails() {
927 let err = KeysetPaginationParams::<u64>::new(101, None, None).unwrap_err();
928 assert_eq!(err.field, "/limit");
929 }
930
931 #[test]
936 fn cursor_pagination_more() {
937 let c = CursorPagination::more("abc123");
938 assert!(c.has_more);
939 assert_eq!(c.next_cursor.as_deref(), Some("abc123"));
940 }
941
942 #[test]
943 fn cursor_pagination_last() {
944 let c = CursorPagination::last_page();
945 assert!(!c.has_more);
946 assert!(c.next_cursor.is_none());
947 }
948
949 #[test]
950 fn cursor_paginated_response_new() {
951 let resp = CursorPaginatedResponse::new(vec!["a", "b"], CursorPagination::more("next"));
952 assert_eq!(resp.data.len(), 2);
953 assert!(resp.pagination.has_more);
954 }
955
956 #[cfg(feature = "serde")]
961 #[test]
962 fn paginated_response_serde_round_trip() {
963 let params = PaginationParams {
964 limit: Some(10),
965 offset: Some(20),
966 };
967 let resp = PaginatedResponse::new(vec![1i32, 2, 3], 50, ¶ms);
968 let json = serde_json::to_value(&resp).unwrap();
969 assert_eq!(json["total_count"], 50);
970 assert_eq!(json["has_more"], true);
971 assert_eq!(json["limit"], 10);
972 assert_eq!(json["offset"], 20);
973 assert_eq!(json["items"], serde_json::json!([1, 2, 3]));
974
975 let back: PaginatedResponse<i32> = serde_json::from_value(json).unwrap();
976 assert_eq!(back, resp);
977 }
978
979 #[cfg(feature = "serde")]
980 #[test]
981 fn snapshot_offset_paginated_response() {
982 let params = PaginationParams {
983 limit: Some(20),
984 offset: Some(0),
985 };
986 let resp = PaginatedResponse::new(vec![1i32, 2, 3], 25, ¶ms);
987 let json = serde_json::to_value(&resp).unwrap();
988 let expected = serde_json::json!({
989 "items": [1, 2, 3],
990 "total_count": 25,
991 "has_more": true,
992 "limit": 20,
993 "offset": 0
994 });
995 assert_eq!(json, expected);
996 }
997
998 #[cfg(feature = "serde")]
999 #[test]
1000 fn pagination_params_serde_defaults() {
1001 let json = serde_json::json!({});
1002 let p: PaginationParams = serde_json::from_value(json).unwrap();
1003 assert_eq!(p.limit(), 20);
1004 assert_eq!(p.offset(), 0);
1005 }
1006
1007 #[cfg(feature = "serde")]
1008 #[test]
1009 fn pagination_params_serde_custom() {
1010 let json = serde_json::json!({"limit": 50, "offset": 100});
1011 let p: PaginationParams = serde_json::from_value(json).unwrap();
1012 assert_eq!(p.limit(), 50);
1013 assert_eq!(p.offset(), 100);
1014 }
1015
1016 #[cfg(feature = "serde")]
1017 #[test]
1018 fn cursor_response_serde_omits_null_cursor() {
1019 let resp = CursorPaginatedResponse::new(vec!["x"], CursorPagination::last_page());
1020 let json = serde_json::to_value(&resp).unwrap();
1021 assert!(json["pagination"].get("next_cursor").is_none());
1022 }
1023
1024 #[cfg(feature = "serde")]
1025 #[test]
1026 fn cursor_response_serde_includes_cursor() {
1027 let resp = CursorPaginatedResponse::new(vec!["x"], CursorPagination::more("eyJpZCI6NDJ9"));
1028 let json = serde_json::to_value(&resp).unwrap();
1029 assert_eq!(json["pagination"]["next_cursor"], "eyJpZCI6NDJ9");
1030 }
1031
1032 #[cfg(feature = "serde")]
1033 #[test]
1034 fn snapshot_cursor_paginated_response() {
1035 let resp =
1036 CursorPaginatedResponse::new(vec!["a", "b"], CursorPagination::more("eyJpZCI6NDJ9"));
1037 let json = serde_json::to_value(&resp).unwrap();
1038 let expected = serde_json::json!({
1039 "data": ["a", "b"],
1040 "pagination": {
1041 "has_more": true,
1042 "next_cursor": "eyJpZCI6NDJ9"
1043 }
1044 });
1045 assert_eq!(json, expected);
1046 }
1047
1048 #[test]
1053 fn cursor_pagination_params_defaults() {
1054 let p = CursorPaginationParams::default();
1055 assert_eq!(p.limit(), 20);
1056 assert!(p.after().is_none());
1057 }
1058
1059 #[test]
1060 fn cursor_pagination_params_none_falls_back_to_defaults() {
1061 let p = CursorPaginationParams {
1062 limit: None,
1063 after: None,
1064 };
1065 assert_eq!(p.limit(), 20);
1066 assert!(p.after().is_none());
1067 }
1068
1069 #[test]
1070 fn cursor_pagination_params_custom_values() {
1071 let p = CursorPaginationParams {
1072 limit: Some(50),
1073 after: Some("eyJpZCI6NDJ9".to_string()),
1074 };
1075 assert_eq!(p.limit(), 50);
1076 assert_eq!(p.after(), Some("eyJpZCI6NDJ9"));
1077 }
1078
1079 #[cfg(feature = "validator")]
1084 #[test]
1085 fn cursor_pagination_params_validate_min_limit() {
1086 use validator::Validate;
1087 let p = CursorPaginationParams {
1088 limit: Some(0),
1089 after: None,
1090 };
1091 assert!(p.validate().is_err());
1092 }
1093
1094 #[cfg(feature = "validator")]
1095 #[test]
1096 fn cursor_pagination_params_validate_max_limit() {
1097 use validator::Validate;
1098 let p = CursorPaginationParams {
1099 limit: Some(101),
1100 after: None,
1101 };
1102 assert!(p.validate().is_err());
1103 }
1104
1105 #[cfg(feature = "validator")]
1106 #[test]
1107 fn cursor_pagination_params_validate_boundary_values() {
1108 use validator::Validate;
1109 let min = CursorPaginationParams {
1110 limit: Some(1),
1111 after: None,
1112 };
1113 assert!(min.validate().is_ok());
1114 let max = CursorPaginationParams {
1115 limit: Some(100),
1116 after: None,
1117 };
1118 assert!(max.validate().is_ok());
1119 }
1120
1121 #[cfg(feature = "serde")]
1126 #[test]
1127 fn cursor_pagination_params_serde_defaults() {
1128 let json = serde_json::json!({});
1129 let p: CursorPaginationParams = serde_json::from_value(json).unwrap();
1130 assert_eq!(p.limit(), 20);
1131 assert!(p.after().is_none());
1132 }
1133
1134 #[cfg(feature = "serde")]
1135 #[test]
1136 fn cursor_pagination_params_serde_custom() {
1137 let json = serde_json::json!({"limit": 50, "after": "eyJpZCI6NDJ9"});
1138 let p: CursorPaginationParams = serde_json::from_value(json).unwrap();
1139 assert_eq!(p.limit(), 50);
1140 assert_eq!(p.after(), Some("eyJpZCI6NDJ9"));
1141 }
1142
1143 #[cfg(feature = "schemars")]
1144 #[test]
1145 fn pagination_params_schema_is_valid() {
1146 let schema = schemars::schema_for!(PaginationParams);
1147 let json = serde_json::to_value(&schema).expect("schema serializable");
1148 assert!(json.is_object());
1149 }
1150
1151 #[cfg(all(feature = "schemars", any(feature = "std", feature = "alloc")))]
1152 #[test]
1153 fn cursor_pagination_schema_is_valid() {
1154 let schema = schemars::schema_for!(CursorPagination);
1155 let json = serde_json::to_value(&schema).expect("schema serializable");
1156 assert!(json.is_object());
1157 }
1158
1159 #[cfg(feature = "axum")]
1160 mod axum_extractor_tests {
1161 use super::super::{CursorPaginationParams, PaginationParams};
1162 use axum::extract::FromRequestParts;
1163 use axum::http::Request;
1164
1165 async fn extract_offset(q: &str) -> Result<PaginationParams, u16> {
1166 let req = Request::builder().uri(format!("/?{q}")).body(()).unwrap();
1167 let (mut parts, ()) = req.into_parts();
1168 PaginationParams::from_request_parts(&mut parts, &())
1169 .await
1170 .map_err(|e| e.status)
1171 }
1172
1173 async fn extract_cursor(q: &str) -> Result<CursorPaginationParams, u16> {
1174 let req = Request::builder().uri(format!("/?{q}")).body(()).unwrap();
1175 let (mut parts, ()) = req.into_parts();
1176 CursorPaginationParams::from_request_parts(&mut parts, &())
1177 .await
1178 .map_err(|e| e.status)
1179 }
1180
1181 #[tokio::test]
1182 async fn default_params() {
1183 let p = extract_offset("").await.unwrap();
1184 assert_eq!(p.limit(), 20);
1185 assert_eq!(p.offset(), 0);
1186 }
1187
1188 #[tokio::test]
1189 async fn custom_params() {
1190 let p = extract_offset("limit=50&offset=100").await.unwrap();
1191 assert_eq!(p.limit(), 50);
1192 assert_eq!(p.offset(), 100);
1193 }
1194
1195 #[cfg(feature = "validator")]
1196 #[tokio::test]
1197 async fn limit_zero_rejected() {
1198 assert_eq!(extract_offset("limit=0").await.unwrap_err(), 400);
1199 }
1200
1201 #[cfg(feature = "validator")]
1202 #[tokio::test]
1203 async fn limit_101_rejected() {
1204 assert_eq!(extract_offset("limit=101").await.unwrap_err(), 400);
1205 }
1206
1207 #[tokio::test]
1208 async fn cursor_default() {
1209 let p = extract_cursor("").await.unwrap();
1210 assert_eq!(p.limit(), 20);
1211 assert!(p.after().is_none());
1212 }
1213
1214 #[tokio::test]
1215 async fn cursor_custom() {
1216 let p = extract_cursor("limit=10&after=abc").await.unwrap();
1217 assert_eq!(p.limit(), 10);
1218 assert_eq!(p.after(), Some("abc"));
1219 }
1220
1221 #[cfg(feature = "validator")]
1222 #[tokio::test]
1223 async fn cursor_limit_101_rejected() {
1224 assert_eq!(extract_cursor("limit=101").await.unwrap_err(), 400);
1225 }
1226
1227 #[tokio::test]
1228 async fn offset_invalid_query_type_rejected() {
1229 assert_eq!(extract_offset("limit=abc").await.unwrap_err(), 400);
1231 }
1232
1233 #[tokio::test]
1234 async fn cursor_invalid_query_type_rejected() {
1235 assert_eq!(extract_cursor("limit=abc").await.unwrap_err(), 400);
1236 }
1237 }
1238
1239 #[cfg(all(feature = "schemars", any(feature = "std", feature = "alloc")))]
1240 #[test]
1241 fn paginated_response_schema_is_valid() {
1242 let schema = schemars::schema_for!(PaginatedResponse<String>);
1243 let json = serde_json::to_value(&schema).expect("schema serializable");
1244 assert!(json.is_object());
1245 }
1246
1247 #[test]
1252 fn keyset_params_default() {
1253 let p = KeysetPaginationParams::<String>::default();
1254 assert_eq!(p.limit(), 20);
1255 assert!(p.after.is_none());
1256 assert!(p.before.is_none());
1257 }
1258
1259 #[test]
1260 fn keyset_params_limit_none_falls_back() {
1261 let p = KeysetPaginationParams::<u64> {
1262 after: None,
1263 before: None,
1264 limit: None,
1265 };
1266 assert_eq!(p.limit(), 20);
1267 }
1268
1269 #[test]
1270 fn keyset_params_custom_values() {
1271 let p = KeysetPaginationParams::<u64> {
1272 after: Some(10),
1273 before: Some(1),
1274 limit: Some(50),
1275 };
1276 assert_eq!(p.limit(), 50);
1277 assert_eq!(p.after, Some(10));
1278 assert_eq!(p.before, Some(1));
1279 }
1280
1281 #[test]
1286 fn keyset_paginated_response_new() {
1287 let resp = KeysetPaginatedResponse::new(
1288 vec![1i32, 2, 3],
1289 true,
1290 false,
1291 None,
1292 Some("cursor_after_3".to_string()),
1293 );
1294 assert_eq!(resp.items, vec![1, 2, 3]);
1295 assert!(resp.has_next);
1296 assert!(!resp.has_prev);
1297 assert!(resp.prev_cursor.is_none());
1298 assert_eq!(resp.next_cursor.as_deref(), Some("cursor_after_3"));
1299 }
1300
1301 #[test]
1302 fn keyset_paginated_response_first_page() {
1303 let resp = KeysetPaginatedResponse::first_page(
1304 vec!["a", "b"],
1305 true,
1306 Some("cursor_after_b".to_string()),
1307 );
1308 assert!(!resp.has_prev);
1309 assert!(resp.has_next);
1310 assert!(resp.prev_cursor.is_none());
1311 assert_eq!(resp.next_cursor.as_deref(), Some("cursor_after_b"));
1312 }
1313
1314 #[test]
1315 fn keyset_paginated_response_last_page() {
1316 let resp = KeysetPaginatedResponse::first_page(vec![1i32], false, None);
1317 assert!(!resp.has_next);
1318 assert!(!resp.has_prev);
1319 assert!(resp.next_cursor.is_none());
1320 }
1321
1322 #[cfg(feature = "serde")]
1323 #[test]
1324 fn keyset_params_serde_round_trip() {
1325 let json = serde_json::json!({"after": 5, "limit": 10});
1326 let p: KeysetPaginationParams<u64> = serde_json::from_value(json).unwrap();
1327 assert_eq!(p.after, Some(5));
1328 assert_eq!(p.limit(), 10);
1329 assert!(p.before.is_none());
1330 }
1331
1332 #[cfg(feature = "serde")]
1333 #[test]
1334 fn keyset_params_serde_defaults() {
1335 let json = serde_json::json!({});
1336 let p: KeysetPaginationParams<u64> = serde_json::from_value(json).unwrap();
1337 assert_eq!(p.limit(), 20);
1338 assert!(p.after.is_none());
1339 }
1340
1341 #[cfg(feature = "arbitrary")]
1342 #[test]
1343 fn arbitrary_pagination_params() {
1344 use arbitrary::{Arbitrary, Unstructured};
1345 let data = [0u8; 64];
1347 let mut u = Unstructured::new(&data);
1348 let p = PaginationParams::arbitrary(&mut u).unwrap();
1349 assert!(p.limit.is_none());
1350 }
1351
1352 #[cfg(feature = "arbitrary")]
1353 #[test]
1354 fn arbitrary_pagination_params_with_limit() {
1355 use arbitrary::{Arbitrary, Unstructured};
1356 let mut data = [0xFFu8; 64];
1358 data[1..9].copy_from_slice(&50u64.to_le_bytes());
1360 let mut u = Unstructured::new(&data);
1361 let p = PaginationParams::arbitrary(&mut u).unwrap();
1362 assert!(p.limit.is_some_and(|l| (1..=100).contains(&l)));
1363 }
1364
1365 #[cfg(all(feature = "arbitrary", any(feature = "std", feature = "alloc")))]
1366 #[test]
1367 fn arbitrary_cursor_pagination_params() {
1368 use arbitrary::{Arbitrary, Unstructured};
1369 let data = [0u8; 128];
1370 let mut u = Unstructured::new(&data);
1371 let p = CursorPaginationParams::arbitrary(&mut u).unwrap();
1372 assert!(p.limit.is_none());
1373 }
1374
1375 #[cfg(all(feature = "arbitrary", any(feature = "std", feature = "alloc")))]
1376 #[test]
1377 fn arbitrary_cursor_pagination_params_with_limit() {
1378 use arbitrary::{Arbitrary, Unstructured};
1379 let mut data = [0xFFu8; 128];
1381 data[1..9].copy_from_slice(&50u64.to_le_bytes());
1382 let mut u = Unstructured::new(&data);
1383 let p = CursorPaginationParams::arbitrary(&mut u).unwrap();
1384 assert!(p.limit.is_some_and(|l| (1..=100).contains(&l)));
1385 }
1386}