Skip to main content

lineark_sdk/
pagination.rs

1//! Cursor-based pagination types.
2//!
3//! Linear uses Relay-style connections. [`Connection`] wraps a page of nodes
4//! with [`PageInfo`] for cursor-based traversal.
5
6use serde::{Deserialize, Serialize};
7
8/// Relay-style page info for cursor-based pagination.
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase", default)]
11pub struct PageInfo {
12    pub has_next_page: bool,
13    pub end_cursor: Option<String>,
14    pub has_previous_page: Option<bool>,
15    pub start_cursor: Option<String>,
16}
17
18/// A paginated collection of nodes with page info.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21#[serde(bound(deserialize = "T: serde::de::DeserializeOwned"))]
22pub struct Connection<T> {
23    #[serde(default)]
24    pub nodes: Vec<T>,
25    #[serde(rename = "pageInfo", default)]
26    pub page_info: PageInfo,
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32
33    #[test]
34    fn page_info_deserializes_camel_case() {
35        let json = r#"{
36            "hasNextPage": true,
37            "endCursor": "abc123",
38            "hasPreviousPage": false,
39            "startCursor": "xyz"
40        }"#;
41        let pi: PageInfo = serde_json::from_str(json).unwrap();
42        assert!(pi.has_next_page);
43        assert_eq!(pi.end_cursor, Some("abc123".to_string()));
44        assert_eq!(pi.has_previous_page, Some(false));
45        assert_eq!(pi.start_cursor, Some("xyz".to_string()));
46    }
47
48    #[test]
49    fn page_info_defaults() {
50        let json = r#"{}"#;
51        let pi: PageInfo = serde_json::from_str(json).unwrap();
52        assert!(!pi.has_next_page);
53        assert!(pi.end_cursor.is_none());
54        assert!(pi.has_previous_page.is_none());
55        assert!(pi.start_cursor.is_none());
56    }
57
58    #[test]
59    fn connection_deserializes_with_nodes() {
60        let json = r#"{
61            "nodes": [{"value": 1}, {"value": 2}],
62            "pageInfo": {"hasNextPage": true, "endCursor": "cur"}
63        }"#;
64        let conn: Connection<serde_json::Value> = serde_json::from_str(json).unwrap();
65        assert_eq!(conn.nodes.len(), 2);
66        assert!(conn.page_info.has_next_page);
67        assert_eq!(conn.page_info.end_cursor, Some("cur".to_string()));
68    }
69
70    #[test]
71    fn connection_deserializes_empty_nodes() {
72        let json = r#"{
73            "nodes": [],
74            "pageInfo": {"hasNextPage": false}
75        }"#;
76        let conn: Connection<serde_json::Value> = serde_json::from_str(json).unwrap();
77        assert!(conn.nodes.is_empty());
78        assert!(!conn.page_info.has_next_page);
79    }
80
81    #[test]
82    fn connection_defaults_when_missing() {
83        let json = r#"{}"#;
84        let conn: Connection<serde_json::Value> = serde_json::from_str(json).unwrap();
85        assert!(conn.nodes.is_empty());
86        assert!(!conn.page_info.has_next_page);
87    }
88
89    #[test]
90    fn page_info_serializes_camel_case() {
91        let pi = PageInfo {
92            has_next_page: true,
93            end_cursor: Some("abc".to_string()),
94            has_previous_page: Some(false),
95            start_cursor: None,
96        };
97        let json = serde_json::to_value(&pi).unwrap();
98        assert_eq!(json["hasNextPage"], true);
99        assert_eq!(json["endCursor"], "abc");
100    }
101}