1use std::fmt;
27
28use serde::Serialize;
29
30use crate::orm::queryset::QuerySet;
31use crate::orm::{HydrateRelated, Model};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum PaginationError {
36 InvalidPage {
38 requested: i64,
40 num_pages: i64,
42 },
43 Db(String),
45}
46
47pub type PageError = PaginationError;
49
50impl fmt::Display for PaginationError {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 PaginationError::InvalidPage {
54 requested,
55 num_pages,
56 } => write!(
57 f,
58 "invalid page number {requested}: valid pages are 1..={num_pages}"
59 ),
60 PaginationError::Db(msg) => write!(f, "pagination query failed: {msg}"),
61 }
62 }
63}
64
65impl std::error::Error for PaginationError {}
66
67impl From<sqlx::Error> for PaginationError {
68 fn from(e: sqlx::Error) -> Self {
69 PaginationError::Db(e.to_string())
70 }
71}
72
73#[derive(Debug, Clone)]
79pub struct Paginator<T> {
80 queryset: QuerySet<T>,
81 per_page: usize,
82}
83
84impl<T> Paginator<T>
85where
86 T: Model
87 + Clone
88 + for<'r> sqlx::FromRow<'r, sqlx::sqlite::SqliteRow>
89 + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>
90 + HydrateRelated,
91{
92 pub fn new(queryset: QuerySet<T>, per_page: usize) -> Self {
97 Self {
98 queryset,
99 per_page: per_page.max(1),
100 }
101 }
102
103 pub fn per_page(&self) -> usize {
105 self.per_page
106 }
107
108 pub async fn count(&self) -> Result<i64, PaginationError> {
110 Ok(self.queryset.clone().count().await?)
111 }
112
113 pub async fn num_pages(&self) -> Result<i64, PaginationError> {
116 let count = self.count().await?;
117 Ok(num_pages_for(count, self.per_page))
118 }
119
120 pub async fn page(&self, number: i64) -> Result<Page<T>, PaginationError> {
123 let count = self.count().await?;
124 let num_pages = num_pages_for(count, self.per_page);
125 if number < 1 || number > num_pages {
126 return Err(PaginationError::InvalidPage {
127 requested: number,
128 num_pages,
129 });
130 }
131 self.build_page(number, count, num_pages).await
132 }
133
134 pub async fn page_clamped(&self, number: i64) -> Result<Page<T>, PaginationError> {
138 let count = self.count().await?;
139 let num_pages = num_pages_for(count, self.per_page);
140 let number = number.clamp(1, num_pages);
141 self.build_page(number, count, num_pages).await
142 }
143
144 async fn build_page(
147 &self,
148 number: i64,
149 count: i64,
150 num_pages: i64,
151 ) -> Result<Page<T>, PaginationError> {
152 let per_page = self.per_page as u64;
153 let offset = (number - 1) as u64 * per_page;
154 let object_list = self
155 .queryset
156 .clone()
157 .limit(per_page)
158 .offset(offset)
159 .fetch()
160 .await?;
161 Ok(Page {
162 object_list,
163 number,
164 per_page: self.per_page,
165 total_count: count,
166 num_pages,
167 })
168 }
169}
170
171fn num_pages_for(count: i64, per_page: usize) -> i64 {
174 if count <= 0 {
175 return 1;
176 }
177 let per_page = per_page.max(1) as i64;
178 (count + per_page - 1) / per_page
182}
183
184#[derive(Debug, Clone)]
190pub struct Page<T> {
191 pub object_list: Vec<T>,
193 pub number: i64,
195 pub per_page: usize,
197 pub total_count: i64,
199 pub num_pages: i64,
201}
202
203impl<T> Page<T> {
204 pub fn has_next(&self) -> bool {
206 self.number < self.num_pages
207 }
208
209 pub fn has_previous(&self) -> bool {
211 self.number > 1
212 }
213
214 pub fn has_other_pages(&self) -> bool {
216 self.has_next() || self.has_previous()
217 }
218
219 pub fn next_page_number(&self) -> Option<i64> {
221 self.has_next().then(|| self.number + 1)
222 }
223
224 pub fn previous_page_number(&self) -> Option<i64> {
226 self.has_previous().then(|| self.number - 1)
227 }
228
229 pub fn start_index(&self) -> i64 {
232 if self.total_count == 0 {
233 return 0;
234 }
235 (self.number - 1) * self.per_page as i64 + 1
236 }
237
238 pub fn end_index(&self) -> i64 {
241 if self.total_count == 0 {
242 return 0;
243 }
244 (self.number * self.per_page as i64).min(self.total_count)
245 }
246
247 pub fn elided_page_range(&self, on_each_side: i64, on_ends: i64) -> Vec<PageItem> {
255 elided_range(self.number, self.num_pages, on_each_side, on_ends)
256 }
257
258 pub fn context(&self) -> PageContext {
261 self.context_with(3, 1)
262 }
263
264 pub fn context_with(&self, on_each_side: i64, on_ends: i64) -> PageContext {
266 PageContext {
267 number: self.number,
268 num_pages: self.num_pages,
269 total_count: self.total_count,
270 per_page: self.per_page,
271 has_next: self.has_next(),
272 has_previous: self.has_previous(),
273 next_page_number: self.next_page_number(),
274 previous_page_number: self.previous_page_number(),
275 start_index: self.start_index(),
276 end_index: self.end_index(),
277 page_range: self
278 .elided_page_range(on_each_side, on_ends)
279 .into_iter()
280 .map(PageItemContext::from)
281 .collect(),
282 }
283 }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
289pub enum PageItem {
290 Number(i64),
292 Ellipsis,
294}
295
296fn elided_range(current: i64, num_pages: i64, on_each_side: i64, on_ends: i64) -> Vec<PageItem> {
302 let on_each_side = on_each_side.max(0);
303 let on_ends = on_ends.max(0);
304
305 if num_pages <= (on_each_side + on_ends) * 2 + 1 {
307 return (1..=num_pages).map(PageItem::Number).collect();
308 }
309
310 let mut items = Vec::new();
311
312 let left_window_start = current - on_each_side;
314 if left_window_start > on_ends + 1 {
315 for p in 1..=on_ends {
316 items.push(PageItem::Number(p));
317 }
318 if left_window_start > on_ends + 2 {
321 items.push(PageItem::Ellipsis);
322 } else {
323 items.push(PageItem::Number(on_ends + 1));
324 }
325 } else {
326 for p in 1..left_window_start.max(1) {
327 items.push(PageItem::Number(p));
328 }
329 }
330
331 let window_start = left_window_start.max(1);
333 let window_end = (current + on_each_side).min(num_pages);
334 for p in window_start..=window_end {
335 items.push(PageItem::Number(p));
336 }
337
338 let right_window_end = current + on_each_side;
340 if right_window_end < num_pages - on_ends {
341 if right_window_end < num_pages - on_ends - 1 {
342 items.push(PageItem::Ellipsis);
343 } else {
344 items.push(PageItem::Number(num_pages - on_ends));
345 }
346 for p in (num_pages - on_ends + 1)..=num_pages {
347 items.push(PageItem::Number(p));
348 }
349 } else {
350 for p in (right_window_end + 1)..=num_pages {
351 items.push(PageItem::Number(p));
352 }
353 }
354
355 items
356}
357
358#[derive(Debug, Clone, Serialize)]
364pub struct PageContext {
365 pub number: i64,
367 pub num_pages: i64,
369 pub total_count: i64,
371 pub per_page: usize,
373 pub has_next: bool,
375 pub has_previous: bool,
377 pub next_page_number: Option<i64>,
379 pub previous_page_number: Option<i64>,
381 pub start_index: i64,
383 pub end_index: i64,
385 pub page_range: Vec<PageItemContext>,
387}
388
389#[derive(Debug, Clone, Serialize)]
393pub struct PageItemContext {
394 pub n: Option<i64>,
396 pub ellipsis: bool,
398}
399
400impl From<PageItem> for PageItemContext {
401 fn from(item: PageItem) -> Self {
402 match item {
403 PageItem::Number(n) => PageItemContext {
404 n: Some(n),
405 ellipsis: false,
406 },
407 PageItem::Ellipsis => PageItemContext {
408 n: None,
409 ellipsis: true,
410 },
411 }
412 }
413}
414
415pub fn querystring_with(current_query: &str, key: &str, value: &str) -> String {
427 let mut pairs: Vec<(String, String)> = Vec::new();
428 let mut replaced = false;
429
430 for pair in current_query.trim_start_matches('?').split('&') {
431 if pair.is_empty() {
432 continue;
433 }
434 let (k, v) = match pair.split_once('=') {
435 Some((k, v)) => (k.to_string(), v.to_string()),
436 None => (pair.to_string(), String::new()),
437 };
438 if k == key {
439 pairs.push((k, encode_component(value)));
440 replaced = true;
441 } else {
442 pairs.push((k, v));
443 }
444 }
445
446 if !replaced {
447 pairs.push((key.to_string(), encode_component(value)));
448 }
449
450 pairs
451 .into_iter()
452 .map(|(k, v)| format!("{k}={v}"))
453 .collect::<Vec<_>>()
454 .join("&")
455}
456
457fn encode_component(value: &str) -> String {
462 let mut out = String::with_capacity(value.len());
463 for ch in value.chars() {
464 match ch {
465 ' ' => out.push_str("%20"),
466 '&' => out.push_str("%26"),
467 '=' => out.push_str("%3D"),
468 '#' => out.push_str("%23"),
469 '?' => out.push_str("%3F"),
470 '%' => out.push_str("%25"),
471 c => out.push(c),
472 }
473 }
474 out
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 fn nums(items: &[PageItem]) -> Vec<String> {
482 items
483 .iter()
484 .map(|i| match i {
485 PageItem::Number(n) => n.to_string(),
486 PageItem::Ellipsis => "…".to_string(),
487 })
488 .collect()
489 }
490
491 #[test]
492 fn num_pages_math() {
493 assert_eq!(num_pages_for(23, 10), 3);
494 assert_eq!(num_pages_for(20, 10), 2);
495 assert_eq!(num_pages_for(21, 10), 3);
496 assert_eq!(num_pages_for(0, 10), 1);
498 assert_eq!(num_pages_for(-5, 10), 1);
499 assert_eq!(num_pages_for(5, 0), 5);
501 }
502
503 fn page_of(number: i64, per_page: usize, total: i64, num_pages: i64) -> Page<()> {
504 Page {
505 object_list: Vec::new(),
506 number,
507 per_page,
508 total_count: total,
509 num_pages,
510 }
511 }
512
513 #[test]
514 fn start_end_index_semantics() {
515 let p1 = page_of(1, 10, 23, 3);
517 assert_eq!(p1.start_index(), 1);
518 assert_eq!(p1.end_index(), 10);
519 assert!(!p1.has_previous());
520 assert!(p1.has_next());
521 assert_eq!(p1.next_page_number(), Some(2));
522 assert_eq!(p1.previous_page_number(), None);
523
524 let p3 = page_of(3, 10, 23, 3);
525 assert_eq!(p3.start_index(), 21);
526 assert_eq!(p3.end_index(), 23);
527 assert!(p3.has_previous());
528 assert!(!p3.has_next());
529 assert_eq!(p3.next_page_number(), None);
530 assert_eq!(p3.previous_page_number(), Some(2));
531 }
532
533 #[test]
534 fn empty_page_indices_are_zero() {
535 let p = page_of(1, 10, 0, 1);
536 assert_eq!(p.start_index(), 0);
537 assert_eq!(p.end_index(), 0);
538 assert!(!p.has_other_pages());
539 }
540
541 #[test]
542 fn elided_range_shows_all_when_small() {
543 let r = elided_range(2, 5, 2, 1);
544 assert_eq!(nums(&r), vec!["1", "2", "3", "4", "5"]);
545 }
546
547 #[test]
548 fn elided_range_windows_middle_with_both_ellipses() {
549 let r = elided_range(6, 20, 2, 1);
551 assert_eq!(
552 nums(&r),
553 vec!["1", "…", "4", "5", "6", "7", "8", "…", "20"]
554 );
555 assert!(r.contains(&PageItem::Ellipsis));
556 }
557
558 #[test]
559 fn elided_range_near_start_only_right_ellipsis() {
560 let r = elided_range(2, 20, 2, 1);
562 assert_eq!(nums(&r), vec!["1", "2", "3", "4", "…", "20"]);
563 }
564
565 #[test]
566 fn elided_range_near_end_only_left_ellipsis() {
567 let r = elided_range(19, 20, 2, 1);
568 assert_eq!(nums(&r), vec!["1", "…", "17", "18", "19", "20"]);
569 }
570
571 #[test]
572 fn querystring_replaces_page_preserving_others() {
573 assert_eq!(
575 querystring_with("page=1&sort=name", "page", "3"),
576 "page=3&sort=name"
577 );
578 assert_eq!(querystring_with("sort=name", "page", "2"), "sort=name&page=2");
580 assert_eq!(querystring_with("", "page", "5"), "page=5");
582 assert_eq!(querystring_with("?page=1", "page", "2"), "page=2");
584 assert_eq!(
586 querystring_with("page=1", "sort", "first name"),
587 "page=1&sort=first%20name"
588 );
589 }
590}