1use serde::{Deserialize, Serialize};
4use std::fmt;
5#[cfg(feature = "openapi")]
6use utoipa::ToSchema;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct PaginationParams {
12 #[serde(default)]
13 pub page: u32,
14 #[serde(default = "default_page_size")]
15 pub page_size: u32,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[cfg_attr(feature = "openapi", derive(ToSchema))]
20#[serde(rename_all = "camelCase")]
21pub struct PaginatedResponse<T> {
22 pub data: Vec<T>,
23 pub pagination: PaginationMeta,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct PaginationMeta {
29 pub total: u64,
30 pub page: u32,
31 pub page_size: u32,
32 pub total_pages: u32,
33 pub has_next: bool,
34 pub has_previous: bool,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "lowercase")]
40pub enum SortOrder {
41 Asc,
42 Desc,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct SortParams {
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub sort_by: Option<String>,
50 #[serde(default = "default_sort_order")]
51 pub sort_order: SortOrder,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct TimeRange {
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub start: Option<chrono::DateTime<chrono::Utc>>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub end: Option<chrono::DateTime<chrono::Utc>>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[cfg_attr(feature = "openapi", derive(ToSchema))]
67#[serde(rename_all = "camelCase")]
68pub struct ApiError {
69 pub code: String,
70 pub message: String,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub details: Option<ErrorDetails>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub request_id: Option<String>,
75 pub timestamp: chrono::DateTime<chrono::Utc>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[cfg_attr(feature = "openapi", derive(ToSchema))]
80#[serde(rename_all = "camelCase")]
81pub struct ErrorDetails {
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub field: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub reason: Option<String>,
86 #[serde(skip_serializing_if = "Vec::is_empty")]
87 pub validation_errors: Vec<ValidationError>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[cfg_attr(feature = "openapi", derive(ToSchema))]
92#[serde(rename_all = "camelCase")]
93pub struct ValidationError {
94 pub field: String,
95 pub code: String,
96 pub message: String,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101#[cfg_attr(feature = "openapi", derive(ToSchema))]
102#[serde(rename_all = "camelCase")]
103pub struct BatchResult<T, E> {
104 pub successful: Vec<T>,
105 pub failed: Vec<BatchError<E>>,
106 pub total: usize,
107 pub success_count: usize,
108 pub failure_count: usize,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[cfg_attr(feature = "openapi", derive(ToSchema))]
113#[serde(rename_all = "camelCase")]
114pub struct BatchError<E> {
115 pub index: usize,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub id: Option<String>,
118 pub error: E,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123#[cfg_attr(feature = "openapi", derive(ToSchema))]
124#[serde(rename_all = "camelCase")]
125pub struct ApiResponse<T> {
126 pub success: bool,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub data: Option<T>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub error: Option<ApiError>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub metadata: Option<ResponseMetadata>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct ResponseMetadata {
138 pub request_id: String,
139 pub timestamp: chrono::DateTime<chrono::Utc>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub version: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub processing_time_ms: Option<u64>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct SearchParams {
150 pub query: String,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub fields: Option<Vec<String>>,
153 #[serde(default)]
154 pub fuzzy: bool,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub highlight: Option<bool>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct FilterParams {
162 #[serde(flatten)]
163 pub filters: serde_json::Map<String, serde_json::Value>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
168#[cfg_attr(feature = "openapi", derive(ToSchema))]
169pub struct Id(pub String);
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct ListRequest {
175 #[serde(flatten)]
176 pub pagination: PaginationParams,
177 #[serde(flatten)]
178 pub sort: SortParams,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub search: Option<SearchParams>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub time_range: Option<TimeRange>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub filters: Option<FilterParams>,
185}
186
187fn default_page_size() -> u32 {
189 20
190}
191
192fn default_sort_order() -> SortOrder {
193 SortOrder::Asc
194}
195
196impl Default for PaginationParams {
198 fn default() -> Self {
199 Self {
200 page: 0,
201 page_size: default_page_size(),
202 }
203 }
204}
205
206impl PaginationParams {
207 pub fn new(page: u32, page_size: u32) -> Self {
208 Self { page, page_size }
209 }
210
211 pub fn offset(&self) -> u64 {
212 (self.page as u64) * (self.page_size as u64)
213 }
214
215 pub fn limit(&self) -> u64 {
216 self.page_size as u64
217 }
218}
219
220impl PaginationMeta {
221 pub fn new(total: u64, page: u32, page_size: u32) -> Self {
222 let total_pages = ((total as f64) / (page_size as f64)).ceil() as u32;
223 Self {
224 total,
225 page,
226 page_size,
227 total_pages,
228 has_next: page < total_pages.saturating_sub(1),
229 has_previous: page > 0,
230 }
231 }
232}
233
234impl<T> PaginatedResponse<T> {
235 pub fn new(data: Vec<T>, total: u64, page: u32, page_size: u32) -> Self {
236 Self {
237 data,
238 pagination: PaginationMeta::new(total, page, page_size),
239 }
240 }
241}
242
243impl Default for SortOrder {
244 fn default() -> Self {
245 Self::Asc
246 }
247}
248
249impl fmt::Display for SortOrder {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 match self {
252 SortOrder::Asc => write!(f, "asc"),
253 SortOrder::Desc => write!(f, "desc"),
254 }
255 }
256}
257
258impl Default for SortParams {
259 fn default() -> Self {
260 Self {
261 sort_by: None,
262 sort_order: default_sort_order(),
263 }
264 }
265}
266
267impl ApiError {
268 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
269 Self {
270 code: code.into(),
271 message: message.into(),
272 details: None,
273 request_id: None,
274 timestamp: chrono::Utc::now(),
275 }
276 }
277
278 pub fn with_field(mut self, field: impl Into<String>) -> Self {
279 self.details = Some(ErrorDetails {
280 field: Some(field.into()),
281 reason: None,
282 validation_errors: Vec::new(),
283 });
284 self
285 }
286
287 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
288 self.request_id = Some(request_id.into());
289 self
290 }
291}
292
293impl<T> ApiResponse<T> {
294 pub fn success(data: T) -> Self {
295 Self {
296 success: true,
297 data: Some(data),
298 error: None,
299 metadata: None,
300 }
301 }
302
303 pub fn error(error: ApiError) -> Self {
304 Self {
305 success: false,
306 data: None,
307 error: Some(error),
308 metadata: None,
309 }
310 }
311
312 pub fn with_metadata(mut self, metadata: ResponseMetadata) -> Self {
313 self.metadata = Some(metadata);
314 self
315 }
316}
317
318impl<T, E> BatchResult<T, E> {
319 pub fn new(successful: Vec<T>, failed: Vec<BatchError<E>>) -> Self {
320 let success_count = successful.len();
321 let failure_count = failed.len();
322 Self {
323 successful,
324 failed,
325 total: success_count + failure_count,
326 success_count,
327 failure_count,
328 }
329 }
330
331 pub fn all_succeeded(&self) -> bool {
332 self.failure_count == 0
333 }
334
335 pub fn all_failed(&self) -> bool {
336 self.success_count == 0
337 }
338}
339
340impl fmt::Display for Id {
341 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342 write!(f, "{}", self.0)
343 }
344}
345
346impl From<String> for Id {
347 fn from(s: String) -> Self {
348 Id(s)
349 }
350}
351
352impl From<&str> for Id {
353 fn from(s: &str) -> Self {
354 Id(s.to_string())
355 }
356}
357
358impl TimeRange {
359 pub fn new(start: Option<chrono::DateTime<chrono::Utc>>, end: Option<chrono::DateTime<chrono::Utc>>) -> Self {
360 Self { start, end }
361 }
362
363 pub fn is_valid(&self) -> bool {
364 match (self.start, self.end) {
365 (Some(start), Some(end)) => start <= end,
366 _ => true,
367 }
368 }
369}
370
371pub use chrono::{DateTime, Utc};