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