1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +03:00

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.
This commit is contained in:
Quentin Gliech
2024-07-24 15:56:47 +02:00
parent 27ca7ec108
commit c177233b33
3 changed files with 215 additions and 0 deletions

View File

@ -24,6 +24,7 @@ use mas_router::{OAuth2AuthorizationEndpoint, OAuth2TokenEndpoint, SimpleRoute};
use tower_http::cors::{Any, CorsLayer};
mod call_context;
mod model;
mod response;
use self::call_context::CallContext;

View File

@ -0,0 +1,35 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use ulid::Ulid;
/// A resource, with a type and an ID
#[allow(dead_code)]
pub trait Resource {
/// The type of the resource
const KIND: &'static str;
/// The canonical path prefix for this kind of resource
const PATH: &'static str;
/// The ID of the resource
fn id(&self) -> Ulid;
/// The canonical path for this resource
///
/// This is the concatenation of the canonical path prefix and the ID
fn path(&self) -> String {
format!("{}/{}", Self::PATH, self.id())
}
}

View File

@ -14,8 +14,187 @@
#![allow(clippy::module_name_repetitions)]
use mas_storage::Pagination;
use schemars::JsonSchema;
use serde::Serialize;
use ulid::Ulid;
use super::model::Resource;
/// Related links
#[derive(Serialize, JsonSchema)]
struct PaginationLinks {
/// The canonical link to the current page
#[serde(rename = "self")]
self_: String,
/// The link to the first page of results
first: String,
/// The link to the last page of results
last: String,
/// The link to the next page of results
///
/// Only present if there is a next page
#[serde(skip_serializing_if = "Option::is_none")]
next: Option<String>,
/// The link to the previous page of results
///
/// Only present if there is a previous page
#[serde(skip_serializing_if = "Option::is_none")]
prev: Option<String>,
}
#[derive(Serialize, JsonSchema)]
struct PaginationMeta {
/// The total number of results
count: usize,
}
/// A top-level response with a page of resources
#[derive(Serialize, JsonSchema)]
pub struct PaginatedResponse<T> {
/// Response metadata
meta: PaginationMeta,
/// The list of resources
data: Vec<SingleResource<T>>,
/// Related links
links: PaginationLinks,
}
fn url_with_pagination(base: &str, pagination: Pagination) -> String {
let (path, query) = base.split_once('?').unwrap_or((base, ""));
let mut query = query.to_owned();
if let Some(before) = pagination.before {
query += &format!("&page[before]={before}");
}
if let Some(after) = pagination.after {
query += &format!("&page[after]={after}");
}
let count = pagination.count;
match pagination.direction {
mas_storage::pagination::PaginationDirection::Forward => {
query += &format!("&page[first]={count}");
}
mas_storage::pagination::PaginationDirection::Backward => {
query += &format!("&page[last]={count}");
}
}
// Remove the first '&'
let query = query.trim_start_matches('&');
format!("{path}?{query}")
}
impl<T: Resource> PaginatedResponse<T> {
pub fn new(
page: mas_storage::Page<T>,
current_pagination: Pagination,
count: usize,
base: &str,
) -> Self {
let links = PaginationLinks {
self_: url_with_pagination(base, current_pagination),
first: url_with_pagination(base, Pagination::first(current_pagination.count)),
last: url_with_pagination(base, Pagination::last(current_pagination.count)),
next: page.has_next_page.then(|| {
url_with_pagination(
base,
current_pagination
.clear_before()
.after(page.edges.last().unwrap().id()),
)
}),
prev: if page.has_previous_page {
Some(url_with_pagination(
base,
current_pagination
.clear_after()
.before(page.edges.first().unwrap().id()),
))
} else {
None
},
};
let data = page.edges.into_iter().map(SingleResource::new).collect();
Self {
meta: PaginationMeta { count },
data,
links,
}
}
}
/// A single resource, with its type, ID, attributes and related links
#[derive(Serialize, JsonSchema)]
struct SingleResource<T> {
/// The type of the resource
#[serde(rename = "type")]
type_: &'static str,
/// The ID of the resource
#[schemars(with = "String")]
id: Ulid,
/// The attributes of the resource
attributes: T,
/// Related links
links: SelfLinks,
}
impl<T: Resource> SingleResource<T> {
fn new(resource: T) -> Self {
let self_ = resource.path();
Self {
type_: T::KIND,
id: resource.id(),
attributes: resource,
links: SelfLinks { self_ },
}
}
}
/// Related links
#[derive(Serialize, JsonSchema)]
struct SelfLinks {
/// The canonical link to the current resource
#[serde(rename = "self")]
self_: String,
}
/// A top-level response with a single resource
#[derive(Serialize, JsonSchema)]
pub struct SingleResponse<T> {
data: SingleResource<T>,
links: SelfLinks,
}
impl<T: Resource> SingleResponse<T> {
/// Create a new single response with the given resource and link to itself
pub fn new(resource: T, self_: String) -> Self {
Self {
data: SingleResource::new(resource),
links: SelfLinks { self_ },
}
}
/// Create a new single response using the canonical path for the resource
pub fn new_canonical(resource: T) -> Self {
let self_ = resource.path();
Self::new(resource, self_)
}
}
/// A single error
#[derive(Serialize, JsonSchema)]