Skip to main content

mas_handlers/admin/
response.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#![allow(clippy::module_name_repetitions)]
8
9use mas_storage::{Pagination, pagination::Edge};
10use schemars::JsonSchema;
11use serde::Serialize;
12use ulid::Ulid;
13
14use super::model::Resource;
15
16/// Related links
17#[derive(Serialize, JsonSchema)]
18struct PaginationLinks {
19    /// The canonical link to the current page
20    #[serde(rename = "self")]
21    self_: String,
22
23    /// The link to the first page of results
24    #[serde(skip_serializing_if = "Option::is_none")]
25    first: Option<String>,
26
27    /// The link to the last page of results
28    #[serde(skip_serializing_if = "Option::is_none")]
29    last: Option<String>,
30
31    /// The link to the next page of results
32    ///
33    /// Only present if there is a next page
34    #[serde(skip_serializing_if = "Option::is_none")]
35    next: Option<String>,
36
37    /// The link to the previous page of results
38    ///
39    /// Only present if there is a previous page
40    #[serde(skip_serializing_if = "Option::is_none")]
41    prev: Option<String>,
42}
43
44#[derive(Serialize, JsonSchema)]
45struct PaginationMeta {
46    /// The total number of results
47    #[serde(skip_serializing_if = "Option::is_none")]
48    count: Option<usize>,
49}
50
51impl PaginationMeta {
52    fn is_empty(&self) -> bool {
53        self.count.is_none()
54    }
55}
56
57/// A top-level response with a page of resources
58#[derive(Serialize, JsonSchema)]
59#[schemars(rename = "PaginatedResponse_for_{T}")]
60pub struct PaginatedResponse<T> {
61    /// Response metadata
62    #[serde(skip_serializing_if = "PaginationMeta::is_empty")]
63    #[schemars(with = "Option<PaginationMeta>")]
64    meta: PaginationMeta,
65
66    /// The list of resources
67    #[serde(skip_serializing_if = "Option::is_none")]
68    data: Option<Vec<SingleResource<T>>>,
69
70    /// Related links
71    links: PaginationLinks,
72}
73
74fn url_with_pagination(base: &str, pagination: Pagination) -> String {
75    let (path, query) = base.split_once('?').unwrap_or((base, ""));
76    let mut query = query.to_owned();
77
78    if let Some(before) = pagination.before {
79        query = format!("{query}&page[before]={before}");
80    }
81
82    if let Some(after) = pagination.after {
83        query = format!("{query}&page[after]={after}");
84    }
85
86    let count = pagination.count;
87    match pagination.direction {
88        mas_storage::pagination::PaginationDirection::Forward => {
89            query = format!("{query}&page[first]={count}");
90        }
91        mas_storage::pagination::PaginationDirection::Backward => {
92            query = format!("{query}&page[last]={count}");
93        }
94    }
95
96    // Remove the first '&'
97    let query = query.trim_start_matches('&');
98
99    format!("{path}?{query}")
100}
101
102impl<T: Resource> PaginatedResponse<T> {
103    pub fn for_page(
104        page: mas_storage::Page<T>,
105        current_pagination: Pagination,
106        count: Option<usize>,
107        base: &str,
108    ) -> Self {
109        let links = PaginationLinks {
110            self_: url_with_pagination(base, current_pagination),
111            first: Some(url_with_pagination(
112                base,
113                Pagination::first(current_pagination.count),
114            )),
115            last: Some(url_with_pagination(
116                base,
117                Pagination::last(current_pagination.count),
118            )),
119            next: page.has_next_page.then(|| {
120                url_with_pagination(
121                    base,
122                    current_pagination
123                        .clear_before()
124                        .after(page.edges.last().unwrap().cursor),
125                )
126            }),
127            prev: if page.has_previous_page {
128                Some(url_with_pagination(
129                    base,
130                    current_pagination
131                        .clear_after()
132                        .before(page.edges.first().unwrap().cursor),
133                ))
134            } else {
135                None
136            },
137        };
138
139        let data = page
140            .edges
141            .into_iter()
142            .map(SingleResource::from_edge)
143            .collect();
144
145        Self {
146            meta: PaginationMeta { count },
147            data: Some(data),
148            links,
149        }
150    }
151
152    pub fn for_count_only(count: usize, base: &str) -> Self {
153        let links = PaginationLinks {
154            self_: base.to_owned(),
155            first: None,
156            last: None,
157            next: None,
158            prev: None,
159        };
160
161        Self {
162            meta: PaginationMeta { count: Some(count) },
163            data: None,
164            links,
165        }
166    }
167}
168
169/// A single resource, with its type, ID, attributes and related links
170#[derive(Serialize, JsonSchema)]
171#[schemars(rename = "SingleResource_for_{T}")]
172struct SingleResource<T> {
173    /// The type of the resource
174    #[serde(rename = "type")]
175    type_: &'static str,
176
177    /// The ID of the resource
178    #[schemars(with = "super::schema::Ulid")]
179    id: Ulid,
180
181    /// The attributes of the resource
182    attributes: T,
183
184    /// Related links
185    links: SelfLinks,
186
187    /// Metadata about the resource
188    #[serde(skip_serializing_if = "SingleResourceMeta::is_empty")]
189    #[schemars(with = "Option<SingleResourceMeta>")]
190    meta: SingleResourceMeta,
191}
192
193/// Metadata associated with a resource
194#[derive(Serialize, JsonSchema)]
195struct SingleResourceMeta {
196    /// Information about the pagination of the resource
197    #[serde(skip_serializing_if = "Option::is_none")]
198    page: Option<SingleResourceMetaPage>,
199}
200
201impl SingleResourceMeta {
202    fn is_empty(&self) -> bool {
203        self.page.is_none()
204    }
205}
206
207/// Pagination metadata for a resource
208#[derive(Serialize, JsonSchema)]
209struct SingleResourceMetaPage {
210    /// The cursor of this resource in the paginated result
211    cursor: String,
212}
213
214impl<T: Resource> SingleResource<T> {
215    fn new(resource: T) -> Self {
216        let self_ = resource.path();
217        Self {
218            type_: T::KIND,
219            id: resource.id(),
220            attributes: resource,
221            links: SelfLinks { self_ },
222            meta: SingleResourceMeta { page: None },
223        }
224    }
225
226    fn from_edge<C: ToString>(edge: Edge<T, C>) -> Self {
227        let cursor = edge.cursor.to_string();
228        let mut resource = Self::new(edge.node);
229        resource.meta.page = Some(SingleResourceMetaPage { cursor });
230        resource
231    }
232}
233
234/// Related links
235#[derive(Serialize, JsonSchema)]
236struct SelfLinks {
237    /// The canonical link to the current resource
238    #[serde(rename = "self")]
239    self_: String,
240}
241
242/// A top-level response with a single resource
243#[derive(Serialize, JsonSchema)]
244#[schemars(rename = "SingleResponse_for_{T}")]
245pub struct SingleResponse<T> {
246    data: SingleResource<T>,
247    links: SelfLinks,
248}
249
250impl<T: Resource> SingleResponse<T> {
251    /// Create a new single response with the given resource and link to itself
252    pub fn new(resource: T, self_: String) -> Self {
253        Self {
254            data: SingleResource::new(resource),
255            links: SelfLinks { self_ },
256        }
257    }
258
259    /// Create a new single response using the canonical path for the resource
260    pub fn new_canonical(resource: T) -> Self {
261        let self_ = resource.path();
262        Self::new(resource, self_)
263    }
264}
265
266/// A single error
267#[derive(Serialize, JsonSchema)]
268struct Error {
269    /// A human-readable title for the error
270    title: String,
271}
272
273impl Error {
274    fn from_error(error: &(dyn std::error::Error + 'static)) -> Self {
275        Self {
276            title: error.to_string(),
277        }
278    }
279}
280
281/// A top-level response with a list of errors
282#[derive(Serialize, JsonSchema)]
283pub struct ErrorResponse {
284    /// The list of errors
285    errors: Vec<Error>,
286}
287
288impl ErrorResponse {
289    /// Create a new error response from any Rust error
290    pub fn from_error(error: &(dyn std::error::Error + 'static)) -> Self {
291        let mut errors = Vec::new();
292        let mut head = Some(error);
293        while let Some(error) = head {
294            errors.push(Error::from_error(error));
295            head = error.source();
296        }
297        Self { errors }
298    }
299}