degen_sql/
pagination.rs

1use crate::tiny_safe_string::TinySafeString;
2use serde::{Deserialize, Serialize, Serializer, Deserializer};
3use std::fmt;
4use serde::de::{self, Visitor};
5
6#[cfg_attr(feature = "utoipa-schema", derive(utoipa::ToSchema))]
7#[derive(Debug, Clone)]
8pub struct PaginationData {
9    pub page: Option<i64>,       // Current page (1-indexed)
10    pub page_size: Option<i64>,  // Number of items per page
11    pub sort_by: Option<TinySafeString>, // Column to sort by
12    pub sort_dir: Option<ColumnSortDir>, // Sort direction (asc/desc)
13}
14
15#[cfg_attr(feature = "utoipa-schema", derive(utoipa::ToSchema))]
16#[derive(Debug, Clone)]
17pub enum ColumnSortDir { 
18    Asc,
19    Desc
20}
21
22// Custom serialization for ColumnSortDir
23impl Serialize for ColumnSortDir {
24    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
25    where
26        S: Serializer,
27    {
28        match self {
29            ColumnSortDir::Asc => serializer.serialize_str("asc"),
30            ColumnSortDir::Desc => serializer.serialize_str("desc"),
31        }
32    }
33}
34
35// Custom deserialization for ColumnSortDir
36impl<'de> Deserialize<'de> for ColumnSortDir {
37    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38    where
39        D: Deserializer<'de>,
40    {
41        struct ColumnSortDirVisitor;
42        impl<'de> Visitor<'de> for ColumnSortDirVisitor {
43            type Value = ColumnSortDir;
44            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
45                formatter.write_str("a string representing sort direction: 'asc' or 'desc'")
46            }
47            fn visit_str<E>(self, value: &str) -> Result<ColumnSortDir, E>
48            where
49                E: de::Error,
50            {
51                match value.to_lowercase().as_str() {
52                    "asc" => Ok(ColumnSortDir::Asc),
53                    "desc" => Ok(ColumnSortDir::Desc),
54                    _ => Ok(ColumnSortDir::Desc), // Default to DESC for invalid values
55                }
56            }
57        }
58        deserializer.deserialize_str(ColumnSortDirVisitor)
59    }
60}
61
62// Add Serialize for the whole PaginationData struct
63impl Serialize for PaginationData {
64    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
65    where
66        S: Serializer,
67    {
68        use serde::ser::SerializeStruct;
69        let mut state = serializer.serialize_struct("PaginationData", 4)?;
70        state.serialize_field("page", &self.page)?;
71        state.serialize_field("page_size", &self.page_size)?;
72        state.serialize_field("sort_by", &self.sort_by)?;
73        state.serialize_field("sort_dir", &self.sort_dir)?;
74        state.end()
75    }
76}
77
78// Add Deserialize for the whole PaginationData struct
79impl<'de> Deserialize<'de> for PaginationData {
80    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
81    where
82        D: Deserializer<'de>,
83    {
84        #[derive(Deserialize)]
85        struct PaginationDataHelper {
86            page: Option<i64>,
87            page_size: Option<i64>,
88            sort_by: Option<TinySafeString>,
89            sort_dir: Option<ColumnSortDir>,
90        }
91        let helper = PaginationDataHelper::deserialize(deserializer)?;
92        
93        Ok(PaginationData {
94            page: helper.page,
95            page_size: helper.page_size,
96            sort_by: helper.sort_by,
97            sort_dir: helper.sort_dir,
98        })
99    }
100}
101
102impl Default for PaginationData {
103    fn default() -> Self {
104        Self {
105            page: Some(1),
106            page_size: Some(10),
107            sort_by: Some("created_at".try_into().unwrap()),
108            sort_dir: Some(ColumnSortDir::Desc),
109        }
110    }
111}
112
113impl ColumnSortDir {
114    // Convert enum to SQL direction
115    pub fn to_sql_string(&self) -> &'static str {
116        match self {
117            ColumnSortDir::Asc => "ASC",
118            ColumnSortDir::Desc => "DESC",
119        }
120    }
121}
122
123impl PaginationData {
124    // Get SQL limit clause
125    pub fn get_limit(&self) -> i64 {
126        self.page_size.unwrap_or(10).min(100) // Limit maximum page size to 100
127    }
128    
129    // Get SQL offset clause
130    pub fn get_offset(&self) -> i64 {
131        let page = self.page.unwrap_or(1).max(1) - 1; // 1-indexed to 0-indexed
132        page * self.get_limit()
133    }
134    
135    // Get SQL order by clause
136    pub fn get_order_by(&self) -> String {
137        let column = match &self.sort_by {
138            Some(col) => col.to_sql_string(),
139            None => "created_at", // Default column
140        };
141        
142        let direction = match &self.sort_dir {
143            Some(dir) => dir.to_sql_string(),
144            None => "DESC", // Default direction
145        };
146        
147        format!("{} {}", column, direction)
148    }
149    
150    // Build the SQL pagination part
151    pub fn build_query_part(&self) -> String {
152        format!(
153            "ORDER BY {} LIMIT {} OFFSET {}",
154            self.get_order_by(),
155            self.get_limit(),
156            self.get_offset()
157        )
158    }
159}
160
161#[cfg_attr(feature = "utoipa-schema", derive(utoipa::ToSchema))]
162#[derive(Serialize)]
163pub struct PaginatedResponse<T> {
164    pub items: Vec<T>,
165    pub total_count: i64,
166    pub page: i64,
167    pub page_size: i64,
168    pub total_pages: i64,
169}