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}