mas_handlers/admin/
params.rs1#![allow(clippy::str_to_string)]
9
10use std::{borrow::Cow, num::NonZeroUsize};
11
12use aide::OperationIo;
13use axum::{
14 Json,
15 extract::{FromRequestParts, Path, rejection::PathRejection},
16 response::IntoResponse,
17};
18use axum_extra::extract::{Query, QueryRejection};
19use axum_macros::FromRequestParts;
20use hyper::StatusCode;
21use mas_storage::pagination::PaginationDirection;
22use schemars::JsonSchema;
23use serde::Deserialize;
24use ulid::Ulid;
25
26use super::response::ErrorResponse;
27
28#[derive(Debug, thiserror::Error)]
29#[error("Invalid ULID in path")]
30pub struct UlidPathParamRejection(#[from] PathRejection);
31
32impl IntoResponse for UlidPathParamRejection {
33 fn into_response(self) -> axum::response::Response {
34 (
35 StatusCode::BAD_REQUEST,
36 Json(ErrorResponse::from_error(&self)),
37 )
38 .into_response()
39 }
40}
41
42#[derive(JsonSchema, Debug, Clone, Copy, Deserialize)]
43struct UlidInPath {
44 #[schemars(with = "super::schema::Ulid")]
46 id: Ulid,
47}
48
49#[derive(FromRequestParts, OperationIo, Debug, Clone, Copy)]
50#[from_request(rejection(UlidPathParamRejection))]
51#[aide(input_with = "Path<UlidInPath>")]
52pub struct UlidPathParam(#[from_request(via(Path))] UlidInPath);
53
54impl std::ops::Deref for UlidPathParam {
55 type Target = Ulid;
56
57 fn deref(&self) -> &Self::Target {
58 &self.0.id
59 }
60}
61
62const DEFAULT_PAGE_SIZE: usize = 10;
64
65#[derive(Deserialize, JsonSchema, Clone, Copy, Default, Debug)]
66pub enum IncludeCount {
67 #[default]
69 #[serde(rename = "true")]
70 True,
71
72 #[serde(rename = "false")]
74 False,
75
76 #[serde(rename = "only")]
78 Only,
79}
80
81impl IncludeCount {
82 pub(crate) fn add_to_base(self, base: &str) -> Cow<'_, str> {
83 let separator = if base.contains('?') { '&' } else { '?' };
84 match self {
85 Self::True => Cow::Borrowed(base),
87 Self::False => format!("{base}{separator}count=false").into(),
88 Self::Only => format!("{base}{separator}count=only").into(),
89 }
90 }
91}
92
93#[derive(Deserialize, JsonSchema, Clone, Copy)]
94struct PaginationParams {
95 #[serde(rename = "page[before]")]
97 #[schemars(with = "Option<super::schema::Ulid>")]
98 before: Option<Ulid>,
99
100 #[serde(rename = "page[after]")]
102 #[schemars(with = "Option<super::schema::Ulid>")]
103 after: Option<Ulid>,
104
105 #[serde(rename = "page[first]")]
107 first: Option<NonZeroUsize>,
108
109 #[serde(rename = "page[last]")]
111 last: Option<NonZeroUsize>,
112
113 #[serde(rename = "count")]
115 include_count: Option<IncludeCount>,
116}
117
118#[derive(Debug, thiserror::Error)]
119pub enum PaginationRejection {
120 #[error("Invalid pagination parameters")]
121 Invalid(#[from] QueryRejection),
122
123 #[error("Cannot specify both `page[first]` and `page[last]` parameters")]
124 FirstAndLast,
125}
126
127impl IntoResponse for PaginationRejection {
128 fn into_response(self) -> axum::response::Response {
129 (
130 StatusCode::BAD_REQUEST,
131 Json(ErrorResponse::from_error(&self)),
132 )
133 .into_response()
134 }
135}
136
137#[derive(OperationIo, Debug, Clone, Copy)]
139#[aide(input_with = "Query<PaginationParams>")]
140pub struct Pagination(pub mas_storage::Pagination, pub IncludeCount);
141
142impl<S: Send + Sync> FromRequestParts<S> for Pagination {
143 type Rejection = PaginationRejection;
144
145 async fn from_request_parts(
146 parts: &mut axum::http::request::Parts,
147 state: &S,
148 ) -> Result<Self, Self::Rejection> {
149 let params = Query::<PaginationParams>::from_request_parts(parts, state).await?;
150
151 let (direction, count) = match (params.first, params.last) {
153 (Some(_), Some(_)) => return Err(PaginationRejection::FirstAndLast),
155
156 (None, None) => (PaginationDirection::Forward, DEFAULT_PAGE_SIZE),
158
159 (Some(first), None) => (PaginationDirection::Forward, first.into()),
160 (None, Some(last)) => (PaginationDirection::Backward, last.into()),
161 };
162
163 Ok(Self(
164 mas_storage::Pagination {
165 before: params.before,
166 after: params.after,
167 direction,
168 count,
169 },
170 params.include_count.unwrap_or_default(),
171 ))
172 }
173}