mas_handlers/admin/
params.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7// Generated code from schemars violates this rule
8#![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    /// # The ID of the resource
45    #[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
62/// The default page size if not specified
63const DEFAULT_PAGE_SIZE: usize = 10;
64
65#[derive(Deserialize, JsonSchema, Clone, Copy, Default, Debug)]
66pub enum IncludeCount {
67    /// Include the total number of items (default)
68    #[default]
69    #[serde(rename = "true")]
70    True,
71
72    /// Do not include the total number of items
73    #[serde(rename = "false")]
74    False,
75
76    /// Only include the total number of items, skip the items themselves
77    #[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            // This is the default, don't add anything
86            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    /// Retrieve the items before the given ID
96    #[serde(rename = "page[before]")]
97    #[schemars(with = "Option<super::schema::Ulid>")]
98    before: Option<Ulid>,
99
100    /// Retrieve the items after the given ID
101    #[serde(rename = "page[after]")]
102    #[schemars(with = "Option<super::schema::Ulid>")]
103    after: Option<Ulid>,
104
105    /// Retrieve the first N items
106    #[serde(rename = "page[first]")]
107    first: Option<NonZeroUsize>,
108
109    /// Retrieve the last N items
110    #[serde(rename = "page[last]")]
111    last: Option<NonZeroUsize>,
112
113    /// Include the total number of items. Defaults to `true`.
114    #[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/// An extractor for pagination parameters in the query string
138#[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        // Figure out the direction and the count out of the first and last parameters
152        let (direction, count) = match (params.first, params.last) {
153            // Make sure we don't specify both first and last
154            (Some(_), Some(_)) => return Err(PaginationRejection::FirstAndLast),
155
156            // Default to forward pagination with a default page size
157            (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}