Skip to main content

ghost_io_api/models/
pagination.rs

1//! Pagination types for Ghost API browse endpoints.
2//!
3//! Ghost API browse endpoints return paginated results with metadata about
4//! the current page, total pages, and navigation links.
5//!
6//! # Example
7//!
8//! ```
9//! use ghost_io_api::models::pagination::{Meta, Pagination};
10//!
11//! // Typically you won't construct these manually, they come from API responses
12//! let pagination = Pagination {
13//!     page: 1,
14//!     limit: 15,
15//!     pages: 5,
16//!     total: 73,
17//!     next: Some(2),
18//!     prev: None,
19//! };
20//!
21//! // Check if there are more pages
22//! assert!(pagination.has_next());
23//! assert!(!pagination.has_prev());
24//! ```
25
26use serde::{Deserialize, Serialize};
27
28/// Metadata wrapper returned by Ghost API browse endpoints.
29///
30/// Browse endpoints return paginated results with a `meta` object containing
31/// pagination information.
32///
33/// # Example
34///
35/// ```
36/// use ghost_io_api::models::pagination::Meta;
37/// use serde_json::json;
38///
39/// let json = json!({
40///     "pagination": {
41///         "page": 1,
42///         "limit": 15,
43///         "pages": 5,
44///         "total": 73,
45///         "next": 2,
46///         "prev": null
47///     }
48/// });
49///
50/// let meta: Meta = serde_json::from_value(json).unwrap();
51/// assert_eq!(meta.pagination.page, 1);
52/// assert_eq!(meta.pagination.total, 73);
53/// ```
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct Meta {
56    /// Pagination information for the current request.
57    pub pagination: Pagination,
58}
59
60/// Pagination information for browse endpoints.
61///
62/// Contains information about the current page, total number of pages,
63/// and links to the next/previous pages.
64///
65/// # Fields
66///
67/// * `page` - Current page number (1-indexed)
68/// * `limit` - Number of items per page
69/// * `pages` - Total number of pages available
70/// * `total` - Total number of items across all pages
71/// * `next` - Page number of the next page, if available
72/// * `prev` - Page number of the previous page, if available
73///
74/// # Example
75///
76/// ```
77/// use ghost_io_api::models::pagination::Pagination;
78/// use serde_json::json;
79///
80/// let json = json!({
81///     "page": 2,
82///     "limit": 10,
83///     "pages": 5,
84///     "total": 47,
85///     "next": 3,
86///     "prev": 1
87/// });
88///
89/// let pagination: Pagination = serde_json::from_value(json).unwrap();
90/// assert_eq!(pagination.page, 2);
91/// assert!(pagination.has_next());
92/// assert!(pagination.has_prev());
93/// assert!(!pagination.is_first_page());
94/// assert!(!pagination.is_last_page());
95/// ```
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct Pagination {
98    /// Current page number (1-indexed).
99    pub page: u32,
100
101    /// Number of items per page.
102    pub limit: u32,
103
104    /// Total number of pages available.
105    pub pages: u32,
106
107    /// Total number of items across all pages.
108    pub total: u32,
109
110    /// Page number of the next page, if available.
111    ///
112    /// `None` if this is the last page.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub next: Option<u32>,
115
116    /// Page number of the previous page, if available.
117    ///
118    /// `None` if this is the first page.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub prev: Option<u32>,
121}
122
123impl Pagination {
124    /// Returns `true` if there is a next page available.
125    ///
126    /// # Example
127    ///
128    /// ```
129    /// use ghost_io_api::models::pagination::Pagination;
130    ///
131    /// let pagination = Pagination {
132    ///     page: 1,
133    ///     limit: 15,
134    ///     pages: 5,
135    ///     total: 73,
136    ///     next: Some(2),
137    ///     prev: None,
138    /// };
139    ///
140    /// assert!(pagination.has_next());
141    /// ```
142    pub fn has_next(&self) -> bool {
143        self.next.is_some()
144    }
145
146    /// Returns `true` if there is a previous page available.
147    ///
148    /// # Example
149    ///
150    /// ```
151    /// use ghost_io_api::models::pagination::Pagination;
152    ///
153    /// let pagination = Pagination {
154    ///     page: 2,
155    ///     limit: 15,
156    ///     pages: 5,
157    ///     total: 73,
158    ///     next: Some(3),
159    ///     prev: Some(1),
160    /// };
161    ///
162    /// assert!(pagination.has_prev());
163    /// ```
164    pub fn has_prev(&self) -> bool {
165        self.prev.is_some()
166    }
167
168    /// Returns `true` if this is the first page.
169    ///
170    /// # Example
171    ///
172    /// ```
173    /// use ghost_io_api::models::pagination::Pagination;
174    ///
175    /// let pagination = Pagination {
176    ///     page: 1,
177    ///     limit: 15,
178    ///     pages: 5,
179    ///     total: 73,
180    ///     next: Some(2),
181    ///     prev: None,
182    /// };
183    ///
184    /// assert!(pagination.is_first_page());
185    /// ```
186    pub fn is_first_page(&self) -> bool {
187        self.page == 1
188    }
189
190    /// Returns `true` if this is the last page.
191    ///
192    /// # Example
193    ///
194    /// ```
195    /// use ghost_io_api::models::pagination::Pagination;
196    ///
197    /// let pagination = Pagination {
198    ///     page: 5,
199    ///     limit: 15,
200    ///     pages: 5,
201    ///     total: 73,
202    ///     next: None,
203    ///     prev: Some(4),
204    /// };
205    ///
206    /// assert!(pagination.is_last_page());
207    /// ```
208    pub fn is_last_page(&self) -> bool {
209        self.page == self.pages
210    }
211
212    /// Returns the number of items on the current page.
213    ///
214    /// For all pages except potentially the last one, this will equal `limit`.
215    /// On the last page, it may be less than `limit`.
216    ///
217    /// # Example
218    ///
219    /// ```
220    /// use ghost_io_api::models::pagination::Pagination;
221    ///
222    /// let pagination = Pagination {
223    ///     page: 5,
224    ///     limit: 15,
225    ///     pages: 5,
226    ///     total: 73,
227    ///     next: None,
228    ///     prev: Some(4),
229    /// };
230    ///
231    /// // Last page: 73 total - (4 * 15) = 13 items on this page
232    /// assert_eq!(pagination.items_on_page(), 13);
233    /// ```
234    pub fn items_on_page(&self) -> u32 {
235        if self.is_last_page() {
236            // Calculate remaining items on last page
237            let items_before_last_page = (self.pages - 1) * self.limit;
238            self.total.saturating_sub(items_before_last_page)
239        } else {
240            self.limit
241        }
242    }
243
244    /// Returns the starting item index for the current page (0-indexed).
245    ///
246    /// # Example
247    ///
248    /// ```
249    /// use ghost_io_api::models::pagination::Pagination;
250    ///
251    /// let pagination = Pagination {
252    ///     page: 2,
253    ///     limit: 15,
254    ///     pages: 5,
255    ///     total: 73,
256    ///     next: Some(3),
257    ///     prev: Some(1),
258    /// };
259    ///
260    /// // Page 2, limit 15: starts at index 15
261    /// assert_eq!(pagination.start_index(), 15);
262    /// ```
263    pub fn start_index(&self) -> u32 {
264        (self.page - 1) * self.limit
265    }
266
267    /// Returns the ending item index for the current page (0-indexed, exclusive).
268    ///
269    /// # Example
270    ///
271    /// ```
272    /// use ghost_io_api::models::pagination::Pagination;
273    ///
274    /// let pagination = Pagination {
275    ///     page: 2,
276    ///     limit: 15,
277    ///     pages: 5,
278    ///     total: 73,
279    ///     next: Some(3),
280    ///     prev: Some(1),
281    /// };
282    ///
283    /// // Page 2, limit 15: ends at index 30 (exclusive)
284    /// assert_eq!(pagination.end_index(), 30);
285    /// ```
286    pub fn end_index(&self) -> u32 {
287        self.start_index() + self.items_on_page()
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use serde_json::json;
295
296    #[test]
297    fn test_pagination_deserialization() {
298        let json = json!({
299            "page": 1,
300            "limit": 15,
301            "pages": 5,
302            "total": 73,
303            "next": 2,
304            "prev": null
305        });
306
307        let pagination: Pagination = serde_json::from_value(json).unwrap();
308        assert_eq!(pagination.page, 1);
309        assert_eq!(pagination.limit, 15);
310        assert_eq!(pagination.pages, 5);
311        assert_eq!(pagination.total, 73);
312        assert_eq!(pagination.next, Some(2));
313        assert_eq!(pagination.prev, None);
314    }
315
316    #[test]
317    fn test_pagination_middle_page() {
318        let json = json!({
319            "page": 3,
320            "limit": 10,
321            "pages": 5,
322            "total": 47,
323            "next": 4,
324            "prev": 2
325        });
326
327        let pagination: Pagination = serde_json::from_value(json).unwrap();
328        assert_eq!(pagination.page, 3);
329        assert_eq!(pagination.next, Some(4));
330        assert_eq!(pagination.prev, Some(2));
331    }
332
333    #[test]
334    fn test_pagination_last_page() {
335        let json = json!({
336            "page": 5,
337            "limit": 15,
338            "pages": 5,
339            "total": 73,
340            "next": null,
341            "prev": 4
342        });
343
344        let pagination: Pagination = serde_json::from_value(json).unwrap();
345        assert_eq!(pagination.page, 5);
346        assert_eq!(pagination.next, None);
347        assert_eq!(pagination.prev, Some(4));
348    }
349
350    #[test]
351    fn test_meta_deserialization() {
352        let json = json!({
353            "pagination": {
354                "page": 1,
355                "limit": 15,
356                "pages": 5,
357                "total": 73,
358                "next": 2,
359                "prev": null
360            }
361        });
362
363        let meta: Meta = serde_json::from_value(json).unwrap();
364        assert_eq!(meta.pagination.page, 1);
365        assert_eq!(meta.pagination.total, 73);
366    }
367
368    #[test]
369    fn test_has_next() {
370        let pagination = Pagination {
371            page: 1,
372            limit: 15,
373            pages: 5,
374            total: 73,
375            next: Some(2),
376            prev: None,
377        };
378        assert!(pagination.has_next());
379
380        let last_page = Pagination {
381            page: 5,
382            limit: 15,
383            pages: 5,
384            total: 73,
385            next: None,
386            prev: Some(4),
387        };
388        assert!(!last_page.has_next());
389    }
390
391    #[test]
392    fn test_has_prev() {
393        let pagination = Pagination {
394            page: 2,
395            limit: 15,
396            pages: 5,
397            total: 73,
398            next: Some(3),
399            prev: Some(1),
400        };
401        assert!(pagination.has_prev());
402
403        let first_page = Pagination {
404            page: 1,
405            limit: 15,
406            pages: 5,
407            total: 73,
408            next: Some(2),
409            prev: None,
410        };
411        assert!(!first_page.has_prev());
412    }
413
414    #[test]
415    fn test_is_first_page() {
416        let pagination = Pagination {
417            page: 1,
418            limit: 15,
419            pages: 5,
420            total: 73,
421            next: Some(2),
422            prev: None,
423        };
424        assert!(pagination.is_first_page());
425
426        let not_first = Pagination {
427            page: 2,
428            limit: 15,
429            pages: 5,
430            total: 73,
431            next: Some(3),
432            prev: Some(1),
433        };
434        assert!(!not_first.is_first_page());
435    }
436
437    #[test]
438    fn test_is_last_page() {
439        let pagination = Pagination {
440            page: 5,
441            limit: 15,
442            pages: 5,
443            total: 73,
444            next: None,
445            prev: Some(4),
446        };
447        assert!(pagination.is_last_page());
448
449        let not_last = Pagination {
450            page: 4,
451            limit: 15,
452            pages: 5,
453            total: 73,
454            next: Some(5),
455            prev: Some(3),
456        };
457        assert!(!not_last.is_last_page());
458    }
459
460    #[test]
461    fn test_items_on_page() {
462        // Full page
463        let full_page = Pagination {
464            page: 1,
465            limit: 15,
466            pages: 5,
467            total: 73,
468            next: Some(2),
469            prev: None,
470        };
471        assert_eq!(full_page.items_on_page(), 15);
472
473        // Last page with partial items: 73 - (4 * 15) = 13
474        let last_page = Pagination {
475            page: 5,
476            limit: 15,
477            pages: 5,
478            total: 73,
479            next: None,
480            prev: Some(4),
481        };
482        assert_eq!(last_page.items_on_page(), 13);
483    }
484
485    #[test]
486    fn test_start_index() {
487        let page_1 = Pagination {
488            page: 1,
489            limit: 15,
490            pages: 5,
491            total: 73,
492            next: Some(2),
493            prev: None,
494        };
495        assert_eq!(page_1.start_index(), 0);
496
497        let page_2 = Pagination {
498            page: 2,
499            limit: 15,
500            pages: 5,
501            total: 73,
502            next: Some(3),
503            prev: Some(1),
504        };
505        assert_eq!(page_2.start_index(), 15);
506
507        let page_5 = Pagination {
508            page: 5,
509            limit: 15,
510            pages: 5,
511            total: 73,
512            next: None,
513            prev: Some(4),
514        };
515        assert_eq!(page_5.start_index(), 60);
516    }
517
518    #[test]
519    fn test_end_index() {
520        let page_1 = Pagination {
521            page: 1,
522            limit: 15,
523            pages: 5,
524            total: 73,
525            next: Some(2),
526            prev: None,
527        };
528        assert_eq!(page_1.end_index(), 15);
529
530        let page_2 = Pagination {
531            page: 2,
532            limit: 15,
533            pages: 5,
534            total: 73,
535            next: Some(3),
536            prev: Some(1),
537        };
538        assert_eq!(page_2.end_index(), 30);
539
540        // Last page: starts at 60, has 13 items
541        let page_5 = Pagination {
542            page: 5,
543            limit: 15,
544            pages: 5,
545            total: 73,
546            next: None,
547            prev: Some(4),
548        };
549        assert_eq!(page_5.end_index(), 73);
550    }
551
552    #[test]
553    fn test_pagination_serialization() {
554        let pagination = Pagination {
555            page: 2,
556            limit: 10,
557            pages: 5,
558            total: 47,
559            next: Some(3),
560            prev: Some(1),
561        };
562
563        let json = serde_json::to_value(&pagination).unwrap();
564        assert_eq!(json["page"], 2);
565        assert_eq!(json["limit"], 10);
566        assert_eq!(json["pages"], 5);
567        assert_eq!(json["total"], 47);
568        assert_eq!(json["next"], 3);
569        assert_eq!(json["prev"], 1);
570    }
571
572    #[test]
573    fn test_pagination_serialization_with_nulls() {
574        let pagination = Pagination {
575            page: 1,
576            limit: 15,
577            pages: 1,
578            total: 10,
579            next: None,
580            prev: None,
581        };
582
583        let json = serde_json::to_value(&pagination).unwrap();
584        // Fields with None should be omitted due to skip_serializing_if
585        assert!(!json.as_object().unwrap().contains_key("next"));
586        assert!(!json.as_object().unwrap().contains_key("prev"));
587    }
588
589    #[test]
590    fn test_single_page() {
591        let pagination = Pagination {
592            page: 1,
593            limit: 15,
594            pages: 1,
595            total: 10,
596            next: None,
597            prev: None,
598        };
599
600        assert!(pagination.is_first_page());
601        assert!(pagination.is_last_page());
602        assert!(!pagination.has_next());
603        assert!(!pagination.has_prev());
604        assert_eq!(pagination.items_on_page(), 10);
605        assert_eq!(pagination.start_index(), 0);
606        assert_eq!(pagination.end_index(), 10);
607    }
608
609    #[test]
610    fn test_meta_serialization() {
611        let meta = Meta {
612            pagination: Pagination {
613                page: 1,
614                limit: 15,
615                pages: 5,
616                total: 73,
617                next: Some(2),
618                prev: None,
619            },
620        };
621
622        let json = serde_json::to_value(&meta).unwrap();
623        assert_eq!(json["pagination"]["page"], 1);
624        assert_eq!(json["pagination"]["total"], 73);
625    }
626
627    #[test]
628    fn test_pagination_clone_and_eq() {
629        let pagination = Pagination {
630            page: 1,
631            limit: 15,
632            pages: 5,
633            total: 73,
634            next: Some(2),
635            prev: None,
636        };
637
638        let cloned = pagination.clone();
639        assert_eq!(pagination, cloned);
640    }
641
642    #[test]
643    fn test_meta_clone_and_eq() {
644        let meta = Meta {
645            pagination: Pagination {
646                page: 1,
647                limit: 15,
648                pages: 5,
649                total: 73,
650                next: Some(2),
651                prev: None,
652            },
653        };
654
655        let cloned = meta.clone();
656        assert_eq!(meta, cloned);
657    }
658}