klauthed_data/pagination/
offset.rs1use serde::{Deserialize, Serialize};
4
5use crate::error::DataError;
6
7use super::{DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, SortKey};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct OffsetPageRequest {
15 pub page: u32,
17 pub per_page: u32,
19 pub sort: Vec<SortKey>,
21}
22
23impl OffsetPageRequest {
24 pub fn new(page: u32, per_page: u32) -> Result<Self, DataError> {
27 if page < 1 {
28 return Err(DataError::InvalidPage("page must be >= 1 (pages are 1-indexed)".into()));
29 }
30 let per_page = per_page.clamp(1, MAX_PAGE_SIZE);
31 Ok(OffsetPageRequest { page, per_page, sort: Vec::new() })
32 }
33
34 pub fn first(per_page: u32) -> Result<Self, DataError> {
36 Self::new(1, per_page)
37 }
38
39 pub fn offset(&self) -> u64 {
41 (self.page as u64 - 1) * self.per_page as u64
42 }
43
44 pub fn limit(&self) -> u64 {
46 self.per_page as u64
47 }
48}
49
50impl Default for OffsetPageRequest {
51 fn default() -> Self {
52 OffsetPageRequest { page: 1, per_page: DEFAULT_PAGE_SIZE, sort: Vec::new() }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Page<T> {
59 pub items: Vec<T>,
61 pub total_items: u64,
63 pub page: u32,
65 pub per_page: u32,
67 pub total_pages: u64,
69 pub has_next: bool,
71 pub has_prev: bool,
73}
74
75impl<T> Page<T> {
76 pub fn new(items: Vec<T>, total_items: u64, req: &OffsetPageRequest) -> Self {
79 let per_page = req.per_page as u64;
80 let total_pages = if per_page == 0 { 0 } else { total_items.div_ceil(per_page) };
81 let page = req.page;
82 let has_prev = page > 1;
83 let has_next = (page as u64) < total_pages;
84 Page { items, total_items, page, per_page: req.per_page, total_pages, has_next, has_prev }
85 }
86
87 pub fn empty(req: &OffsetPageRequest) -> Self {
89 Page::new(Vec::new(), 0, req)
90 }
91
92 pub fn map<U, F: FnMut(T) -> U>(self, f: F) -> Page<U> {
94 Page {
95 items: self.items.into_iter().map(f).collect(),
96 total_items: self.total_items,
97 page: self.page,
98 per_page: self.per_page,
99 total_pages: self.total_pages,
100 has_next: self.has_next,
101 has_prev: self.has_prev,
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn offset_request_default() {
112 let req = OffsetPageRequest::default();
113 assert_eq!(req.page, 1);
114 assert_eq!(req.per_page, DEFAULT_PAGE_SIZE);
115 assert!(req.sort.is_empty());
116 }
117
118 #[test]
119 fn offset_request_new_valid() {
120 let req = OffsetPageRequest::new(3, 10).unwrap();
121 assert_eq!(req.page, 3);
122 assert_eq!(req.per_page, 10);
123 assert_eq!(req.offset(), 20);
124 assert_eq!(req.limit(), 10);
125 }
126
127 #[test]
128 fn offset_request_first() {
129 let req = OffsetPageRequest::first(5).unwrap();
130 assert_eq!(req.page, 1);
131 assert_eq!(req.offset(), 0);
132 assert_eq!(req.limit(), 5);
133 }
134
135 #[test]
136 fn offset_request_page_zero_is_error() {
137 let err = OffsetPageRequest::new(0, 10).unwrap_err();
138 match err {
139 DataError::InvalidPage(msg) => assert!(msg.contains("1-indexed")),
140 other => panic!("expected InvalidPage, got {other:?}"),
141 }
142 }
143
144 #[test]
145 fn offset_request_per_page_capped() {
146 let req = OffsetPageRequest::new(1, 9999).unwrap();
147 assert_eq!(req.per_page, MAX_PAGE_SIZE);
148 }
149
150 #[test]
153 fn page_new_computes_metadata() {
154 let req = OffsetPageRequest::new(2, 10).unwrap();
155 let page: Page<i32> = Page::new(vec![1, 2, 3], 25, &req);
156 assert_eq!(page.total_pages, 3); assert!(page.has_prev); assert!(page.has_next); }
160
161 #[test]
162 fn page_last_page_has_no_next() {
163 let req = OffsetPageRequest::new(3, 10).unwrap();
164 let page: Page<i32> = Page::new(vec![1], 21, &req);
165 assert_eq!(page.total_pages, 3);
166 assert!(page.has_prev);
167 assert!(!page.has_next);
168 }
169
170 #[test]
171 fn page_first_page_has_no_prev() {
172 let req = OffsetPageRequest::new(1, 10).unwrap();
173 let page: Page<i32> = Page::new(vec![1], 5, &req);
174 assert!(!page.has_prev);
175 }
176
177 #[test]
178 fn page_empty() {
179 let req = OffsetPageRequest::default();
180 let page: Page<i32> = Page::empty(&req);
181 assert!(page.items.is_empty());
182 assert_eq!(page.total_items, 0);
183 assert_eq!(page.total_pages, 0);
184 assert!(!page.has_prev);
185 assert!(!page.has_next);
186 }
187
188 #[test]
189 fn page_map_transforms_items_preserves_meta() {
190 let req = OffsetPageRequest::new(2, 5).unwrap();
191 let page = Page::new(vec![1u32, 2, 3], 13, &req);
192 let mapped = page.map(|x| x * 10);
193 assert_eq!(mapped.items, vec![10, 20, 30]);
194 assert_eq!(mapped.total_items, 13);
195 assert_eq!(mapped.total_pages, 3);
196 assert!(mapped.has_prev);
197 assert!(mapped.has_next);
198 }
199}