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