actionqueue_daemon/http/
pagination.rs1use axum::http::StatusCode;
9use axum::response::IntoResponse;
10use axum::Json;
11use serde::Serialize;
12
13pub const DEFAULT_LIMIT: usize = 100;
15pub const MAX_LIMIT: usize = 1000;
17
18#[derive(Debug, Clone, Copy)]
20pub struct Pagination {
21 pub limit: usize,
23 pub offset: usize,
25}
26
27#[derive(Debug, Clone)]
29pub struct PaginationError {
30 pub field: &'static str,
32 pub message: String,
34}
35
36impl PaginationError {
37 pub fn new(field: &'static str, message: impl Into<String>) -> Self {
39 Self { field, message: message.into() }
40 }
41}
42
43#[derive(Debug, Clone, Serialize)]
45pub struct PaginationErrorResponse {
46 pub error: &'static str,
48 pub message: String,
50 pub details: PaginationErrorDetails,
52}
53
54#[derive(Debug, Clone, Serialize)]
56pub struct PaginationErrorDetails {
57 pub field: &'static str,
59}
60
61pub fn parse_pagination(raw_query: Option<&str>) -> Result<Pagination, PaginationError> {
63 let mut limit = None;
64 let mut offset = None;
65
66 if let Some(query) = raw_query {
67 for segment in query.split('&') {
68 if segment.is_empty() {
69 continue;
70 }
71
72 let mut parts = segment.splitn(2, '=');
73 let key = parts.next().unwrap_or("");
74 let value = parts.next().unwrap_or("");
75
76 match key {
77 "limit" => {
78 limit = Some(parse_limit(value)?);
79 }
80 "offset" => {
81 offset = Some(parse_offset(value)?);
82 }
83 _ => {}
84 }
85 }
86 }
87
88 Ok(Pagination { limit: limit.unwrap_or(DEFAULT_LIMIT), offset: offset.unwrap_or(0) })
89}
90
91pub fn parse_limit(value: &str) -> Result<usize, PaginationError> {
93 let parsed = parse_non_negative(value, "limit")?;
94 if parsed == 0 || parsed > MAX_LIMIT {
95 return Err(PaginationError::new(
96 "limit",
97 format!("limit must be between 1 and {MAX_LIMIT}"),
98 ));
99 }
100 Ok(parsed)
101}
102
103pub fn parse_offset(value: &str) -> Result<usize, PaginationError> {
105 parse_non_negative(value, "offset")
106}
107
108pub fn parse_non_negative(value: &str, field: &'static str) -> Result<usize, PaginationError> {
110 if value.is_empty() {
111 return Err(PaginationError::new(field, format!("{field} must be a non-negative integer")));
112 }
113
114 value
115 .parse::<usize>()
116 .map_err(|_| PaginationError::new(field, format!("{field} must be a non-negative integer")))
117}
118
119pub fn pagination_error(error: PaginationError) -> impl IntoResponse {
121 let payload = PaginationErrorResponse {
122 error: "invalid_pagination",
123 message: error.message,
124 details: PaginationErrorDetails { field: error.field },
125 };
126
127 (StatusCode::UNPROCESSABLE_ENTITY, Json(payload))
128}