1use crate::rate_limit::RateLimit;
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum PaginationError {
8 PageZero,
10 PerPageZero,
12 PageLimitZero,
14 InvalidPreviousPage,
16 InvalidNextPage,
18 InvalidLastPage,
20 UnexpectedPage,
22 EmptyPageWithNextPage,
24 InvalidEntryCount,
26 PageSizeChanged,
28 TraversalChanged,
30 PageLimitExceeded,
32 Complete,
34}
35
36#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
38pub struct PageNumber(u64);
39
40impl PageNumber {
41 pub const fn new(value: u64) -> Result<Self, PaginationError> {
43 if value == 0 {
44 return Err(PaginationError::PageZero);
45 }
46 Ok(Self(value))
47 }
48
49 #[must_use]
51 pub const fn get(self) -> u64 {
52 self.0
53 }
54}
55
56#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub struct PageLimit(u32);
59
60impl PageLimit {
61 pub const fn new(value: u32) -> Result<Self, PaginationError> {
63 if value == 0 {
64 return Err(PaginationError::PageLimitZero);
65 }
66 Ok(Self(value))
67 }
68
69 #[must_use]
71 pub const fn get(self) -> u32 {
72 self.0
73 }
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub struct PageMetadata {
79 page: PageNumber,
80 per_page: u64,
81 previous_page: Option<PageNumber>,
82 next_page: Option<PageNumber>,
83 last_page: Option<PageNumber>,
84 total_entries: Option<u64>,
85}
86
87impl PageMetadata {
88 pub const fn new(
90 page: PageNumber,
91 per_page: u64,
92 previous_page: Option<PageNumber>,
93 next_page: Option<PageNumber>,
94 last_page: Option<PageNumber>,
95 total_entries: Option<u64>,
96 ) -> Result<Self, PaginationError> {
97 if per_page == 0 {
98 return Err(PaginationError::PerPageZero);
99 }
100 if let Some(previous) = previous_page {
101 let Some(expected) = page.0.checked_sub(1) else {
102 return Err(PaginationError::InvalidPreviousPage);
103 };
104 if previous.0 != expected {
105 return Err(PaginationError::InvalidPreviousPage);
106 }
107 }
108 if let Some(next) = next_page {
109 let Some(expected) = page.0.checked_add(1) else {
110 return Err(PaginationError::InvalidNextPage);
111 };
112 if next.0 != expected {
113 return Err(PaginationError::InvalidNextPage);
114 }
115 }
116 if let Some(last) = last_page {
117 if last.0 < page.0 {
118 return Err(PaginationError::InvalidLastPage);
119 }
120 if (page.0 < last.0) != next_page.is_some() {
121 return Err(PaginationError::InvalidLastPage);
122 }
123 if let Some(next) = next_page
124 && next.0 > last.0
125 {
126 return Err(PaginationError::InvalidLastPage);
127 }
128 }
129 Ok(Self {
130 page,
131 per_page,
132 previous_page,
133 next_page,
134 last_page,
135 total_entries,
136 })
137 }
138
139 #[must_use]
141 pub const fn page(self) -> PageNumber {
142 self.page
143 }
144
145 #[must_use]
147 pub const fn per_page(self) -> u64 {
148 self.per_page
149 }
150
151 #[must_use]
153 pub const fn previous_page(self) -> Option<PageNumber> {
154 self.previous_page
155 }
156
157 #[must_use]
159 pub const fn next_page(self) -> Option<PageNumber> {
160 self.next_page
161 }
162
163 #[must_use]
165 pub const fn last_page(self) -> Option<PageNumber> {
166 self.last_page
167 }
168
169 #[must_use]
171 pub const fn total_entries(self) -> Option<u64> {
172 self.total_entries
173 }
174}
175
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
178pub struct PageBoundary {
179 metadata: PageMetadata,
180 entries: usize,
181 rate_limit: Option<RateLimit>,
182}
183
184impl PageBoundary {
185 #[must_use]
187 pub const fn metadata(self) -> PageMetadata {
188 self.metadata
189 }
190
191 #[must_use]
193 pub const fn entries(self) -> usize {
194 self.entries
195 }
196
197 #[must_use]
199 pub const fn rate_limit(self) -> Option<RateLimit> {
200 self.rate_limit
201 }
202
203 #[must_use]
205 pub const fn is_terminal(self) -> bool {
206 self.metadata.next_page.is_none()
207 }
208}
209
210#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212pub struct PaginationCursor {
213 next_page: Option<PageNumber>,
214 pages_seen: u32,
215 limit: PageLimit,
216 expected_per_page: u64,
217 expected_total_entries: Option<u64>,
218 expected_last_page: Option<PageNumber>,
219}
220
221impl PaginationCursor {
222 pub const fn new(
224 first_page: PageNumber,
225 expected_per_page: u64,
226 limit: PageLimit,
227 ) -> Result<Self, PaginationError> {
228 if expected_per_page == 0 {
229 return Err(PaginationError::PerPageZero);
230 }
231 Ok(Self {
232 next_page: Some(first_page),
233 pages_seen: 0,
234 limit,
235 expected_per_page,
236 expected_total_entries: None,
237 expected_last_page: None,
238 })
239 }
240
241 pub const fn next_page(self) -> Result<PageNumber, PaginationError> {
243 match self.next_page {
244 Some(page) => Ok(page),
245 None => Err(PaginationError::Complete),
246 }
247 }
248
249 #[must_use]
251 pub const fn pages_seen(self) -> u32 {
252 self.pages_seen
253 }
254
255 pub fn observe(
257 &mut self,
258 metadata: PageMetadata,
259 entries: usize,
260 rate_limit: Option<RateLimit>,
261 ) -> Result<PageBoundary, PaginationError> {
262 let expected = match self.next_page {
263 Some(page) => page,
264 None => return Err(PaginationError::Complete),
265 };
266 if metadata.page.0 != expected.0 {
267 return Err(PaginationError::UnexpectedPage);
268 }
269 if metadata.per_page != self.expected_per_page {
270 return Err(PaginationError::PageSizeChanged);
271 }
272 if self.pages_seen != 0
273 && (metadata.total_entries != self.expected_total_entries
274 || metadata.last_page != self.expected_last_page)
275 {
276 return Err(PaginationError::TraversalChanged);
277 }
278 let entry_count = u64::try_from(entries).map_err(|_| PaginationError::InvalidEntryCount)?;
279 if entry_count > metadata.per_page {
280 return Err(PaginationError::InvalidEntryCount);
281 }
282 if let Some(total) = metadata.total_entries {
283 let page_offset = metadata
284 .page
285 .0
286 .checked_sub(1)
287 .and_then(|page| page.checked_mul(metadata.per_page))
288 .ok_or(PaginationError::InvalidEntryCount)?;
289 let expected_entries = total.saturating_sub(page_offset).min(metadata.per_page);
290 let expected_continuation = total
291 > page_offset
292 .checked_add(entry_count)
293 .ok_or(PaginationError::InvalidEntryCount)?;
294 if entry_count != expected_entries
295 || expected_continuation != metadata.next_page.is_some()
296 {
297 return Err(PaginationError::InvalidEntryCount);
298 }
299 }
300 if entries == 0 && metadata.next_page.is_some() {
301 return Err(PaginationError::EmptyPageWithNextPage);
302 }
303 let pages_seen = match self.pages_seen.checked_add(1) {
304 Some(value) => value,
305 None => return Err(PaginationError::PageLimitExceeded),
306 };
307 if metadata.next_page.is_some() && pages_seen >= self.limit.0 {
308 return Err(PaginationError::PageLimitExceeded);
309 }
310 self.pages_seen = pages_seen;
311 self.next_page = metadata.next_page;
312 if pages_seen == 1 {
313 self.expected_total_entries = metadata.total_entries;
314 self.expected_last_page = metadata.last_page;
315 }
316 Ok(PageBoundary {
317 metadata,
318 entries,
319 rate_limit,
320 })
321 }
322}
323
324#[cfg(test)]
325mod tests;