Skip to main content

headless_lms_utils/
pagination.rs

1use std::{fmt, num::ParseIntError};
2
3use anyhow::bail;
4use serde::{
5    Deserialize, Deserializer,
6    de::{self, MapAccess, Visitor},
7};
8
9/// Represents the URL query parameters `page` and `limit`, used for paginating database queries.
10#[derive(Debug, Clone, Copy)]
11
12pub struct Pagination {
13    // the deserialize implementation contains a default value for page
14    page: u32,
15    // the deserialize implementation contains a default value for limit
16    limit: u32,
17}
18
19impl Pagination {
20    /// Errors on non-positive page or limit values.
21    pub fn new(page: u32, limit: u32) -> anyhow::Result<Self> {
22        if page == 0 {
23            bail!("Page must be a positive value.");
24        }
25        if limit == 0 {
26            bail!("Limit must be a positive value.");
27        }
28        if limit > 10_000 {
29            bail!("Limit can be at most 10000.")
30        }
31        Ok(Pagination { page, limit })
32    }
33
34    /// Guaranteed to be positive.
35    pub fn page(&self) -> i64 {
36        self.page.into()
37    }
38
39    /// Guaranteed to be positive.
40    pub fn limit(&self) -> i64 {
41        self.limit.into()
42    }
43
44    /// Guaranteed to be nonnegative.
45    pub fn offset(&self) -> i64 {
46        // Computed in i64 so a large `page` cannot overflow the u32 multiplication; an out-of-range
47        // page then just yields an empty result.
48        i64::from(self.limit) * (i64::from(self.page) - 1)
49    }
50
51    /// Guaranteed to be positive.
52    pub fn total_pages(&self, total_count: u32) -> u32 {
53        let remainder = total_count % self.limit;
54        if remainder == 0 {
55            total_count / self.limit
56        } else {
57            total_count / self.limit + 1
58        }
59    }
60
61    /// Helper to paginate an existing Vec efficiently.
62    pub fn paginate<T>(&self, v: &mut Vec<T>) {
63        let limit = self.limit as usize;
64        let start = limit * (self.page as usize - 1);
65        v.truncate(start + limit);
66        v.drain(..start);
67    }
68
69    pub fn next_page(&mut self) {
70        self.page += 1;
71    }
72}
73
74impl Default for Pagination {
75    fn default() -> Self {
76        Self {
77            page: 1,
78            limit: 100,
79        }
80    }
81}
82
83impl<'de> Deserialize<'de> for Pagination {
84    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
85    where
86        D: Deserializer<'de>,
87    {
88        struct PaginationVisitor;
89
90        impl<'de> Visitor<'de> for PaginationVisitor {
91            type Value = Pagination;
92
93            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
94                formatter.write_str("query parameters `page` and `limit`")
95            }
96
97            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
98            where
99                A: MapAccess<'de>,
100            {
101                let mut page = None;
102                let mut limit = None;
103                while let Some(key) = map.next_key().map_err(|e| {
104                    de::Error::custom(format!("Failed to deserialize map key: {}", e))
105                })? {
106                    match key {
107                        "page" => {
108                            if page.is_some() {
109                                return Err(de::Error::duplicate_field("page"));
110                            }
111                            let value: StrOrInt = map.next_value().map_err(|e| {
112                                de::Error::custom(format!(
113                                    "Failed to deserialize page value: {}",
114                                    e
115                                ))
116                            })?;
117                            let value = value.into_int().map_err(|e| {
118                                de::Error::custom(format!(
119                                    "Failed to deserialize page value: {}",
120                                    e
121                                ))
122                            })?;
123                            if value < 1 {
124                                return Err(de::Error::custom(
125                                    "query parameter `page` must be a positive integer",
126                                ));
127                            }
128                            page = Some(value);
129                        }
130                        "limit" => {
131                            if limit.is_some() {
132                                return Err(de::Error::duplicate_field("limit"));
133                            }
134                            let value: StrOrInt = map.next_value().map_err(|e| {
135                                de::Error::custom(format!(
136                                    "Failed to deserialize limit value: {}",
137                                    e
138                                ))
139                            })?;
140                            let value = value.into_int().map_err(|e| {
141                                de::Error::custom(format!(
142                                    "Failed to deserialize limit value: {}",
143                                    e
144                                ))
145                            })?;
146                            if !(1..=10000).contains(&value) {
147                                return Err(de::Error::custom(
148                                    "query parameter `limit` must be an integer between 1 and 10000",
149                                ));
150                            }
151                            limit = Some(value);
152                        }
153                        field => {
154                            return Err(de::Error::custom(format!(
155                                "unexpected parameter `{}`",
156                                field
157                            )));
158                        }
159                    }
160                }
161                Ok(Pagination {
162                    page: page.unwrap_or(Pagination::default().page),
163                    limit: limit.unwrap_or(Pagination::default().limit),
164                })
165            }
166        }
167
168        deserializer.deserialize_struct("Pagination", &["page", "limit"], PaginationVisitor)
169    }
170}
171
172// for some reason, it seems like when there are only numeric query parameters, actix gives them to serde as numbers,
173// but if there's a string mixed in, they are all given as strings. this helper is used to handle both cases
174#[derive(Debug, Deserialize)]
175#[serde(untagged)]
176enum StrOrInt<'a> {
177    Str(&'a str),
178    Int(u32),
179}
180
181impl StrOrInt<'_> {
182    fn into_int(self) -> Result<u32, ParseIntError> {
183        match self {
184            Self::Str(s) => s.parse(),
185            Self::Int(i) => Ok(i),
186        }
187    }
188}
189
190#[cfg(test)]
191mod test {
192    use super::*;
193
194    #[test]
195    fn paginates() {
196        let mut v = vec![1, 2, 3, 4, 5, 6, 7, 8];
197        let pagination = Pagination::new(2, 3).unwrap();
198        pagination.paginate(&mut v);
199        assert_eq!(v, &[4, 5, 6]);
200    }
201
202    #[test]
203    fn paginates_non_existent_page() {
204        let mut v = vec![1, 2, 3, 4, 5, 6, 7, 8];
205        let pagination = Pagination::new(3, 4).unwrap();
206        pagination.paginate(&mut v);
207        assert_eq!(v, &[] as &[i32]);
208    }
209
210    #[test]
211    fn paginates_incomplete_page() {
212        let mut v = vec![1, 2, 3, 4, 5, 6, 7, 8];
213        let pagination = Pagination::new(2, 5).unwrap();
214        pagination.paginate(&mut v);
215        assert_eq!(v, &[6, 7, 8]);
216    }
217}