Skip to main content

ic_canister_kit/common/
pages.rs

1use std::cmp::Ordering;
2
3use candid::CandidType;
4use serde::{Deserialize, Serialize};
5
6// ============= 分页查询 =============
7
8/// 分页对象
9#[derive(Debug, Clone, Serialize, Deserialize, CandidType)]
10pub struct QueryPage {
11    /// 当前页码 1 开始计数
12    pub page: u64,
13    /// 每页大小
14    pub size: u32,
15}
16
17/// 分页查询错误
18#[derive(Debug, Clone, Serialize, Deserialize, CandidType)]
19pub enum QueryPageError {
20    /// 错误的页码,不能为 0
21    WrongPage, // page can not be 0
22
23    /// 错误的页面大小
24    WrongSize {
25        /// 页面大小
26        size: u32,
27        /// 最大页面大小
28        #[serde(alias = "max")]
29        max_page_size: u32,
30    }, // size can not be 0 and has max value
31}
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/// 分页查询结果
49#[derive(Debug, Clone, Serialize, Deserialize, CandidType)]
50pub struct PageData<T> {
51    /// 请求的页码
52    pub page: u64,
53    /// 请求的页面大小
54    pub size: u32,
55    /// 总个数
56    pub total: u64,
57    /// 查到的分页数据
58    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
72// 空结果
73impl QueryPage {
74    /// 空结果
75    #[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    /// 检查分页选项是否有效
86    #[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    /// 分页数据对象
101    #[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    /// 对所有数据进行分页查询
144    #[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    /// 对所有数据进行倒序分页查询
154    #[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    /// 倒序过滤分页查询
171    #[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, // 过滤条件
177    ) -> 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    /// 按条件分页查询
198    #[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,       // 过滤条件
204        compare: Compare,     // 排序方法
205        transform: Transform, // 变形方法
206    ) -> Result<PageData<R>, QueryPageError>
207    where
208        Filter: Fn(&T) -> bool,
209        Compare: Fn(&T, &T) -> Ordering,
210        Transform: Fn(&T) -> R,
211    {
212        // 1. 过滤有效的结果
213        let mut list: Vec<&T> = list.iter().filter(|&item| filter(item)).collect();
214
215        // 2. 进行排序
216        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}