Skip to main content

renox_core/db/
paginate.rs

1use std::convert::Infallible;
2
3use axum::extract::FromRequestParts;
4use axum::http::request::Parts;
5use serde::Serialize;
6
7/// The `?page=` query parameter, defaulting to 1.
8///
9/// ```
10/// # use renox::prelude::*;
11/// # #[derive(Model, serde::Serialize, Default)] struct Product { id: i64 }
12/// async fn index(State(db): State<Db>, Page(page): Page) -> Result<View> {
13///     let products = Product::query().latest().paginate(&db, page, 20).await?;
14///     Ok(view("products/index.html", context! { products }))
15/// }
16/// ```
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Page(pub u32);
19
20impl<S: Send + Sync> FromRequestParts<S> for Page {
21    type Rejection = Infallible;
22
23    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Infallible> {
24        let page = parts
25            .uri
26            .query()
27            .and_then(|query| {
28                form_urlencoded::parse(query.as_bytes())
29                    .find(|(key, _)| key == "page")
30                    .and_then(|(_, value)| value.parse().ok())
31            })
32            .unwrap_or(1u32)
33            .max(1);
34        Ok(Self(page))
35    }
36}
37
38/// One page of results. Render its links with the built-in macro:
39///
40/// ```jinja
41/// {% from "renox/pagination.html" import pagination %}
42/// {{ pagination(products) }}
43/// ```
44#[derive(Debug, Clone, Serialize)]
45#[non_exhaustive]
46pub struct Paginated<T> {
47    /// The rows on this page.
48    pub items: Vec<T>,
49    /// The current page, 1-based.
50    pub page: u32,
51    /// Rows per page.
52    pub per_page: u32,
53    /// Rows matching the query on all pages.
54    pub total: u64,
55    /// The last page number, at least 1 (also when there are no rows).
56    pub last_page: u32,
57    /// Position of the first and last item on this page (1-based), 0 when empty.
58    pub from: u64,
59    /// Position of the last item on this page (1-based), 0 when empty.
60    pub to: u64,
61    /// Whether there is a page before this one.
62    pub has_prev: bool,
63    /// Whether there is a page after this one.
64    pub has_next: bool,
65    /// Page numbers to link to; `None` marks a gap ("…").
66    pub pages: Vec<Option<u32>>,
67}
68
69impl<T> Paginated<T> {
70    /// A page of `items` out of `total` rows; works out the other fields.
71    pub fn new(items: Vec<T>, page: u32, per_page: u32, total: u64) -> Self {
72        let last_page = total.div_ceil(u64::from(per_page)).max(1) as u32;
73        let from = if items.is_empty() {
74            0
75        } else {
76            u64::from(page - 1) * u64::from(per_page) + 1
77        };
78        let to = if items.is_empty() {
79            0
80        } else {
81            from + items.len() as u64 - 1
82        };
83        Self {
84            items,
85            page,
86            per_page,
87            total,
88            last_page,
89            from,
90            to,
91            has_prev: page > 1,
92            has_next: page < last_page,
93            pages: window(page, last_page),
94        }
95    }
96
97    /// Converts the items, keeping the page information.
98    pub fn map<U>(self, f: impl FnMut(T) -> U) -> Paginated<U> {
99        Paginated {
100            items: self.items.into_iter().map(f).collect(),
101            page: self.page,
102            per_page: self.per_page,
103            total: self.total,
104            last_page: self.last_page,
105            from: self.from,
106            to: self.to,
107            has_prev: self.has_prev,
108            has_next: self.has_next,
109            pages: self.pages,
110        }
111    }
112}
113
114/// One page without the total (`Query::simple_paginate`): one query, for
115/// "previous / next" links on large tables.
116///
117/// ```jinja
118/// {% from "renox/pagination.html" import simple_pagination %}
119/// {{ simple_pagination(orders) }}
120/// ```
121#[derive(Debug, Clone, Serialize)]
122#[non_exhaustive]
123pub struct SimplePage<T> {
124    /// The rows on this page.
125    pub items: Vec<T>,
126    /// The current page, 1-based.
127    pub page: u32,
128    /// Rows per page.
129    pub per_page: u32,
130    /// Whether there is a page before this one.
131    pub has_prev: bool,
132    /// Whether there is a page after this one (one extra row was found).
133    pub has_next: bool,
134}
135
136impl<T> SimplePage<T> {
137    /// Converts the items, keeping the page information.
138    pub fn map<U>(self, f: impl FnMut(T) -> U) -> SimplePage<U> {
139        SimplePage {
140            items: self.items.into_iter().map(f).collect(),
141            page: self.page,
142            per_page: self.per_page,
143            has_prev: self.has_prev,
144            has_next: self.has_next,
145        }
146    }
147}
148
149/// Rows after a cursor (`Query::cursor_paginate`), newest first: stable
150/// while rows are added, and fast at any depth. Pass `next_cursor` back as
151/// the cursor to get the following rows; `None` means there are no more.
152#[derive(Debug, Clone, Serialize)]
153#[non_exhaustive]
154pub struct CursorPage<T> {
155    /// The rows after the cursor.
156    pub items: Vec<T>,
157    /// Rows per page.
158    pub per_page: u32,
159    /// The cursor for the following rows; `None` on the last page.
160    pub next_cursor: Option<String>,
161}
162
163impl<T> CursorPage<T> {
164    /// Converts the items, keeping the cursor.
165    pub fn map<U>(self, f: impl FnMut(T) -> U) -> CursorPage<U> {
166        CursorPage {
167            items: self.items.into_iter().map(f).collect(),
168            per_page: self.per_page,
169            next_cursor: self.next_cursor,
170        }
171    }
172}
173
174/// First, last, and two pages either side of the current one.
175fn window(page: u32, last: u32) -> Vec<Option<u32>> {
176    let mut pages = Vec::new();
177    let mut previous = 0;
178    for n in 1..=last {
179        if n == 1 || n == last || n.abs_diff(page) <= 2 {
180            if n > previous + 1 {
181                pages.push(None);
182            }
183            pages.push(Some(n));
184            previous = n;
185        }
186    }
187    pages
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn counts_pages_and_positions() {
196        let p = Paginated::new(vec![11, 12, 13], 2, 10, 23);
197        assert_eq!((p.last_page, p.from, p.to), (3, 11, 13));
198        assert!(p.has_prev && p.has_next);
199
200        let empty = Paginated::<i32>::new(vec![], 1, 10, 0);
201        assert_eq!((empty.last_page, empty.from, empty.to), (1, 0, 0));
202        assert!(!empty.has_prev && !empty.has_next);
203    }
204
205    #[test]
206    fn windows_page_links() {
207        assert_eq!(window(1, 3), vec![Some(1), Some(2), Some(3)]);
208        assert_eq!(
209            window(10, 20),
210            vec![
211                Some(1),
212                None,
213                Some(8),
214                Some(9),
215                Some(10),
216                Some(11),
217                Some(12),
218                None,
219                Some(20)
220            ]
221        );
222        assert_eq!(
223            window(2, 9),
224            vec![Some(1), Some(2), Some(3), Some(4), None, Some(9)]
225        );
226    }
227
228    // #254: map keeps the page information.
229    #[test]
230    fn map_keeps_the_page_information() {
231        let page = Paginated {
232            items: vec![1, 2],
233            page: 2,
234            per_page: 2,
235            total: 5,
236            last_page: 3,
237            from: 3,
238            to: 4,
239            has_prev: true,
240            has_next: true,
241            pages: window(2, 3),
242        }
243        .map(|n| n * 10);
244        assert_eq!(page.items, [10, 20]);
245        assert_eq!((page.page, page.total, page.last_page), (2, 5, 3));
246        assert!(page.has_prev && page.has_next);
247
248        let simple = SimplePage {
249            items: vec!["a"],
250            page: 1,
251            per_page: 10,
252            has_prev: false,
253            has_next: true,
254        }
255        .map(str::to_uppercase);
256        assert_eq!(simple.items, ["A"]);
257        assert!(!simple.has_prev && simple.has_next);
258
259        let cursor = CursorPage {
260            items: vec![1],
261            per_page: 1,
262            next_cursor: Some("abc".into()),
263        }
264        .map(|n| n + 1);
265        assert_eq!(cursor.items, [2]);
266        assert_eq!(cursor.next_cursor.as_deref(), Some("abc"));
267    }
268}