Skip to main content

soma_http_api/
pagination.rs

1//! Generic pagination query and response DTOs.
2//!
3//! Not yet wired into any Soma route — no current Soma list action needs
4//! pagination — but declared here per plan section 3.11 so the first product
5//! route that does need it has a shared shape to reach for instead of
6//! inventing another one-off `limit`/`offset` pair.
7
8use serde::{Deserialize, Serialize};
9
10fn default_limit() -> usize {
11    50
12}
13
14/// Query parameters for a paginated list route: `?limit=&offset=`.
15#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
16#[serde(default)]
17pub struct PageParams {
18    #[serde(default = "default_limit")]
19    pub limit: usize,
20    pub offset: usize,
21}
22
23impl Default for PageParams {
24    fn default() -> Self {
25        Self {
26            limit: default_limit(),
27            offset: 0,
28        }
29    }
30}
31
32impl PageParams {
33    /// Clamp `limit` to `max`. This is opt-in — the type itself does not
34    /// enforce a bound, so callers that build a `Page` from client-supplied
35    /// `PageParams` must call this (or otherwise validate `limit`) before
36    /// passing the params to a query; nothing at the type level prevents
37    /// skipping this step.
38    #[must_use]
39    pub fn clamped(mut self, max: usize) -> Self {
40        self.limit = self.limit.min(max);
41        self
42    }
43}
44
45/// A single page of `T` plus enough metadata to fetch the next one.
46#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
47pub struct Page<T> {
48    pub items: Vec<T>,
49    pub limit: usize,
50    pub offset: usize,
51    /// Total item count across all pages, when the source can report it
52    /// cheaply. `None` when computing it would require a second query.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub total: Option<usize>,
55}
56
57impl<T> Page<T> {
58    pub fn new(items: Vec<T>, params: PageParams, total: Option<usize>) -> Self {
59        Self {
60            items,
61            limit: params.limit,
62            offset: params.offset,
63            total,
64        }
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn page_params_default_limit_is_fifty() {
74        assert_eq!(
75            PageParams::default(),
76            PageParams {
77                limit: 50,
78                offset: 0
79            }
80        );
81    }
82
83    #[test]
84    fn page_params_clamped_never_exceeds_max() {
85        let params = PageParams {
86            limit: 500,
87            offset: 0,
88        }
89        .clamped(100);
90        assert_eq!(params.limit, 100);
91    }
92
93    #[test]
94    fn page_carries_params_and_total() {
95        let page = Page::new(
96            vec!["a", "b"],
97            PageParams {
98                limit: 2,
99                offset: 4,
100            },
101            Some(10),
102        );
103        assert_eq!(page.items, vec!["a", "b"]);
104        assert_eq!(page.limit, 2);
105        assert_eq!(page.offset, 4);
106        assert_eq!(page.total, Some(10));
107    }
108
109    #[test]
110    fn page_omits_total_when_unknown() {
111        let page = Page::new(vec!["a"], PageParams::default(), None);
112        assert_eq!(
113            serde_json::to_value(&page).unwrap(),
114            serde_json::json!({ "items": ["a"], "limit": 50, "offset": 0 })
115        );
116    }
117}