1use crate::rate_limit::RateLimit;
2
3use super::{PageStrategy, PaginationBudget, PaginationError, PaginationProgress, SnapshotId};
4
5#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7pub struct PageNumber(u64);
8
9impl PageNumber {
10 pub const fn new(value: u64) -> Result<Self, PaginationError> {
12 if value == 0 {
13 return Err(PaginationError::PageZero);
14 }
15 Ok(Self(value))
16 }
17
18 #[must_use]
20 pub const fn get(self) -> u64 {
21 self.0
22 }
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub struct NumberedPageMetadata {
28 page: PageNumber,
29 page_size: u64,
30 previous_page: Option<PageNumber>,
31 next_page: Option<PageNumber>,
32 last_page: Option<PageNumber>,
33 total_entries: Option<u64>,
34}
35
36impl NumberedPageMetadata {
37 pub const fn new(
39 page: PageNumber,
40 page_size: u64,
41 previous_page: Option<PageNumber>,
42 next_page: Option<PageNumber>,
43 last_page: Option<PageNumber>,
44 total_entries: Option<u64>,
45 ) -> Result<Self, PaginationError> {
46 if page_size == 0 {
47 return Err(PaginationError::PageSizeZero);
48 }
49 if let Some(previous) = previous_page {
50 let Some(expected) = page.0.checked_sub(1) else {
51 return Err(PaginationError::InvalidPreviousPage);
52 };
53 if previous.0 != expected {
54 return Err(PaginationError::InvalidPreviousPage);
55 }
56 }
57 if let Some(next) = next_page {
58 let Some(expected) = page.0.checked_add(1) else {
59 return Err(PaginationError::InvalidNextPage);
60 };
61 if next.0 != expected {
62 return Err(PaginationError::InvalidNextPage);
63 }
64 }
65 if let Some(last) = last_page {
66 if last.0 < page.0 || (page.0 < last.0) != next_page.is_some() {
67 return Err(PaginationError::InvalidLastPage);
68 }
69 if let Some(next) = next_page
70 && next.0 > last.0
71 {
72 return Err(PaginationError::InvalidLastPage);
73 }
74 }
75 Ok(Self {
76 page,
77 page_size,
78 previous_page,
79 next_page,
80 last_page,
81 total_entries,
82 })
83 }
84
85 #[must_use]
87 pub const fn page(self) -> PageNumber {
88 self.page
89 }
90
91 #[must_use]
93 pub const fn page_size(self) -> u64 {
94 self.page_size
95 }
96
97 #[must_use]
99 pub const fn previous_page(self) -> Option<PageNumber> {
100 self.previous_page
101 }
102
103 #[must_use]
105 pub const fn next_page(self) -> Option<PageNumber> {
106 self.next_page
107 }
108
109 #[must_use]
111 pub const fn last_page(self) -> Option<PageNumber> {
112 self.last_page
113 }
114
115 #[must_use]
117 pub const fn total_entries(self) -> Option<u64> {
118 self.total_entries
119 }
120}
121
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub struct NumberedPageBoundary {
125 metadata: NumberedPageMetadata,
126 entries: usize,
127 rate_limit: Option<RateLimit>,
128 progress: PaginationProgress,
129}
130
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub struct NumberedPageObservation<'snapshot> {
134 metadata: NumberedPageMetadata,
135 entries: usize,
136 rate_limit: Option<RateLimit>,
137 snapshot: Option<SnapshotId<'snapshot>>,
138}
139
140impl<'snapshot> NumberedPageObservation<'snapshot> {
141 #[must_use]
143 pub const fn new(
144 metadata: NumberedPageMetadata,
145 entries: usize,
146 rate_limit: Option<RateLimit>,
147 snapshot: Option<SnapshotId<'snapshot>>,
148 ) -> Self {
149 Self {
150 metadata,
151 entries,
152 rate_limit,
153 snapshot,
154 }
155 }
156}
157
158impl NumberedPageBoundary {
159 #[must_use]
161 pub const fn metadata(self) -> NumberedPageMetadata {
162 self.metadata
163 }
164
165 #[must_use]
167 pub const fn entries(self) -> usize {
168 self.entries
169 }
170
171 #[must_use]
173 pub const fn rate_limit(self) -> Option<RateLimit> {
174 self.rate_limit
175 }
176
177 #[must_use]
179 pub const fn progress(self) -> PaginationProgress {
180 self.progress
181 }
182
183 #[must_use]
185 pub const fn is_terminal(self) -> bool {
186 self.metadata.next_page.is_none()
187 }
188}
189
190pub struct NumberedPagination {
194 next_page: Option<PageNumber>,
195 expected_page_size: u64,
196 expected_total_entries: Option<u64>,
197 expected_last_page: Option<PageNumber>,
198 metadata_initialized: bool,
199 budget: PaginationBudget,
200}
201
202impl NumberedPagination {
203 pub fn new(
205 first_page: PageNumber,
206 expected_page_size: u64,
207 budget: PaginationBudget,
208 ) -> Result<Self, PaginationError> {
209 if expected_page_size == 0 {
210 return Err(PaginationError::PageSizeZero);
211 }
212 Ok(Self {
213 next_page: Some(first_page),
214 expected_page_size,
215 expected_total_entries: None,
216 expected_last_page: None,
217 metadata_initialized: false,
218 budget,
219 })
220 }
221
222 pub const fn next_page(&self) -> Result<PageNumber, PaginationError> {
224 match self.next_page {
225 Some(page) => Ok(page),
226 None => Err(PaginationError::Complete),
227 }
228 }
229
230 #[must_use]
232 pub const fn progress(&self) -> PaginationProgress {
233 self.budget.progress()
234 }
235
236 pub fn observe(
238 &mut self,
239 metadata: NumberedPageMetadata,
240 entries: usize,
241 rate_limit: Option<RateLimit>,
242 snapshot: Option<SnapshotId<'_>>,
243 ) -> Result<NumberedPageBoundary, PaginationError> {
244 let expected = self.next_page.ok_or(PaginationError::Complete)?;
245 if metadata.page != expected {
246 return Err(PaginationError::UnexpectedPosition);
247 }
248 if metadata.page_size != self.expected_page_size {
249 return Err(PaginationError::PageSizeChanged);
250 }
251 if self.metadata_initialized
252 && (metadata.total_entries != self.expected_total_entries
253 || metadata.last_page != self.expected_last_page)
254 {
255 return Err(PaginationError::TraversalChanged);
256 }
257 validate_entry_count(metadata, entries)?;
258 let has_continuation = metadata.next_page.is_some();
259 if entries == 0 && has_continuation {
260 return Err(PaginationError::EmptyPageWithContinuation);
261 }
262 let progress = self.budget.admit(entries, has_continuation, snapshot)?;
263 self.next_page = metadata.next_page;
264 if !self.metadata_initialized {
265 self.expected_total_entries = metadata.total_entries;
266 self.expected_last_page = metadata.last_page;
267 self.metadata_initialized = true;
268 }
269 Ok(NumberedPageBoundary {
270 metadata,
271 entries,
272 rate_limit,
273 progress,
274 })
275 }
276}
277
278fn validate_entry_count(
279 metadata: NumberedPageMetadata,
280 entries: usize,
281) -> Result<(), PaginationError> {
282 let entry_count = u64::try_from(entries).map_err(|_| PaginationError::InvalidEntryCount)?;
283 if entry_count > metadata.page_size {
284 return Err(PaginationError::InvalidEntryCount);
285 }
286 if let Some(total) = metadata.total_entries {
287 let offset = metadata
288 .page
289 .0
290 .checked_sub(1)
291 .and_then(|page| page.checked_mul(metadata.page_size))
292 .ok_or(PaginationError::InvalidEntryCount)?;
293 let expected = total.saturating_sub(offset).min(metadata.page_size);
294 let continuation = total
295 > offset
296 .checked_add(entry_count)
297 .ok_or(PaginationError::InvalidEntryCount)?;
298 if entry_count != expected || continuation != metadata.next_page.is_some() {
299 return Err(PaginationError::InvalidEntryCount);
300 }
301 }
302 Ok(())
303}
304
305impl core::fmt::Debug for NumberedPagination {
306 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
307 formatter
308 .debug_struct("NumberedPagination")
309 .field("next_page", &self.next_page)
310 .field("expected_page_size", &self.expected_page_size)
311 .field("traversal_metadata", &"[redacted]")
312 .field("budget", &self.budget)
313 .finish()
314 }
315}
316
317impl PageStrategy for NumberedPagination {
318 type Request = PageNumber;
319 type Observation<'observation> = NumberedPageObservation<'observation>;
320 type Boundary = NumberedPageBoundary;
321
322 fn next_request(&self) -> Result<Self::Request, PaginationError> {
323 self.next_page()
324 }
325
326 fn observe<'observation>(
327 &mut self,
328 observation: Self::Observation<'observation>,
329 ) -> Result<Self::Boundary, PaginationError> {
330 self.observe(
331 observation.metadata,
332 observation.entries,
333 observation.rate_limit,
334 observation.snapshot,
335 )
336 }
337}