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}