fakecloud_core/
pagination.rs1#[must_use]
12pub fn paginate<T: Clone>(
13 items: &[T],
14 next_token: Option<&str>,
15 max_results: usize,
16) -> (Vec<T>, Option<String>) {
17 if max_results == 0 {
18 return (Vec::new(), None);
19 }
20 let offset: usize = next_token.and_then(|s| s.parse().ok()).unwrap_or(0);
21 let page = if offset < items.len() {
22 &items[offset..]
23 } else {
24 &[][..]
25 };
26 let has_more = page.len() > max_results;
27 let result: Vec<T> = page.iter().take(max_results).cloned().collect();
28 let token = if has_more {
29 Some((offset + max_results).to_string())
30 } else {
31 None
32 };
33 (result, token)
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct InvalidNextToken;
42
43pub fn paginate_checked<T: Clone>(
48 items: &[T],
49 next_token: Option<&str>,
50 max_results: usize,
51) -> Result<(Vec<T>, Option<String>), InvalidNextToken> {
52 let offset: usize = match next_token {
53 None => 0,
54 Some(tok) => tok.parse().map_err(|_| InvalidNextToken)?,
55 };
56 if max_results == 0 {
57 return Ok((Vec::new(), None));
58 }
59 let page = if offset < items.len() {
60 &items[offset..]
61 } else {
62 &[][..]
63 };
64 let has_more = page.len() > max_results;
65 let result: Vec<T> = page.iter().take(max_results).cloned().collect();
66 let token = if has_more {
67 Some((offset + max_results).to_string())
68 } else {
69 None
70 };
71 Ok((result, token))
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn first_page() {
80 let items: Vec<i32> = (0..10).collect();
81 let (page, token) = paginate(&items, None, 3);
82 assert_eq!(page, vec![0, 1, 2]);
83 assert_eq!(token, Some("3".to_string()));
84 }
85
86 #[test]
87 fn middle_page() {
88 let items: Vec<i32> = (0..10).collect();
89 let (page, token) = paginate(&items, Some("3"), 3);
90 assert_eq!(page, vec![3, 4, 5]);
91 assert_eq!(token, Some("6".to_string()));
92 }
93
94 #[test]
95 fn last_page() {
96 let items: Vec<i32> = (0..10).collect();
97 let (page, token) = paginate(&items, Some("9"), 3);
98 assert_eq!(page, vec![9]);
99 assert_eq!(token, None);
100 }
101
102 #[test]
103 fn exact_page_boundary() {
104 let items: Vec<i32> = (0..6).collect();
105 let (page, token) = paginate(&items, Some("3"), 3);
106 assert_eq!(page, vec![3, 4, 5]);
107 assert_eq!(token, None);
108 }
109
110 #[test]
111 fn offset_beyond_items() {
112 let items: Vec<i32> = (0..3).collect();
113 let (page, token) = paginate(&items, Some("100"), 3);
114 assert!(page.is_empty());
115 assert_eq!(token, None);
116 }
117
118 #[test]
119 fn invalid_token_defaults_to_zero() {
120 let items: Vec<i32> = (0..5).collect();
121 let (page, token) = paginate(&items, Some("not_a_number"), 3);
122 assert_eq!(page, vec![0, 1, 2]);
123 assert_eq!(token, Some("3".to_string()));
124 }
125
126 #[test]
127 fn zero_max_results_returns_empty_page_without_token() {
128 let items: Vec<i32> = (0..5).collect();
132 let (page, token) = paginate(&items, None, 0);
133 assert!(page.is_empty());
134 assert_eq!(token, None);
135 }
136
137 #[test]
138 fn empty_items() {
139 let items: Vec<i32> = vec![];
140 let (page, token) = paginate(&items, None, 10);
141 assert!(page.is_empty());
142 assert_eq!(token, None);
143 }
144
145 #[test]
148 fn checked_none_is_first_page() {
149 let items: Vec<i32> = (0..5).collect();
150 let (page, token) = paginate_checked(&items, None, 3).unwrap();
151 assert_eq!(page, vec![0, 1, 2]);
152 assert_eq!(token, Some("3".to_string()));
153 }
154
155 #[test]
156 fn checked_valid_token_advances() {
157 let items: Vec<i32> = (0..5).collect();
158 let (page, token) = paginate_checked(&items, Some("3"), 3).unwrap();
159 assert_eq!(page, vec![3, 4]);
160 assert_eq!(token, None);
161 }
162
163 #[test]
164 fn checked_garbage_token_is_rejected() {
165 let items: Vec<i32> = (0..5).collect();
166 assert_eq!(
167 paginate_checked(&items, Some("not_a_number"), 3),
168 Err(InvalidNextToken)
169 );
170 }
171
172 #[test]
173 fn checked_negative_token_is_rejected() {
174 let items: Vec<i32> = (0..5).collect();
175 assert_eq!(
176 paginate_checked(&items, Some("-1"), 3),
177 Err(InvalidNextToken)
178 );
179 }
180}