llm_cost_ops_api/api/
pagination.rs

1// Pagination support for API responses
2
3use serde::{Deserialize, Serialize};
4
5/// Pagination parameters
6#[derive(Debug, Clone, Deserialize)]
7pub struct PaginationParams {
8    /// Page number (1-indexed)
9    #[serde(default = "default_page")]
10    pub page: u32,
11
12    /// Page size (number of items per page)
13    #[serde(default = "default_page_size")]
14    pub page_size: u32,
15
16    /// Sort field
17    pub sort_by: Option<String>,
18
19    /// Sort order (asc/desc)
20    #[serde(default)]
21    pub sort_order: SortOrder,
22}
23
24fn default_page() -> u32 {
25    1
26}
27
28fn default_page_size() -> u32 {
29    20
30}
31
32/// Sort order
33#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
34#[serde(rename_all = "lowercase")]
35pub enum SortOrder {
36    #[default]
37    Asc,
38    Desc,
39}
40
41impl PaginationParams {
42    /// Get limit for SQL queries
43    pub fn limit(&self) -> u32 {
44        self.page_size.min(100) // Max 100 items per page
45    }
46
47    /// Get offset for SQL queries
48    pub fn offset(&self) -> u32 {
49        (self.page.saturating_sub(1)) * self.limit()
50    }
51
52    /// Validate pagination parameters
53    pub fn validate(&self) -> Result<(), String> {
54        if self.page == 0 {
55            return Err("Page must be greater than 0".to_string());
56        }
57
58        if self.page_size == 0 {
59            return Err("Page size must be greater than 0".to_string());
60        }
61
62        if self.page_size > 100 {
63            return Err("Page size cannot exceed 100".to_string());
64        }
65
66        Ok(())
67    }
68}
69
70/// Paginated response wrapper
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct PaginatedResponse<T> {
73    /// The data items
74    pub data: Vec<T>,
75
76    /// Pagination metadata
77    pub pagination: PaginationMetadata,
78}
79
80/// Pagination metadata
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PaginationMetadata {
83    /// Current page number (1-indexed)
84    pub page: u32,
85
86    /// Page size (items per page)
87    pub page_size: u32,
88
89    /// Total number of items
90    pub total_items: u64,
91
92    /// Total number of pages
93    pub total_pages: u32,
94
95    /// Whether there is a next page
96    pub has_next: bool,
97
98    /// Whether there is a previous page
99    pub has_prev: bool,
100}
101
102impl PaginationMetadata {
103    /// Create pagination metadata
104    pub fn new(page: u32, page_size: u32, total_items: u64) -> Self {
105        let total_pages = ((total_items as f64) / (page_size as f64)).ceil() as u32;
106        let has_next = page < total_pages;
107        let has_prev = page > 1;
108
109        Self {
110            page,
111            page_size,
112            total_items,
113            total_pages,
114            has_next,
115            has_prev,
116        }
117    }
118}
119
120impl<T> PaginatedResponse<T> {
121    /// Create a new paginated response
122    pub fn new(data: Vec<T>, params: &PaginationParams, total_items: u64) -> Self {
123        Self {
124            data,
125            pagination: PaginationMetadata::new(params.page, params.page_size, total_items),
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_pagination_params_defaults() {
136        let params = PaginationParams {
137            page: default_page(),
138            page_size: default_page_size(),
139            sort_by: None,
140            sort_order: SortOrder::default(),
141        };
142
143        assert_eq!(params.page, 1);
144        assert_eq!(params.page_size, 20);
145        assert_eq!(params.limit(), 20);
146        assert_eq!(params.offset(), 0);
147    }
148
149    #[test]
150    fn test_pagination_offset_calculation() {
151        let params = PaginationParams {
152            page: 3,
153            page_size: 10,
154            sort_by: None,
155            sort_order: SortOrder::Asc,
156        };
157
158        assert_eq!(params.offset(), 20); // (3-1) * 10
159        assert_eq!(params.limit(), 10);
160    }
161
162    #[test]
163    fn test_pagination_max_page_size() {
164        let params = PaginationParams {
165            page: 1,
166            page_size: 200, // Exceeds max
167            sort_by: None,
168            sort_order: SortOrder::Asc,
169        };
170
171        assert_eq!(params.limit(), 100); // Capped at 100
172    }
173
174    #[test]
175    fn test_pagination_validation() {
176        let valid_params = PaginationParams {
177            page: 1,
178            page_size: 20,
179            sort_by: None,
180            sort_order: SortOrder::Asc,
181        };
182        assert!(valid_params.validate().is_ok());
183
184        let invalid_page = PaginationParams {
185            page: 0,
186            page_size: 20,
187            sort_by: None,
188            sort_order: SortOrder::Asc,
189        };
190        assert!(invalid_page.validate().is_err());
191
192        let invalid_page_size = PaginationParams {
193            page: 1,
194            page_size: 0,
195            sort_by: None,
196            sort_order: SortOrder::Asc,
197        };
198        assert!(invalid_page_size.validate().is_err());
199    }
200
201    #[test]
202    fn test_pagination_metadata() {
203        let metadata = PaginationMetadata::new(2, 10, 45);
204
205        assert_eq!(metadata.page, 2);
206        assert_eq!(metadata.page_size, 10);
207        assert_eq!(metadata.total_items, 45);
208        assert_eq!(metadata.total_pages, 5); // ceil(45/10)
209        assert!(metadata.has_next);
210        assert!(metadata.has_prev);
211    }
212
213    #[test]
214    fn test_paginated_response_creation() {
215        let data = vec![1, 2, 3];
216        let params = PaginationParams {
217            page: 1,
218            page_size: 10,
219            sort_by: None,
220            sort_order: SortOrder::Asc,
221        };
222
223        let response = PaginatedResponse::new(data.clone(), &params, 100);
224
225        assert_eq!(response.data.len(), 3);
226        assert_eq!(response.pagination.total_items, 100);
227        assert_eq!(response.pagination.total_pages, 10);
228        assert!(response.pagination.has_next);
229        assert!(!response.pagination.has_prev);
230    }
231}