1use std::cmp::Ordering;
2
3use candid::CandidType;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize, CandidType)]
10pub struct QueryPage {
11 pub page: u64,
13 pub size: u32,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, CandidType)]
19pub enum QueryPageError {
20 WrongPage, WrongSize {
25 size: u32,
27 #[serde(alias = "max")]
29 max_page_size: u32,
30 }, }
32impl std::fmt::Display for QueryPageError {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 QueryPageError::WrongPage => write!(f, "page can not be 0"),
36 QueryPageError::WrongSize { size, max_page_size } => {
37 if *size == 0 {
38 write!(f, "size can not be 0")
39 } else {
40 write!(f, "max_page_size({max_page_size}) < size({size})")
41 }
42 }
43 }
44 }
45}
46impl std::error::Error for QueryPageError {}
47
48#[derive(Debug, Clone, Serialize, Deserialize, CandidType)]
50pub struct PageData<T> {
51 pub page: u64,
53 pub size: u32,
55 pub total: u64,
57 pub data: Vec<T>,
59}
60
61impl<T: Clone> From<PageData<&T>> for PageData<T> {
62 fn from(value: PageData<&T>) -> Self {
63 PageData {
64 page: value.page,
65 size: value.size,
66 total: value.total,
67 data: value.data.into_iter().cloned().collect(),
68 }
69 }
70}
71
72impl QueryPage {
74 #[inline]
76 pub fn empty<T>(&self) -> PageData<T> {
77 PageData {
78 page: self.page,
79 size: self.size,
80 total: 0,
81 data: Vec::new(),
82 }
83 }
84
85 #[inline]
87 pub fn check(&self, max_page_size: u32) -> Result<(), QueryPageError> {
88 if self.page == 0 {
89 return Err(QueryPageError::WrongPage);
90 }
91 if self.size == 0 || max_page_size < self.size {
92 return Err(QueryPageError::WrongSize {
93 size: self.size,
94 max_page_size,
95 });
96 }
97 Ok(())
98 }
99
100 #[inline]
102 pub fn from_data<T>(&self, total: u64, data: Vec<T>) -> PageData<T> {
103 PageData {
104 page: self.page,
105 size: self.size,
106 total,
107 data,
108 }
109 }
110
111 #[inline]
112 fn page_start(&self) -> Option<usize> {
113 let start = (self.page - 1).checked_mul(u64::from(self.size))?;
114 usize::try_from(start).ok()
115 }
116
117 #[inline]
118 fn page_window(&self, total_items: usize) -> Option<(usize, usize)> {
119 let start = self.page_start()?;
120 if total_items <= start {
121 return None;
122 }
123
124 let end = start.saturating_add(self.size as usize).min(total_items);
125 Some((start, end))
126 }
127
128 #[inline]
129 fn inner_query_by_list<'a, T>(&self, list: &'a [T], max_page_size: u32) -> Result<Vec<&'a T>, QueryPageError> {
130 self.check(max_page_size)?;
131
132 if list.is_empty() {
133 return Ok(Vec::new());
134 }
135
136 let Some((start, end)) = self.page_window(list.len()) else {
137 return Ok(Vec::new());
138 };
139
140 Ok(list[start..end].iter().collect())
141 }
142
143 #[inline]
145 pub fn query_by_list<'a, T>(&self, list: &'a [T], max_page_size: u32) -> Result<PageData<&'a T>, QueryPageError> {
146 let total_items = list.len() as u64;
147
148 let data = self.inner_query_by_list(list, max_page_size)?;
149
150 Ok(self.from_data(total_items, data))
151 }
152
153 #[inline]
155 pub fn query_desc_by_list<'a, T>(
156 &self,
157 list: &'a [T],
158 max_page_size: u32,
159 ) -> Result<PageData<&'a T>, QueryPageError> {
160 self.check(max_page_size)?;
161 let total_items = list.len() as u64;
162 let data = self
163 .page_window(list.len())
164 .map(|(start, end)| list.iter().rev().skip(start).take(end - start).collect())
165 .unwrap_or_default();
166
167 Ok(self.from_data(total_items, data))
168 }
169
170 #[inline]
172 pub fn query_desc_by_list_and_filter<'a, T, F>(
173 &self,
174 list: &'a [T],
175 max_page_size: u32,
176 filter: F, ) -> Result<PageData<&'a T>, QueryPageError>
178 where
179 F: Fn(&T) -> bool,
180 {
181 self.check(max_page_size)?;
182 let start = self.page_start();
183 let mut total_items = 0_usize;
184 let mut data = Vec::with_capacity((self.size as usize).min(list.len()));
185 for item in list.iter().rev() {
186 if filter(item) {
187 if start.is_some_and(|start| start <= total_items) && data.len() < self.size as usize {
188 data.push(item);
189 }
190 total_items += 1;
191 }
192 }
193
194 Ok(self.from_data(total_items as u64, data))
195 }
196
197 #[inline]
199 pub fn custom_query_by_list<T, R, Filter, Compare, Transform>(
200 &self,
201 list: &[T],
202 max_page_size: u32,
203 filter: Filter, compare: Compare, transform: Transform, ) -> Result<PageData<R>, QueryPageError>
207 where
208 Filter: Fn(&T) -> bool,
209 Compare: Fn(&T, &T) -> Ordering,
210 Transform: Fn(&T) -> R,
211 {
212 let mut list: Vec<&T> = list.iter().filter(|&item| filter(item)).collect();
214
215 list.sort_by(|&a, &b| compare(a, b));
217
218 let total = list.len() as u64;
219
220 let data = self.inner_query_by_list(&list, max_page_size)?;
221
222 let data = data.into_iter().map(|t| transform(t)).collect::<Vec<_>>();
223
224 Ok(self.from_data(total, data))
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use ciborium::value::Value;
231 use serde::Serialize;
232
233 use super::{PageData, QueryPage, QueryPageError};
234
235 #[test]
236 fn rejects_zero_page_and_invalid_size() {
237 assert!(matches!(
238 QueryPage { page: 0, size: 1 }.check(10),
239 Err(QueryPageError::WrongPage)
240 ));
241 assert!(matches!(
242 QueryPage { page: 1, size: 0 }.check(10),
243 Err(QueryPageError::WrongSize { .. })
244 ));
245 assert!(matches!(
246 QueryPage { page: 1, size: 11 }.check(10),
247 Err(QueryPageError::WrongSize { .. })
248 ));
249 }
250
251 #[test]
252 fn returns_empty_for_overflowing_or_out_of_range_page() {
253 let data = [1, 2, 3];
254 let overflow = QueryPage {
255 page: u64::MAX,
256 size: 10,
257 };
258 assert!(overflow.query_by_list(&data, 10).unwrap().data.is_empty());
259
260 let out_of_range = QueryPage { page: 3, size: 2 };
261 assert!(out_of_range.query_by_list(&data, 10).unwrap().data.is_empty());
262 }
263
264 #[test]
265 fn paginates_forward_reverse_and_filtered_data() {
266 use std::cell::Cell;
267
268 let data = [1, 2, 3, 4, 5];
269 let page = QueryPage { page: 2, size: 2 };
270
271 assert_eq!(page.query_by_list(&data, 10).unwrap().data, vec![&3, &4]);
272 assert_eq!(page.query_desc_by_list(&data, 10).unwrap().data, vec![&3, &2]);
273
274 let calls = Cell::new(0);
275 let filtered = page.query_desc_by_list_and_filter(&data, 10, |value| {
276 calls.set(calls.get() + 1);
277 value % 2 == 1
278 });
279 let filtered = filtered.unwrap();
280 assert_eq!(filtered.total, 3);
281 assert_eq!(filtered.data, vec![&1]);
282 assert_eq!(calls.get(), data.len());
283 }
284
285 #[test]
286 fn preserves_page_total_and_deserializes_legacy_error_name() {
287 #[derive(Serialize)]
288 struct LegacyPageData {
289 page: u64,
290 size: u32,
291 total: u64,
292 data: Vec<u8>,
293 }
294
295 #[derive(Serialize)]
296 enum LegacyQueryPageError {
297 WrongSize { size: u32, max: u32 },
298 }
299
300 let mut legacy_page_cbor = Vec::new();
301 ciborium::ser::into_writer(
302 &LegacyPageData {
303 page: 1,
304 size: 10,
305 total: 2,
306 data: vec![1, 2],
307 },
308 &mut legacy_page_cbor,
309 )
310 .unwrap();
311 let page: PageData<u8> = ciborium::de::from_reader(legacy_page_cbor.as_slice()).unwrap();
312 assert_eq!(page.total, 2);
313
314 let mut current_page_cbor = Vec::new();
315 ciborium::ser::into_writer(&page, &mut current_page_cbor).unwrap();
316 let current: Value = ciborium::de::from_reader(current_page_cbor.as_slice()).unwrap();
317 let Value::Map(entries) = current else {
318 panic!("expected a CBOR map")
319 };
320 let keys: Vec<&str> = entries
321 .iter()
322 .filter_map(|(key, _)| match key {
323 Value::Text(key) => Some(key.as_str()),
324 _ => None,
325 })
326 .collect();
327 assert!(keys.contains(&"total"));
328 assert!(!keys.contains(&"total_items"));
329
330 let mut legacy_error_cbor = Vec::new();
331 ciborium::ser::into_writer(
332 &LegacyQueryPageError::WrongSize { size: 11, max: 10 },
333 &mut legacy_error_cbor,
334 )
335 .unwrap();
336 let error: QueryPageError = ciborium::de::from_reader(legacy_error_cbor.as_slice()).unwrap();
337 assert!(matches!(
338 error,
339 QueryPageError::WrongSize {
340 size: 11,
341 max_page_size: 10
342 }
343 ));
344
345 let mut current_error_cbor = Vec::new();
346 ciborium::ser::into_writer(
347 &QueryPageError::WrongSize {
348 size: 11,
349 max_page_size: 10,
350 },
351 &mut current_error_cbor,
352 )
353 .unwrap();
354 let current_error: Value = ciborium::de::from_reader(current_error_cbor.as_slice()).unwrap();
355 let Value::Map(variant) = current_error else {
356 panic!("expected a CBOR enum map")
357 };
358 let Value::Map(fields) = &variant[0].1 else {
359 panic!("expected WrongSize fields")
360 };
361 let keys: Vec<&str> = fields
362 .iter()
363 .filter_map(|(key, _)| match key {
364 Value::Text(key) => Some(key.as_str()),
365 _ => None,
366 })
367 .collect();
368 assert!(keys.contains(&"max_page_size"));
369 assert!(!keys.contains(&"max"));
370 }
371}