cloud_sdk/pagination/
offset.rs1use crate::rate_limit::RateLimit;
2
3use super::{PaginationBudget, PaginationError, PaginationProgress, SnapshotId};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub struct OffsetPageMetadata {
8 offset: u64,
9 page_size: u64,
10 next_offset: Option<u64>,
11 total_entries: Option<u64>,
12}
13
14impl OffsetPageMetadata {
15 pub const fn new(
17 offset: u64,
18 page_size: u64,
19 next_offset: Option<u64>,
20 total_entries: Option<u64>,
21 ) -> Result<Self, PaginationError> {
22 if page_size == 0 {
23 return Err(PaginationError::PageSizeZero);
24 }
25 if let Some(next) = next_offset
26 && next <= offset
27 {
28 return Err(PaginationError::InvalidNextPage);
29 }
30 Ok(Self {
31 offset,
32 page_size,
33 next_offset,
34 total_entries,
35 })
36 }
37
38 #[must_use]
40 pub const fn offset(self) -> u64 {
41 self.offset
42 }
43
44 #[must_use]
46 pub const fn page_size(self) -> u64 {
47 self.page_size
48 }
49
50 #[must_use]
52 pub const fn next_offset(self) -> Option<u64> {
53 self.next_offset
54 }
55
56 #[must_use]
58 pub const fn total_entries(self) -> Option<u64> {
59 self.total_entries
60 }
61}
62
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub struct OffsetPageBoundary {
66 metadata: OffsetPageMetadata,
67 entries: usize,
68 rate_limit: Option<RateLimit>,
69 progress: PaginationProgress,
70}
71
72impl OffsetPageBoundary {
73 #[must_use]
75 pub const fn metadata(self) -> OffsetPageMetadata {
76 self.metadata
77 }
78
79 #[must_use]
81 pub const fn entries(self) -> usize {
82 self.entries
83 }
84
85 #[must_use]
87 pub const fn rate_limit(self) -> Option<RateLimit> {
88 self.rate_limit
89 }
90
91 #[must_use]
93 pub const fn progress(self) -> PaginationProgress {
94 self.progress
95 }
96
97 #[must_use]
99 pub const fn is_terminal(self) -> bool {
100 self.metadata.next_offset.is_none()
101 }
102}
103
104pub struct OffsetPagination {
108 next_offset: Option<u64>,
109 expected_page_size: u64,
110 expected_total_entries: Option<u64>,
111 metadata_initialized: bool,
112 budget: PaginationBudget,
113}
114
115impl OffsetPagination {
116 pub fn new(
118 first_offset: u64,
119 expected_page_size: u64,
120 budget: PaginationBudget,
121 ) -> Result<Self, PaginationError> {
122 if expected_page_size == 0 {
123 return Err(PaginationError::PageSizeZero);
124 }
125 Ok(Self {
126 next_offset: Some(first_offset),
127 expected_page_size,
128 expected_total_entries: None,
129 metadata_initialized: false,
130 budget,
131 })
132 }
133
134 pub const fn next_offset(&self) -> Result<u64, PaginationError> {
136 match self.next_offset {
137 Some(offset) => Ok(offset),
138 None => Err(PaginationError::Complete),
139 }
140 }
141
142 #[must_use]
144 pub const fn progress(&self) -> PaginationProgress {
145 self.budget.progress()
146 }
147
148 pub fn observe(
150 &mut self,
151 metadata: OffsetPageMetadata,
152 entries: usize,
153 rate_limit: Option<RateLimit>,
154 snapshot: Option<SnapshotId<'_>>,
155 ) -> Result<OffsetPageBoundary, PaginationError> {
156 let expected = self.next_offset.ok_or(PaginationError::Complete)?;
157 if metadata.offset != expected {
158 return Err(PaginationError::UnexpectedPosition);
159 }
160 if metadata.page_size != self.expected_page_size {
161 return Err(PaginationError::PageSizeChanged);
162 }
163 if self.metadata_initialized && metadata.total_entries != self.expected_total_entries {
164 return Err(PaginationError::TraversalChanged);
165 }
166 let entry_count = u64::try_from(entries).map_err(|_| PaginationError::InvalidEntryCount)?;
167 if entry_count > metadata.page_size {
168 return Err(PaginationError::InvalidEntryCount);
169 }
170 let expected_next = metadata
171 .offset
172 .checked_add(entry_count)
173 .ok_or(PaginationError::InvalidEntryCount)?;
174 if let Some(next) = metadata.next_offset
175 && (entries == 0 || next != expected_next)
176 {
177 return Err(PaginationError::InvalidNextPage);
178 }
179 if let Some(total) = metadata.total_entries {
180 let continuation = total > expected_next;
181 if expected_next > total || continuation != metadata.next_offset.is_some() {
182 return Err(PaginationError::InvalidEntryCount);
183 }
184 }
185 let has_continuation = metadata.next_offset.is_some();
186 if entries == 0 && has_continuation {
187 return Err(PaginationError::EmptyPageWithContinuation);
188 }
189 let progress = self.budget.admit(entries, has_continuation, snapshot)?;
190 self.next_offset = metadata.next_offset;
191 if !self.metadata_initialized {
192 self.expected_total_entries = metadata.total_entries;
193 self.metadata_initialized = true;
194 }
195 Ok(OffsetPageBoundary {
196 metadata,
197 entries,
198 rate_limit,
199 progress,
200 })
201 }
202}
203
204impl core::fmt::Debug for OffsetPagination {
205 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
206 formatter
207 .debug_struct("OffsetPagination")
208 .field("next_offset", &self.next_offset)
209 .field("expected_page_size", &self.expected_page_size)
210 .field("traversal_metadata", &"[redacted]")
211 .field("budget", &self.budget)
212 .finish()
213 }
214}