Skip to main content

cloud_sdk/pagination/
offset.rs

1use crate::rate_limit::RateLimit;
2
3use super::{PageStrategy, PaginationBudget, PaginationError, PaginationProgress, SnapshotId};
4
5/// Validated metadata from one offset-based response.
6#[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    /// Creates offset metadata without inferring continuation.
16    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    /// Returns the current offset.
39    #[must_use]
40    pub const fn offset(self) -> u64 {
41        self.offset
42    }
43
44    /// Returns the requested page size.
45    #[must_use]
46    pub const fn page_size(self) -> u64 {
47        self.page_size
48    }
49
50    /// Returns the next offset when advertised.
51    #[must_use]
52    pub const fn next_offset(self) -> Option<u64> {
53        self.next_offset
54    }
55
56    /// Returns total entries when known.
57    #[must_use]
58    pub const fn total_entries(self) -> Option<u64> {
59        self.total_entries
60    }
61}
62
63/// Accepted offset-page boundary.
64#[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
72/// One decoded offset-page response supplied to a pager driver.
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub struct OffsetPageObservation<'snapshot> {
75    metadata: OffsetPageMetadata,
76    entries: usize,
77    rate_limit: Option<RateLimit>,
78    snapshot: Option<SnapshotId<'snapshot>>,
79}
80
81impl<'snapshot> OffsetPageObservation<'snapshot> {
82    /// Groups one complete decoded response observation.
83    #[must_use]
84    pub const fn new(
85        metadata: OffsetPageMetadata,
86        entries: usize,
87        rate_limit: Option<RateLimit>,
88        snapshot: Option<SnapshotId<'snapshot>>,
89    ) -> Self {
90        Self {
91            metadata,
92            entries,
93            rate_limit,
94            snapshot,
95        }
96    }
97}
98
99impl OffsetPageBoundary {
100    /// Returns validated metadata.
101    #[must_use]
102    pub const fn metadata(self) -> OffsetPageMetadata {
103        self.metadata
104    }
105
106    /// Returns decoded entry count.
107    #[must_use]
108    pub const fn entries(self) -> usize {
109        self.entries
110    }
111
112    /// Returns rate-limit metadata when supplied.
113    #[must_use]
114    pub const fn rate_limit(self) -> Option<RateLimit> {
115        self.rate_limit
116    }
117
118    /// Returns traversal counters after this response.
119    #[must_use]
120    pub const fn progress(self) -> PaginationProgress {
121        self.progress
122    }
123
124    /// Reports whether no continuation remains.
125    #[must_use]
126    pub const fn is_terminal(self) -> bool {
127        self.metadata.next_offset.is_none()
128    }
129}
130
131/// Stateful offset strategy with fixed page size and total metadata.
132///
133/// This type is intentionally neither `Copy` nor `Clone`.
134pub struct OffsetPagination {
135    next_offset: Option<u64>,
136    expected_page_size: u64,
137    expected_total_entries: Option<u64>,
138    metadata_initialized: bool,
139    budget: PaginationBudget,
140}
141
142impl OffsetPagination {
143    /// Starts an offset traversal.
144    pub fn new(
145        first_offset: u64,
146        expected_page_size: u64,
147        budget: PaginationBudget,
148    ) -> Result<Self, PaginationError> {
149        if expected_page_size == 0 {
150            return Err(PaginationError::PageSizeZero);
151        }
152        Ok(Self {
153            next_offset: Some(first_offset),
154            expected_page_size,
155            expected_total_entries: None,
156            metadata_initialized: false,
157            budget,
158        })
159    }
160
161    /// Returns the offset the caller must request next.
162    pub const fn next_offset(&self) -> Result<u64, PaginationError> {
163        match self.next_offset {
164            Some(offset) => Ok(offset),
165            None => Err(PaginationError::Complete),
166        }
167    }
168
169    /// Returns accepted counters.
170    #[must_use]
171    pub const fn progress(&self) -> PaginationProgress {
172        self.budget.progress()
173    }
174
175    /// Validates one response transactionally, then advances the strategy.
176    pub fn observe(
177        &mut self,
178        metadata: OffsetPageMetadata,
179        entries: usize,
180        rate_limit: Option<RateLimit>,
181        snapshot: Option<SnapshotId<'_>>,
182    ) -> Result<OffsetPageBoundary, PaginationError> {
183        let expected = self.next_offset.ok_or(PaginationError::Complete)?;
184        if metadata.offset != expected {
185            return Err(PaginationError::UnexpectedPosition);
186        }
187        if metadata.page_size != self.expected_page_size {
188            return Err(PaginationError::PageSizeChanged);
189        }
190        if self.metadata_initialized && metadata.total_entries != self.expected_total_entries {
191            return Err(PaginationError::TraversalChanged);
192        }
193        let entry_count = u64::try_from(entries).map_err(|_| PaginationError::InvalidEntryCount)?;
194        if entry_count > metadata.page_size {
195            return Err(PaginationError::InvalidEntryCount);
196        }
197        let expected_next = metadata
198            .offset
199            .checked_add(entry_count)
200            .ok_or(PaginationError::InvalidEntryCount)?;
201        if let Some(next) = metadata.next_offset
202            && (entries == 0 || next != expected_next)
203        {
204            return Err(PaginationError::InvalidNextPage);
205        }
206        if let Some(total) = metadata.total_entries {
207            let continuation = total > expected_next;
208            if expected_next > total || continuation != metadata.next_offset.is_some() {
209                return Err(PaginationError::InvalidEntryCount);
210            }
211        }
212        let has_continuation = metadata.next_offset.is_some();
213        if entries == 0 && has_continuation {
214            return Err(PaginationError::EmptyPageWithContinuation);
215        }
216        let progress = self.budget.admit(entries, has_continuation, snapshot)?;
217        self.next_offset = metadata.next_offset;
218        if !self.metadata_initialized {
219            self.expected_total_entries = metadata.total_entries;
220            self.metadata_initialized = true;
221        }
222        Ok(OffsetPageBoundary {
223            metadata,
224            entries,
225            rate_limit,
226            progress,
227        })
228    }
229}
230
231impl core::fmt::Debug for OffsetPagination {
232    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
233        formatter
234            .debug_struct("OffsetPagination")
235            .field("next_offset", &self.next_offset)
236            .field("expected_page_size", &self.expected_page_size)
237            .field("traversal_metadata", &"[redacted]")
238            .field("budget", &self.budget)
239            .finish()
240    }
241}
242
243impl PageStrategy for OffsetPagination {
244    type Request = u64;
245    type Observation<'observation> = OffsetPageObservation<'observation>;
246    type Boundary = OffsetPageBoundary;
247
248    fn next_request(&self) -> Result<Self::Request, PaginationError> {
249        self.next_offset()
250    }
251
252    fn observe<'observation>(
253        &mut self,
254        observation: Self::Observation<'observation>,
255    ) -> Result<Self::Boundary, PaginationError> {
256        self.observe(
257            observation.metadata,
258            observation.entries,
259            observation.rate_limit,
260            observation.snapshot,
261        )
262    }
263}