You've already forked authentication-service
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:
@ -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;
|
||||
|
35
crates/handlers/src/admin/model.rs
Normal file
35
crates/handlers/src/admin/model.rs
Normal 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())
|
||||
}
|
||||
}
|
@ -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)]
|
||||
|
Reference in New Issue
Block a user