Skip to main content

github_bot_sdk/client/
pagination.rs

1// GENERATED FROM: docs/spec/interfaces/pagination.md
2// Pagination support for GitHub API
3
4use serde::{Deserialize, Serialize};
5
6/// Paginated response wrapper.
7///
8/// GitHub API returns paginated results with Link headers for navigation.
9///
10/// See docs/spec/interfaces/pagination.md
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PagedResponse<T> {
13    /// Items in this page
14    pub items: Vec<T>,
15
16    /// Total count (if available from API)
17    pub total_count: Option<u64>,
18
19    /// Pagination metadata
20    pub pagination: Pagination,
21}
22
23/// Pagination metadata extracted from Link headers.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Pagination {
26    /// URL for next page (if available)
27    pub next: Option<String>,
28
29    /// URL for previous page (if available)
30    pub prev: Option<String>,
31
32    /// URL for first page (if available)
33    pub first: Option<String>,
34
35    /// URL for last page (if available)
36    pub last: Option<String>,
37
38    /// Current page number
39    pub page: Option<u64>,
40
41    /// Items per page
42    pub per_page: Option<u64>,
43}
44
45impl Pagination {
46    /// Check if there are more pages available.
47    pub fn has_next(&self) -> bool {
48        self.next.is_some()
49    }
50
51    /// Check if there are previous pages available.
52    pub fn has_prev(&self) -> bool {
53        self.prev.is_some()
54    }
55
56    /// Get the next page number.
57    pub fn next_page(&self) -> Option<u64> {
58        self.page.map(|p| p + 1)
59    }
60
61    /// Get the previous page number.
62    pub fn prev_page(&self) -> Option<u64> {
63        self.page
64            .and_then(|p| if p > 1 { Some(p - 1) } else { None })
65    }
66}
67
68impl Default for Pagination {
69    fn default() -> Self {
70        Self {
71            next: None,
72            prev: None,
73            first: None,
74            last: None,
75            page: Some(1),
76            per_page: Some(30), // GitHub's default
77        }
78    }
79}
80
81impl<T> PagedResponse<T> {
82    /// Check if there are more pages available.
83    ///
84    /// Returns true if a next page URL exists in the pagination metadata.
85    pub fn has_next(&self) -> bool {
86        self.pagination.has_next()
87    }
88
89    /// Get the next page number from the pagination URL.
90    ///
91    /// Extracts the page number from the next page URL if available.
92    ///
93    /// # Examples
94    ///
95    /// ```rust
96    /// use github_bot_sdk::client::{PagedResponse, Pagination};
97    ///
98    /// let mut pagination = Pagination::default();
99    /// pagination.next = Some("https://api.github.com/repos/o/r/issues?page=3".to_string());
100    ///
101    /// let response = PagedResponse {
102    ///     items: vec![1, 2, 3],
103    ///     total_count: None,
104    ///     pagination,
105    /// };
106    ///
107    /// assert_eq!(response.next_page_number(), Some(3));
108    /// ```
109    pub fn next_page_number(&self) -> Option<u32> {
110        self.pagination
111            .next
112            .as_ref()
113            .and_then(|url| extract_page_number(url))
114    }
115
116    /// Check if this is the last page.
117    ///
118    /// Returns true if there is no next page available.
119    pub fn is_last_page(&self) -> bool {
120        !self.has_next()
121    }
122}
123
124/// Parse pagination metadata from Link header.
125///
126/// GitHub returns Link headers like:
127/// `<https://api.github.com/resource?page=2>; rel="next", <https://api.github.com/resource?page=5>; rel="last"`
128///
129/// See docs/spec/interfaces/pagination.md
130pub fn parse_link_header(link_header: Option<&str>) -> Pagination {
131    let mut pagination = Pagination::default();
132
133    if let Some(header) = link_header {
134        for link in header.split(',') {
135            let parts: Vec<&str> = link.split(';').collect();
136            if parts.len() != 2 {
137                continue;
138            }
139
140            let url = parts[0]
141                .trim()
142                .trim_start_matches('<')
143                .trim_end_matches('>');
144            let rel = parts[1]
145                .trim()
146                .trim_start_matches("rel=\"")
147                .trim_end_matches('"');
148
149            match rel {
150                "next" => pagination.next = Some(url.to_string()),
151                "prev" => pagination.prev = Some(url.to_string()),
152                "first" => pagination.first = Some(url.to_string()),
153                "last" => pagination.last = Some(url.to_string()),
154                _ => {}
155            }
156        }
157    }
158
159    pagination
160}
161
162/// Extract page number from a URL.
163///
164/// Parses the query string to find the `page` parameter.
165///
166/// # Arguments
167///
168/// * `url` - Full URL containing page query parameter
169///
170/// # Returns
171///
172/// Returns page number if found, None otherwise.
173///
174/// # Examples
175///
176/// ```rust
177/// use github_bot_sdk::client::extract_page_number;
178///
179/// let url = "https://api.github.com/repos/o/r/issues?page=3";
180/// assert_eq!(extract_page_number(url), Some(3));
181/// ```
182pub fn extract_page_number(url: &str) -> Option<u32> {
183    // Parse the URL and extract query parameters
184    url.split('?')
185        .nth(1) // Get query string part
186        .and_then(|query| {
187            // Split by & to get individual parameters
188            query.split('&').find_map(|param| {
189                // Split by = to get key-value pairs
190                let mut parts = param.split('=');
191                let key = parts.next()?;
192                let value = parts.next()?;
193
194                // Check if this is the page parameter
195                if key == "page" {
196                    // Parse the value as u32
197                    value.parse::<u32>().ok()
198                } else {
199                    None
200                }
201            })
202        })
203}
204
205#[cfg(test)]
206#[path = "pagination_tests.rs"]
207mod tests;