1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Deserialize)]
6pub struct PagedRequest {
7 #[serde(default = "default_page")]
8 pub page: u32,
9 #[serde(default = "default_page_size")]
10 pub page_size: u32,
11}
12
13fn default_page() -> u32 {
14 1
15}
16fn default_page_size() -> u32 {
17 20
18}
19
20impl Default for PagedRequest {
21 fn default() -> Self {
22 Self {
23 page: 1,
24 page_size: 20,
25 }
26 }
27}
28
29impl PagedRequest {
30 pub fn new(page: u32, page_size: u32) -> Self {
31 Self { page, page_size }
32 }
33
34 pub fn from_parts(page: Option<u32>, page_size: Option<u32>) -> Self {
54 Self::new(page.unwrap_or(1), page_size.unwrap_or(20)).clamped()
55 }
56
57 pub fn offset(&self) -> u32 {
58 (self.page.saturating_sub(1)).saturating_mul(self.page_size)
59 }
60
61 pub fn clamped(mut self) -> Self {
62 self.page = self.page.max(1);
63 self.page_size = self.page_size.clamp(1, 100);
64 self
65 }
66}
67
68#[derive(Debug, Clone, Serialize)]
69pub struct PagedResult<T: Serialize> {
70 pub items: Vec<T>,
71 pub total: u64,
72 pub page: u32,
73 pub page_size: u32,
74 pub total_pages: u32,
75}
76
77impl<T: Serialize> PagedResult<T> {
78 pub fn new(items: Vec<T>, total: u64, page: u32, page_size: u32) -> Self {
79 let total_pages = if page_size > 0 {
80 total.div_ceil(page_size as u64) as u32
81 } else {
82 0
83 };
84 Self {
85 items,
86 total,
87 page,
88 page_size,
89 total_pages,
90 }
91 }
92
93 pub fn from_request(items: Vec<T>, total: u64, req: &PagedRequest) -> Self {
94 Self::new(items, total, req.page, req.page_size)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 fn parts(page: Option<u32>, page_size: Option<u32>) -> (u32, u32) {
103 let request = PagedRequest::from_parts(page, page_size);
104 (request.page, request.page_size)
105 }
106
107 #[test]
108 fn from_parts_defaults_when_both_are_absent() {
109 assert_eq!(parts(None, None), (1, 20));
110 }
111
112 #[test]
113 fn from_parts_fills_only_the_missing_half() {
114 assert_eq!(parts(Some(4), None), (4, 20));
115 assert_eq!(parts(None, Some(50)), (1, 50));
116 }
117
118 #[test]
119 fn from_parts_clamps_zero_and_over_the_limit() {
120 assert_eq!(parts(Some(0), Some(0)), (1, 1));
123 assert_eq!(parts(Some(0), Some(1_000)), (1, 100));
124 assert_eq!(parts(Some(7), Some(1_000)), (7, 100));
125 assert_eq!(parts(Some(u32::MAX), Some(u32::MAX)), (u32::MAX, 100));
126 }
127
128 #[test]
129 fn from_parts_keeps_in_range_values_untouched() {
130 assert_eq!(parts(Some(1), Some(1)), (1, 1));
131 assert_eq!(parts(Some(3), Some(100)), (3, 100));
132 }
133
134 #[test]
135 fn from_parts_is_new_plus_clamped() {
136 for (page, page_size) in [
137 (None, None),
138 (Some(0), None),
139 (None, Some(0)),
140 (Some(0), Some(101)),
141 (Some(2), Some(25)),
142 (Some(u32::MAX), Some(u32::MAX)),
143 ] {
144 let from_parts = PagedRequest::from_parts(page, page_size);
145 let expected = PagedRequest::new(page.unwrap_or(1), page_size.unwrap_or(20)).clamped();
146 assert_eq!(
147 (from_parts.page, from_parts.page_size),
148 (expected.page, expected.page_size),
149 "from_parts must equal new(..).clamped() for {page:?}/{page_size:?}"
150 );
151 }
152 }
153
154 #[test]
155 fn from_parts_clamps_before_offset_is_computed() {
156 assert_eq!(PagedRequest::from_parts(Some(3), None).offset(), 40);
158 assert_eq!(
159 PagedRequest::from_parts(Some(3), Some(0)).offset(),
160 2,
161 "a zero page size is clamped to 1, so it cannot zero the offset"
162 );
163 }
164
165 #[test]
166 fn negative_query_values_are_rejected_by_deserialization() {
167 assert!(serde_json::from_str::<PagedRequest>(r#"{ "page": -1 }"#).is_err());
170 assert!(serde_json::from_str::<PagedRequest>(r#"{ "page_size": -20 }"#).is_err());
171 }
172}