mik_sql/pagination/
page_info.rs

1//! PageInfo struct and methods for pagination responses.
2
3use super::cursor::Cursor;
4
5/// Page information for paginated responses.
6///
7/// # Example
8///
9/// ```
10/// # use mik_sql::PageInfo;
11/// let page_info = PageInfo::new(20, 20)
12///     .with_next_cursor(Some("abc123".to_string()))
13///     .with_prev_cursor(Some("xyz789".to_string()))
14///     .with_total(100);
15///
16/// assert!(page_info.has_next);
17/// assert!(page_info.has_prev);
18/// assert_eq!(page_info.total, Some(100));
19/// ```
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21#[non_exhaustive]
22pub struct PageInfo {
23    /// Whether there are more items after this page.
24    pub has_next: bool,
25    /// Whether there are items before this page.
26    pub has_prev: bool,
27    /// Cursor to fetch the next page.
28    pub next_cursor: Option<String>,
29    /// Cursor to fetch the previous page.
30    pub prev_cursor: Option<String>,
31    /// Total count (if available).
32    pub total: Option<u64>,
33}
34
35impl PageInfo {
36    /// Create page info based on returned count vs requested limit.
37    ///
38    /// If `count >= limit`, assumes there are more items.
39    #[must_use]
40    pub const fn new(count: usize, limit: usize) -> Self {
41        Self {
42            has_next: count >= limit,
43            has_prev: false,
44            next_cursor: None,
45            prev_cursor: None,
46            total: None,
47        }
48    }
49
50    /// Set whether there are previous items.
51    #[must_use]
52    pub const fn with_has_prev(mut self, has_prev: bool) -> Self {
53        self.has_prev = has_prev;
54        self
55    }
56
57    /// Set the next cursor.
58    #[must_use]
59    pub fn with_next_cursor(mut self, cursor: Option<String>) -> Self {
60        self.next_cursor = cursor;
61        if self.next_cursor.is_some() {
62            self.has_next = true;
63        }
64        self
65    }
66
67    /// Set the previous cursor.
68    #[must_use]
69    pub fn with_prev_cursor(mut self, cursor: Option<String>) -> Self {
70        self.prev_cursor = cursor;
71        if self.prev_cursor.is_some() {
72            self.has_prev = true;
73        }
74        self
75    }
76
77    /// Set the total count.
78    #[must_use]
79    pub const fn with_total(mut self, total: u64) -> Self {
80        self.total = Some(total);
81        self
82    }
83
84    /// Create cursor from the last item using a builder function.
85    #[allow(clippy::single_option_map)] // Intentional API design
86    pub fn cursor_from<T, F>(item: Option<&T>, builder: F) -> Option<String>
87    where
88        F: FnOnce(&T) -> Cursor,
89    {
90        item.map(|item| builder(item).encode())
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_page_info_basic() {
100        let info = PageInfo::new(20, 20);
101        assert!(info.has_next);
102        assert!(!info.has_prev);
103
104        let info = PageInfo::new(15, 20);
105        assert!(!info.has_next);
106    }
107
108    #[test]
109    fn test_page_info_with_cursors() {
110        let info = PageInfo::new(20, 20)
111            .with_next_cursor(Some("abc".to_string()))
112            .with_prev_cursor(Some("xyz".to_string()))
113            .with_total(100);
114
115        assert!(info.has_next);
116        assert!(info.has_prev);
117        assert_eq!(info.next_cursor, Some("abc".to_string()));
118        assert_eq!(info.prev_cursor, Some("xyz".to_string()));
119        assert_eq!(info.total, Some(100));
120    }
121}