Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit c177233

Browse files
committed
Define common response types for the admin API
This adds a Single and a Paginated response type, which have links to the next, previous, first and last pages.
1 parent 27ca7ec commit c177233

File tree

3 files changed

+215
-0
lines changed

3 files changed

+215
-0
lines changed

crates/handlers/src/admin/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use mas_router::{OAuth2AuthorizationEndpoint, OAuth2TokenEndpoint, SimpleRoute};
2424
use tower_http::cors::{Any, CorsLayer};
2525

2626
mod call_context;
27+
mod model;
2728
mod response;
2829

2930
use self::call_context::CallContext;

crates/handlers/src/admin/model.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2024 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use ulid::Ulid;
16+
17+
/// A resource, with a type and an ID
18+
#[allow(dead_code)]
19+
pub trait Resource {
20+
/// The type of the resource
21+
const KIND: &'static str;
22+
23+
/// The canonical path prefix for this kind of resource
24+
const PATH: &'static str;
25+
26+
/// The ID of the resource
27+
fn id(&self) -> Ulid;
28+
29+
/// The canonical path for this resource
30+
///
31+
/// This is the concatenation of the canonical path prefix and the ID
32+
fn path(&self) -> String {
33+
format!("{}/{}", Self::PATH, self.id())
34+
}
35+
}

crates/handlers/src/admin/response.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,187 @@
1414

1515
#![allow(clippy::module_name_repetitions)]
1616

17+
use mas_storage::Pagination;
1718
use schemars::JsonSchema;
1819
use serde::Serialize;
20+
use ulid::Ulid;
21+
22+
use super::model::Resource;
23+
24+
/// Related links
25+
#[derive(Serialize, JsonSchema)]
26+
struct PaginationLinks {
27+
/// The canonical link to the current page
28+
#[serde(rename = "self")]
29+
self_: String,
30+
31+
/// The link to the first page of results
32+
first: String,
33+
34+
/// The link to the last page of results
35+
last: String,
36+
37+
/// The link to the next page of results
38+
///
39+
/// Only present if there is a next page
40+
#[serde(skip_serializing_if = "Option::is_none")]
41+
next: Option<String>,
42+
43+
/// The link to the previous page of results
44+
///
45+
/// Only present if there is a previous page
46+
#[serde(skip_serializing_if = "Option::is_none")]
47+
prev: Option<String>,
48+
}
49+
50+
#[derive(Serialize, JsonSchema)]
51+
struct PaginationMeta {
52+
/// The total number of results
53+
count: usize,
54+
}
55+
56+
/// A top-level response with a page of resources
57+
#[derive(Serialize, JsonSchema)]
58+
pub struct PaginatedResponse<T> {
59+
/// Response metadata
60+
meta: PaginationMeta,
61+
62+
/// The list of resources
63+
data: Vec<SingleResource<T>>,
64+
65+
/// Related links
66+
links: PaginationLinks,
67+
}
68+
69+
fn url_with_pagination(base: &str, pagination: Pagination) -> String {
70+
let (path, query) = base.split_once('?').unwrap_or((base, ""));
71+
let mut query = query.to_owned();
72+
73+
if let Some(before) = pagination.before {
74+
query += &format!("&page[before]={before}");
75+
}
76+
77+
if let Some(after) = pagination.after {
78+
query += &format!("&page[after]={after}");
79+
}
80+
81+
let count = pagination.count;
82+
match pagination.direction {
83+
mas_storage::pagination::PaginationDirection::Forward => {
84+
query += &format!("&page[first]={count}");
85+
}
86+
mas_storage::pagination::PaginationDirection::Backward => {
87+
query += &format!("&page[last]={count}");
88+
}
89+
}
90+
91+
// Remove the first '&'
92+
let query = query.trim_start_matches('&');
93+
94+
format!("{path}?{query}")
95+
}
96+
97+
impl<T: Resource> PaginatedResponse<T> {
98+
pub fn new(
99+
page: mas_storage::Page<T>,
100+
current_pagination: Pagination,
101+
count: usize,
102+
base: &str,
103+
) -> Self {
104+
let links = PaginationLinks {
105+
self_: url_with_pagination(base, current_pagination),
106+
first: url_with_pagination(base, Pagination::first(current_pagination.count)),
107+
last: url_with_pagination(base, Pagination::last(current_pagination.count)),
108+
next: page.has_next_page.then(|| {
109+
url_with_pagination(
110+
base,
111+
current_pagination
112+
.clear_before()
113+
.after(page.edges.last().unwrap().id()),
114+
)
115+
}),
116+
prev: if page.has_previous_page {
117+
Some(url_with_pagination(
118+
base,
119+
current_pagination
120+
.clear_after()
121+
.before(page.edges.first().unwrap().id()),
122+
))
123+
} else {
124+
None
125+
},
126+
};
127+
128+
let data = page.edges.into_iter().map(SingleResource::new).collect();
129+
130+
Self {
131+
meta: PaginationMeta { count },
132+
data,
133+
links,
134+
}
135+
}
136+
}
137+
138+
/// A single resource, with its type, ID, attributes and related links
139+
#[derive(Serialize, JsonSchema)]
140+
struct SingleResource<T> {
141+
/// The type of the resource
142+
#[serde(rename = "type")]
143+
type_: &'static str,
144+
145+
/// The ID of the resource
146+
#[schemars(with = "String")]
147+
id: Ulid,
148+
149+
/// The attributes of the resource
150+
attributes: T,
151+
152+
/// Related links
153+
links: SelfLinks,
154+
}
155+
156+
impl<T: Resource> SingleResource<T> {
157+
fn new(resource: T) -> Self {
158+
let self_ = resource.path();
159+
Self {
160+
type_: T::KIND,
161+
id: resource.id(),
162+
attributes: resource,
163+
links: SelfLinks { self_ },
164+
}
165+
}
166+
}
167+
168+
/// Related links
169+
#[derive(Serialize, JsonSchema)]
170+
struct SelfLinks {
171+
/// The canonical link to the current resource
172+
#[serde(rename = "self")]
173+
self_: String,
174+
}
175+
176+
/// A top-level response with a single resource
177+
#[derive(Serialize, JsonSchema)]
178+
pub struct SingleResponse<T> {
179+
data: SingleResource<T>,
180+
links: SelfLinks,
181+
}
182+
183+
impl<T: Resource> SingleResponse<T> {
184+
/// Create a new single response with the given resource and link to itself
185+
pub fn new(resource: T, self_: String) -> Self {
186+
Self {
187+
data: SingleResource::new(resource),
188+
links: SelfLinks { self_ },
189+
}
190+
}
191+
192+
/// Create a new single response using the canonical path for the resource
193+
pub fn new_canonical(resource: T) -> Self {
194+
let self_ = resource.path();
195+
Self::new(resource, self_)
196+
}
197+
}
19198

20199
/// A single error
21200
#[derive(Serialize, JsonSchema)]

0 commit comments

Comments
 (0)