Skip to main content

cqrs_rust_lib/read/
paged.rs

1use serde::{Deserialize, Serialize};
2#[cfg(feature = "utoipa")]
3use utoipa::ToSchema;
4
5/// A page of results.
6///
7/// Both pagination vocabularies are exposed:
8/// - `skip` / `limit` — the offset-based form used internally by
9///   [`crate::read::Pagination`] and the storage backends. Always exact.
10/// - `page` / `page_size` — the derived page-based form, kept for clients that
11///   use it. `page` is `skip / limit`, so it is only meaningful when `skip` is a
12///   multiple of `limit`.
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[cfg_attr(feature = "utoipa", derive(ToSchema))]
15#[serde(rename_all = "camelCase")]
16pub struct Paged<T> {
17    pub items: Vec<T>,
18    pub total: i64,
19    /// Number of items skipped before this page.
20    pub skip: i64,
21    /// Maximum number of items in this page.
22    pub limit: i64,
23    /// Zero-based page number, derived from `skip / limit`.
24    pub page: i64,
25    /// Alias of `limit`, kept for page-based clients.
26    pub page_size: i64,
27}
28
29impl<T> Paged<T> {
30    /// Builds a page from the offset-based values, deriving `page` and
31    /// `page_size`.
32    #[must_use]
33    pub fn new(items: Vec<T>, total: i64, skip: i64, limit: i64) -> Self {
34        Self {
35            items,
36            total,
37            skip,
38            limit,
39            page: if limit > 0 { (skip / limit).abs() } else { 0 },
40            page_size: limit,
41        }
42    }
43
44    /// Maps the items while keeping every pagination counter untouched.
45    #[must_use]
46    pub fn map<U, F: FnMut(T) -> U>(self, f: F) -> Paged<U> {
47        Paged {
48            items: self.items.into_iter().map(f).collect(),
49            total: self.total,
50            skip: self.skip,
51            limit: self.limit,
52            page: self.page,
53            page_size: self.page_size,
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn derives_page_from_skip_and_limit() {
64        let p = Paged::new(vec![1, 2, 3], 137, 20, 10);
65        assert_eq!(p.skip, 20);
66        assert_eq!(p.limit, 10);
67        assert_eq!(p.page, 2);
68        assert_eq!(p.page_size, 10);
69    }
70
71    #[test]
72    fn keeps_exact_skip_when_not_a_multiple_of_limit() {
73        let p = Paged::new(vec![1], 137, 25, 10);
74        assert_eq!(p.skip, 25);
75        assert_eq!(p.limit, 10);
76        // page is a lossy approximation, skip/limit stay exact
77        assert_eq!(p.page, 2);
78    }
79
80    #[test]
81    fn zero_limit_does_not_divide_by_zero() {
82        let p = Paged::new(Vec::<i32>::new(), 0, 0, 0);
83        assert_eq!(p.page, 0);
84        assert_eq!(p.page_size, 0);
85    }
86
87    #[test]
88    fn map_preserves_counters() {
89        let p = Paged::new(vec![1, 2], 7, 5, 2).map(|i| i.to_string());
90        assert_eq!(p.items, vec!["1".to_string(), "2".to_string()]);
91        assert_eq!(p.total, 7);
92        assert_eq!(p.skip, 5);
93        assert_eq!(p.limit, 2);
94        assert_eq!(p.page, 2);
95        assert_eq!(p.page_size, 2);
96    }
97}