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;