Skip to main content

axum_api_kit/
cursor.rs

1use axum::{
2    response::{IntoResponse, Response},
3    Json,
4};
5use serde::Serialize;
6
7/// A cursor-based paginated collection response.
8///
9/// Unlike [`ListResponse`](crate::ListResponse) which uses offset/limit pagination,
10/// `CursorResponse` uses opaque cursor tokens for keyset-based pagination.
11/// This is ideal for large or streaming datasets where total count is expensive to compute.
12///
13/// Serializes as:
14/// ```json
15/// { "data": [...], "next_cursor": "abc123", "has_more": true }
16/// { "data": [...], "next_cursor": null, "has_more": false }
17/// ```
18///
19/// # Example
20///
21/// ```rust
22/// use axum::response::IntoResponse;
23/// use axum_api_kit::CursorResponse;
24/// use serde::Serialize;
25///
26/// #[derive(Serialize)]
27/// struct Item { id: String }
28///
29/// async fn list_items(cursor: Option<String>) -> impl IntoResponse {
30///     CursorResponse {
31///         data: vec![Item { id: "1".into() }],
32///         next_cursor: Some("cursor_for_page_2".into()),
33///         has_more: true,
34///     }
35/// }
36/// ```
37#[derive(Debug, Clone, Serialize)]
38pub struct CursorResponse<T: Serialize> {
39    /// The items in this page.
40    pub data: Vec<T>,
41    /// Opaque cursor token for fetching the next page.
42    /// `None` indicates this is the last page.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub next_cursor: Option<String>,
45    /// Whether more items exist after this page.
46    pub has_more: bool,
47}
48
49impl<T: Serialize> IntoResponse for CursorResponse<T> {
50    fn into_response(self) -> Response {
51        Json(self).into_response()
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use serde_json::json;
59
60    #[derive(Serialize)]
61    struct Item {
62        id: String,
63    }
64
65    #[test]
66    fn serializes_with_next_cursor() {
67        let resp = CursorResponse {
68            data: vec![Item { id: "1".into() }],
69            next_cursor: Some("cursor_abc".into()),
70            has_more: true,
71        };
72        let v = serde_json::to_value(&resp).unwrap();
73        assert_eq!(v["data"][0]["id"], "1");
74        assert_eq!(v["next_cursor"], "cursor_abc");
75        assert_eq!(v["has_more"], true);
76    }
77
78    #[test]
79    fn serializes_without_next_cursor_when_none() {
80        let resp: CursorResponse<Item> = CursorResponse {
81            data: vec![],
82            next_cursor: None,
83            has_more: false,
84        };
85        let v = serde_json::to_value(&resp).unwrap();
86        assert!(v.get("next_cursor").is_none());
87        assert_eq!(v["has_more"], false);
88    }
89
90    #[test]
91    fn has_more_false_at_end() {
92        let resp = CursorResponse {
93            data: vec![Item { id: "final".into() }],
94            next_cursor: None,
95            has_more: false,
96        };
97        let v = serde_json::to_value(&resp).unwrap();
98        assert_eq!(v["has_more"], false);
99        assert!(v.get("next_cursor").is_none());
100    }
101
102    #[test]
103    fn has_more_true_with_cursor() {
104        let resp = CursorResponse {
105            data: vec![Item { id: "x".into() }],
106            next_cursor: Some("token".into()),
107            has_more: true,
108        };
109        let v = serde_json::to_value(&resp).unwrap();
110        assert_eq!(v["has_more"], true);
111        assert_eq!(v["next_cursor"], "token");
112    }
113
114    #[test]
115    fn empty_data_with_no_cursor() {
116        let resp: CursorResponse<Item> = CursorResponse {
117            data: vec![],
118            next_cursor: None,
119            has_more: false,
120        };
121        let v = serde_json::to_value(&resp).unwrap();
122        assert_eq!(v["data"], json!([]));
123        assert_eq!(v["has_more"], false);
124    }
125
126    #[test]
127    fn multiple_items() {
128        let resp = CursorResponse {
129            data: vec![
130                Item { id: "1".into() },
131                Item { id: "2".into() },
132                Item { id: "3".into() },
133            ],
134            next_cursor: Some("next".into()),
135            has_more: true,
136        };
137        let v = serde_json::to_value(&resp).unwrap();
138        assert_eq!(v["data"].as_array().unwrap().len(), 3);
139        assert_eq!(v["data"][1]["id"], "2");
140    }
141}