1use crate::{ApiSchema, Result};
3use serde::{Deserialize, Serialize};
4use std::future::Future;
5#[cfg(all(debug_assertions, feature = "openapi"))]
6use utoipa::{IntoParams, ToSchema};
7pub const DEFAULT_FOLDER_LIMIT: u64 = 200;
9pub const DEFAULT_FILE_LIMIT: u64 = 100;
11pub const DEFAULT_PAGE_LIMIT: u64 = 100;
13pub const MAX_PAGE_SIZE: u64 = 1000;
15
16#[derive(Debug, Clone, Copy, Default, Deserialize)]
18#[cfg_attr(
19 all(debug_assertions, feature = "openapi"),
20 derive(IntoParams, ToSchema)
21)]
22pub struct LimitOffsetQuery {
23 pub limit: Option<u64>,
25 pub offset: Option<u64>,
27}
28
29impl LimitOffsetQuery {
30 #[must_use]
32 pub fn limit_or(&self, default: u64, max: u64) -> u64 {
33 self.limit.map_or(default, |v| v.clamp(1, max))
34 }
35
36 #[must_use]
38 pub fn offset(&self) -> u64 {
39 self.offset.unwrap_or(0)
40 }
41}
42
43#[derive(Debug, Clone, Copy, Default, Deserialize)]
45#[cfg_attr(
46 all(debug_assertions, feature = "openapi"),
47 derive(IntoParams, ToSchema)
48)]
49pub struct LimitQuery {
50 pub limit: Option<u64>,
52}
53
54impl LimitQuery {
55 #[must_use]
57 pub fn limit_or(&self, default: u64, max: u64) -> u64 {
58 self.limit.map_or(default, |value| value.clamp(1, max))
59 }
60
61 #[must_use]
63 pub fn limit(&self) -> u64 {
64 self.limit_or(DEFAULT_PAGE_LIMIT, MAX_PAGE_SIZE)
65 }
66}
67
68#[derive(Debug, Clone, Serialize)]
70#[cfg_attr(all(debug_assertions, feature = "openapi"), derive(ToSchema))]
71pub struct OffsetPage<T: Serialize + ApiSchema> {
72 pub items: Vec<T>,
74 pub total: u64,
76 pub limit: u64,
78 pub offset: u64,
80}
81
82impl<T: Serialize + ApiSchema> OffsetPage<T> {
83 #[must_use]
85 pub fn new(items: Vec<T>, total: u64, limit: u64, offset: u64) -> Self {
86 Self {
87 items,
88 total,
89 limit,
90 offset,
91 }
92 }
93}
94
95pub async fn load_offset_page<T, F, Fut>(
101 limit: u64,
102 offset: u64,
103 max_limit: u64,
104 fetch: F,
105) -> Result<OffsetPage<T>>
106where
107 T: Serialize + ApiSchema,
108 F: FnOnce(u64, u64) -> Fut,
109 Fut: Future<Output = Result<(Vec<T>, u64)>>,
110{
111 let limit = limit.clamp(1, max_limit);
112 let (items, total) = fetch(limit, offset).await?;
113 Ok(OffsetPage::new(items, total, limit, offset))
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::ApiError;
120 use serde_json::json;
121
122 #[test]
123 fn queries_preserve_large_offsets_and_clamp_limits_without_integer_loss() {
124 let query = LimitOffsetQuery {
125 limit: Some(u64::MAX),
126 offset: Some(u64::MAX),
127 };
128 assert_eq!(query.limit_or(20, MAX_PAGE_SIZE), MAX_PAGE_SIZE);
129 assert_eq!(query.offset(), u64::MAX);
130 assert_eq!(
131 LimitQuery {
132 limit: Some(u64::MAX)
133 }
134 .limit(),
135 MAX_PAGE_SIZE
136 );
137 assert_eq!(LimitQuery { limit: Some(0) }.limit_or(20, 1), 1);
138 let page = OffsetPage::new(Vec::<u8>::new(), u64::MAX, MAX_PAGE_SIZE, u64::MAX);
139 assert_eq!(
140 serde_json::to_value(page).unwrap(),
141 json!({"items":[],"total":u64::MAX,"limit":MAX_PAGE_SIZE,"offset":u64::MAX})
142 );
143 }
144 #[test]
145 fn limit_offset_query_applies_defaults_and_bounds() {
146 let query = LimitOffsetQuery {
147 limit: None,
148 offset: None,
149 };
150 assert_eq!(query.limit_or(50, 100), 50);
151 assert_eq!(query.offset(), 0);
152
153 let query = LimitOffsetQuery {
154 limit: Some(0),
155 offset: Some(25),
156 };
157 assert_eq!(query.limit_or(50, 100), 1);
158 assert_eq!(query.offset(), 25);
159
160 let query = LimitOffsetQuery {
161 limit: Some(500),
162 offset: Some(10),
163 };
164 assert_eq!(query.limit_or(50, 100), 100);
165 }
166
167 #[test]
168 fn limit_query_applies_defaults_and_bounds() {
169 let query = LimitQuery { limit: None };
170 assert_eq!(query.limit_or(50, 100), 50);
171 assert_eq!(query.limit(), DEFAULT_PAGE_LIMIT);
172
173 let query = LimitQuery { limit: Some(0) };
174 assert_eq!(query.limit_or(50, 100), 1);
175
176 let query = LimitQuery { limit: Some(500) };
177 assert_eq!(query.limit_or(50, 100), 100);
178 }
179
180 #[test]
181 fn offset_page_serializes_expected_shape() {
182 let page = OffsetPage::new(vec!["a", "b"], 10, 2, 4);
183 let value = serde_json::to_value(page).unwrap();
184
185 assert_eq!(
186 value,
187 json!({
188 "items": ["a", "b"],
189 "total": 10,
190 "limit": 2,
191 "offset": 4
192 })
193 );
194 }
195
196 #[tokio::test]
197 async fn load_offset_page_clamps_limit_and_forwards_offset() {
198 let page = load_offset_page(500, 30, 100, |limit, offset| async move {
199 assert_eq!(limit, 100);
200 assert_eq!(offset, 30);
201 Ok((vec![1, 2, 3], 9))
202 })
203 .await
204 .unwrap();
205
206 assert_eq!(page.items, vec![1, 2, 3]);
207 assert_eq!(page.total, 9);
208 assert_eq!(page.limit, 100);
209 assert_eq!(page.offset, 30);
210 }
211
212 #[tokio::test]
213 async fn load_offset_page_propagates_fetch_error() {
214 let error = load_offset_page::<u8, _, _>(10, 0, 100, |_limit, _offset| async {
215 Err(ApiError::new("fetch failed"))
216 })
217 .await
218 .unwrap_err();
219
220 assert_eq!(error.message(), "fetch failed");
221 }
222}