mas_handlers/admin/
response.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7#![allow(clippy::module_name_repetitions)]
8
9use mas_storage::Pagination;
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    first: String,
25
26    /// The link to the last page of results
27    last: String,
28
29    /// The link to the next page of results
30    ///
31    /// Only present if there is a next page
32    #[serde(skip_serializing_if = "Option::is_none")]
33    next: Option<String>,
34
35    /// The link to the previous page of results
36    ///
37    /// Only present if there is a previous page
38    #[serde(skip_serializing_if = "Option::is_none")]
39    prev: Option<String>,
40}
41
42#[derive(Serialize, JsonSchema)]
43struct PaginationMeta {
44    /// The total number of results
45    count: usize,
46}
47
48/// A top-level response with a page of resources
49#[derive(Serialize, JsonSchema)]
50pub struct PaginatedResponse<T> {
51    /// Response metadata
52    meta: PaginationMeta,
53
54    /// The list of resources
55    data: Vec<SingleResource<T>>,
56
57    /// Related links
58    links: PaginationLinks,
59}
60
61fn url_with_pagination(base: &str, pagination: Pagination) -> String {
62    let (path, query) = base.split_once('?').unwrap_or((base, ""));
63    let mut query = query.to_owned();
64
65    if let Some(before) = pagination.before {
66        query = format!("{query}&page[before]={before}");
67    }
68
69    if let Some(after) = pagination.after {
70        query = format!("{query}&page[after]={after}");
71    }
72
73    let count = pagination.count;
74    match pagination.direction {
75        mas_storage::pagination::PaginationDirection::Forward => {
76            query = format!("{query}&page[first]={count}");
77        }
78        mas_storage::pagination::PaginationDirection::Backward => {
79            query = format!("{query}&page[last]={count}");
80        }
81    }
82
83    // Remove the first '&'
84    let query = query.trim_start_matches('&');
85
86    format!("{path}?{query}")
87}
88
89impl<T: Resource> PaginatedResponse<T> {
90    pub fn new(
91        page: mas_storage::Page<T>,
92        current_pagination: Pagination,
93        count: usize,
94        base: &str,
95    ) -> Self {
96        let links = PaginationLinks {
97            self_: url_with_pagination(base, current_pagination),
98            first: url_with_pagination(base, Pagination::first(current_pagination.count)),
99            last: url_with_pagination(base, Pagination::last(current_pagination.count)),
100            next: page.has_next_page.then(|| {
101                url_with_pagination(
102                    base,
103                    current_pagination
104                        .clear_before()
105                        .after(page.edges.last().unwrap().id()),
106                )
107            }),
108            prev: if page.has_previous_page {
109                Some(url_with_pagination(
110                    base,
111                    current_pagination
112                        .clear_after()
113                        .before(page.edges.first().unwrap().id()),
114                ))
115            } else {
116                None
117            },
118        };
119
120        let data = page.edges.into_iter().map(SingleResource::new).collect();
121
122        Self {
123            meta: PaginationMeta { count },
124            data,
125            links,
126        }
127    }
128}
129
130/// A single resource, with its type, ID, attributes and related links
131#[derive(Serialize, JsonSchema)]
132struct SingleResource<T> {
133    /// The type of the resource
134    #[serde(rename = "type")]
135    type_: &'static str,
136
137    /// The ID of the resource
138    #[schemars(with = "super::schema::Ulid")]
139    id: Ulid,
140
141    /// The attributes of the resource
142    attributes: T,
143
144    /// Related links
145    links: SelfLinks,
146}
147
148impl<T: Resource> SingleResource<T> {
149    fn new(resource: T) -> Self {
150        let self_ = resource.path();
151        Self {
152            type_: T::KIND,
153            id: resource.id(),
154            attributes: resource,
155            links: SelfLinks { self_ },
156        }
157    }
158}
159
160/// Related links
161#[derive(Serialize, JsonSchema)]
162struct SelfLinks {
163    /// The canonical link to the current resource
164    #[serde(rename = "self")]
165    self_: String,
166}
167
168/// A top-level response with a single resource
169#[derive(Serialize, JsonSchema)]
170pub struct SingleResponse<T> {
171    data: SingleResource<T>,
172    links: SelfLinks,
173}
174
175impl<T: Resource> SingleResponse<T> {
176    /// Create a new single response with the given resource and link to itself
177    pub fn new(resource: T, self_: String) -> Self {
178        Self {
179            data: SingleResource::new(resource),
180            links: SelfLinks { self_ },
181        }
182    }
183
184    /// Create a new single response using the canonical path for the resource
185    pub fn new_canonical(resource: T) -> Self {
186        let self_ = resource.path();
187        Self::new(resource, self_)
188    }
189}
190
191/// A single error
192#[derive(Serialize, JsonSchema)]
193struct Error {
194    /// A human-readable title for the error
195    title: String,
196}
197
198impl Error {
199    fn from_error(error: &(dyn std::error::Error + 'static)) -> Self {
200        Self {
201            title: error.to_string(),
202        }
203    }
204}
205
206/// A top-level response with a list of errors
207#[derive(Serialize, JsonSchema)]
208pub struct ErrorResponse {
209    /// The list of errors
210    errors: Vec<Error>,
211}
212
213impl ErrorResponse {
214    /// Create a new error response from any Rust error
215    pub fn from_error(error: &(dyn std::error::Error + 'static)) -> Self {
216        let mut errors = Vec::new();
217        let mut head = Some(error);
218        while let Some(error) = head {
219            errors.push(Error::from_error(error));
220            head = error.source();
221        }
222        Self { errors }
223    }
224}