diff --git a/crates/handlers/src/admin/mod.rs b/crates/handlers/src/admin/mod.rs index 69eed691..a918d940 100644 --- a/crates/handlers/src/admin/mod.rs +++ b/crates/handlers/src/admin/mod.rs @@ -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; diff --git a/crates/handlers/src/admin/model.rs b/crates/handlers/src/admin/model.rs new file mode 100644 index 00000000..91a5ec06 --- /dev/null +++ b/crates/handlers/src/admin/model.rs @@ -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()) + } +} diff --git a/crates/handlers/src/admin/response.rs b/crates/handlers/src/admin/response.rs index a2ebd2dc..dde7b739 100644 --- a/crates/handlers/src/admin/response.rs +++ b/crates/handlers/src/admin/response.rs @@ -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, + + /// 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, +} + +#[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 { + /// Response metadata + meta: PaginationMeta, + + /// The list of resources + data: Vec>, + + /// 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 PaginatedResponse { + pub fn new( + page: mas_storage::Page, + 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 { + /// 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 SingleResource { + 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 { + data: SingleResource, + links: SelfLinks, +} + +impl SingleResponse { + /// 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)]