Skip to main content

claude_api/
pagination.rs

1//! Generic pagination wrapper used across paginated endpoints.
2//!
3//! Anthropic's list endpoints return:
4//!
5//! ```json
6//! {
7//!     "data": [...],
8//!     "has_more": false,
9//!     "first_id": "...",
10//!     "last_id": "..."
11//! }
12//! ```
13//!
14//! [`Paginated<T>`] models that envelope. Caller-driven paging via
15//! [`Paginated::next_after`] / [`Paginated::next_before`]; auto-paginating
16//! collectors (e.g. `Models::list_all`) live on each endpoint and return
17//! `Vec<T>` for v0.1.
18
19use serde::{Deserialize, Serialize};
20
21/// One page of items returned from a paginated list endpoint.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[non_exhaustive]
24pub struct Paginated<T> {
25    /// Items on this page.
26    pub data: Vec<T>,
27    /// Whether more pages exist after this one.
28    #[serde(default)]
29    pub has_more: bool,
30    /// ID of the first item on this page (cursor for `before_id`).
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub first_id: Option<String>,
33    /// ID of the last item on this page (cursor for `after_id`).
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub last_id: Option<String>,
36}
37
38/// Pagination envelope for endpoints that use opaque `next_page`
39/// cursors instead of `after_id` / `before_id`. Used by Skills, the
40/// Admin API's reports + rate-limit lists, and a few other surfaces.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[non_exhaustive]
43pub struct PaginatedNextPage<T> {
44    /// Items on this page.
45    pub data: Vec<T>,
46    /// Whether more pages exist. Some endpoints omit this in favor of
47    /// the `next_page` field alone; we tolerate either.
48    #[serde(default)]
49    pub has_more: bool,
50    /// Opaque cursor for the next page. `None` when no more pages.
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub next_page: Option<String>,
53}
54
55impl<T> PaginatedNextPage<T> {
56    /// Cursor to pass as `page` on the next request, or `None` when
57    /// the listing is exhausted.
58    pub fn next_cursor(&self) -> Option<&str> {
59        self.next_page.as_deref()
60    }
61
62    /// Whether the page itself is empty.
63    pub fn is_empty(&self) -> bool {
64        self.data.is_empty()
65    }
66}
67
68impl<T> Paginated<T> {
69    /// Cursor for the next page (forward direction): the `last_id` of this
70    /// page, suitable for the next request's `after_id` parameter. Returns
71    /// `None` if there are no more pages.
72    pub fn next_after(&self) -> Option<&str> {
73        if self.has_more {
74            self.last_id.as_deref()
75        } else {
76            None
77        }
78    }
79
80    /// Cursor for the previous page (backward direction): the `first_id`
81    /// of this page, for the next request's `before_id` parameter.
82    pub fn next_before(&self) -> Option<&str> {
83        self.first_id.as_deref()
84    }
85
86    /// Whether the page itself is empty.
87    pub fn is_empty(&self) -> bool {
88        self.data.is_empty()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use pretty_assertions::assert_eq;
96    use serde_json::json;
97
98    #[test]
99    fn paginated_round_trips_full_envelope() {
100        let p = Paginated {
101            data: vec!["a".to_owned(), "b".to_owned()],
102            has_more: true,
103            first_id: Some("first".into()),
104            last_id: Some("last".into()),
105        };
106        let v = serde_json::to_value(&p).unwrap();
107        assert_eq!(
108            v,
109            json!({
110                "data": ["a", "b"],
111                "has_more": true,
112                "first_id": "first",
113                "last_id": "last"
114            })
115        );
116        let parsed: Paginated<String> = serde_json::from_value(v).unwrap();
117        assert_eq!(parsed, p);
118    }
119
120    #[test]
121    fn paginated_tolerates_missing_optional_fields() {
122        let raw = json!({"data": [1, 2, 3]});
123        let p: Paginated<i32> = serde_json::from_value(raw).unwrap();
124        assert_eq!(p.data, vec![1, 2, 3]);
125        assert!(!p.has_more);
126        assert_eq!(p.first_id, None);
127        assert_eq!(p.last_id, None);
128    }
129
130    #[test]
131    fn next_after_returns_last_id_only_when_more_pages() {
132        let p = Paginated::<i32> {
133            data: vec![1],
134            has_more: true,
135            first_id: Some("f".into()),
136            last_id: Some("l".into()),
137        };
138        assert_eq!(p.next_after(), Some("l"));
139
140        let p_done = Paginated::<i32> {
141            has_more: false,
142            ..p
143        };
144        assert_eq!(p_done.next_after(), None);
145    }
146}