pod_types/
pagination.rs

1use anyhow::{Context, anyhow};
2use base64::Engine;
3use serde::{Deserialize, Serialize, Serializer};
4use utoipa::ToSchema;
5
6pub const DEFAULT_QUERY_LIMIT: usize = 100;
7
8#[derive(Debug, Deserialize, Serialize, ToSchema)]
9pub struct CursorPaginationRequest {
10    #[serde(default)]
11    pub cursor: Option<String>,
12    #[serde(default = "default_limit")]
13    pub limit: Option<usize>,
14    #[serde(default)]
15    pub newest_first: Option<bool>,
16}
17
18#[derive(Debug, Clone)]
19pub struct CursorPagination {
20    pub cursor_start: Option<String>,
21    pub cursor_end: Option<String>,
22    pub limit: usize,
23    pub newest_first: Option<bool>,
24}
25
26#[derive(Debug, Serialize, Deserialize, ToSchema)]
27pub struct ApiPaginatedResult<T: Serialize> {
28    pub items: Vec<T>,
29    #[serde(serialize_with = "serialize_cursor")]
30    pub cursor: Option<(String, String)>,
31}
32
33pub fn serialize_cursor<S>(
34    cursor: &Option<(String, String)>,
35    serializer: S,
36) -> Result<S::Ok, S::Error>
37where
38    S: Serializer,
39{
40    match cursor {
41        Some((start, end)) => {
42            let cursor_str = format!("{start}|{end}");
43            let encoded = base64::engine::general_purpose::STANDARD.encode(cursor_str);
44            serializer.serialize_str(&encoded)
45        }
46        None => serializer.serialize_none(),
47    }
48}
49
50fn default_limit() -> Option<usize> {
51    Some(DEFAULT_QUERY_LIMIT)
52}
53
54impl CursorPaginationRequest {
55    pub fn new(cursor: Option<String>, limit: Option<usize>, newest_first: Option<bool>) -> Self {
56        Self {
57            cursor,
58            limit,
59            newest_first,
60        }
61    }
62}
63
64impl Default for CursorPaginationRequest {
65    fn default() -> Self {
66        Self {
67            cursor: None,
68            limit: Some(DEFAULT_QUERY_LIMIT),
69            newest_first: Some(true),
70        }
71    }
72}
73
74impl TryFrom<CursorPaginationRequest> for CursorPagination {
75    type Error = anyhow::Error;
76
77    fn try_from(request: CursorPaginationRequest) -> Result<Self, Self::Error> {
78        let (cursor_start, cursor_end) = match request.cursor.clone() {
79            Some(cursor) => {
80                let decoded = base64::engine::general_purpose::STANDARD
81                    .decode(&cursor)
82                    .context("Failed to decode cursor: {}")?;
83                let decoded_str =
84                    String::from_utf8(decoded).context("Failed to decode cursor as UTF-8: {}")?;
85                let parts: Vec<&str> = decoded_str.split('|').collect();
86
87                if parts.len() != 2 {
88                    (None, None)
89                } else {
90                    (Some(parts[0].to_string()), Some(parts[1].to_string()))
91                }
92            }
93            None => (None, None),
94        };
95
96        if request.newest_first.is_some() && request.cursor.is_some() {
97            return Err(anyhow!(
98                "Cannot have both newest_first and a cursor specified"
99            ));
100        }
101
102        Ok(CursorPagination {
103            cursor_start,
104            cursor_end,
105            limit: request.limit.unwrap_or(DEFAULT_QUERY_LIMIT),
106            newest_first: request.newest_first,
107        })
108    }
109}