pub fn paginate<T: Clone>(
items: &[T],
next_token: Option<&str>,
max_results: usize,
) -> (Vec<T>, Option<String>) {
if max_results == 0 {
return (Vec::new(), None);
}
let offset: usize = next_token.and_then(|s| s.parse().ok()).unwrap_or(0);
let page = if offset < items.len() {
&items[offset..]
} else {
&[][..]
};
let has_more = page.len() > max_results;
let result: Vec<T> = page.iter().take(max_results).cloned().collect();
let token = if has_more {
Some((offset + max_results).to_string())
} else {
None
};
(result, token)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidNextToken;
pub fn paginate_checked<T: Clone>(
items: &[T],
next_token: Option<&str>,
max_results: usize,
) -> Result<(Vec<T>, Option<String>), InvalidNextToken> {
let offset: usize = match next_token {
None => 0,
Some(tok) => tok.parse().map_err(|_| InvalidNextToken)?,
};
if max_results == 0 {
return Ok((Vec::new(), None));
}
let page = if offset < items.len() {
&items[offset..]
} else {
&[][..]
};
let has_more = page.len() > max_results;
let result: Vec<T> = page.iter().take(max_results).cloned().collect();
let token = if has_more {
Some((offset + max_results).to_string())
} else {
None
};
Ok((result, token))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_page() {
let items: Vec<i32> = (0..10).collect();
let (page, token) = paginate(&items, None, 3);
assert_eq!(page, vec![0, 1, 2]);
assert_eq!(token, Some("3".to_string()));
}
#[test]
fn middle_page() {
let items: Vec<i32> = (0..10).collect();
let (page, token) = paginate(&items, Some("3"), 3);
assert_eq!(page, vec![3, 4, 5]);
assert_eq!(token, Some("6".to_string()));
}
#[test]
fn last_page() {
let items: Vec<i32> = (0..10).collect();
let (page, token) = paginate(&items, Some("9"), 3);
assert_eq!(page, vec![9]);
assert_eq!(token, None);
}
#[test]
fn exact_page_boundary() {
let items: Vec<i32> = (0..6).collect();
let (page, token) = paginate(&items, Some("3"), 3);
assert_eq!(page, vec![3, 4, 5]);
assert_eq!(token, None);
}
#[test]
fn offset_beyond_items() {
let items: Vec<i32> = (0..3).collect();
let (page, token) = paginate(&items, Some("100"), 3);
assert!(page.is_empty());
assert_eq!(token, None);
}
#[test]
fn invalid_token_defaults_to_zero() {
let items: Vec<i32> = (0..5).collect();
let (page, token) = paginate(&items, Some("not_a_number"), 3);
assert_eq!(page, vec![0, 1, 2]);
assert_eq!(token, Some("3".to_string()));
}
#[test]
fn zero_max_results_returns_empty_page_without_token() {
let items: Vec<i32> = (0..5).collect();
let (page, token) = paginate(&items, None, 0);
assert!(page.is_empty());
assert_eq!(token, None);
}
#[test]
fn empty_items() {
let items: Vec<i32> = vec![];
let (page, token) = paginate(&items, None, 10);
assert!(page.is_empty());
assert_eq!(token, None);
}
#[test]
fn checked_none_is_first_page() {
let items: Vec<i32> = (0..5).collect();
let (page, token) = paginate_checked(&items, None, 3).unwrap();
assert_eq!(page, vec![0, 1, 2]);
assert_eq!(token, Some("3".to_string()));
}
#[test]
fn checked_valid_token_advances() {
let items: Vec<i32> = (0..5).collect();
let (page, token) = paginate_checked(&items, Some("3"), 3).unwrap();
assert_eq!(page, vec![3, 4]);
assert_eq!(token, None);
}
#[test]
fn checked_garbage_token_is_rejected() {
let items: Vec<i32> = (0..5).collect();
assert_eq!(
paginate_checked(&items, Some("not_a_number"), 3),
Err(InvalidNextToken)
);
}
#[test]
fn checked_negative_token_is_rejected() {
let items: Vec<i32> = (0..5).collect();
assert_eq!(
paginate_checked(&items, Some("-1"), 3),
Err(InvalidNextToken)
);
}
}