use axum::{ extract::FromRequestParts, http::request::Parts, response::{IntoResponse as _, Redirect, Response}, }; use derive_builder::Builder; use sqlx::postgres::types::Oid; use uuid::Uuid; use crate::{app::App, errors::AppError}; pub(crate) trait NavigatorPage { /// Returns the path component of the URL to the corresonding web page. /// Starts with a "/" character and the `root_path` if one is specified for /// [`crate::settings::Settings`]. fn get_path(&self) -> String; /// Wrap [`Self::get_path`] in an Axum HTTP 303 redirection response for /// convenience. fn redirect_to(&self) -> Response { Redirect::to(&self.get_path()).into_response() } } /// Helper type for semantically generating URI paths, e.g. for redirects. #[derive(Clone, Debug)] pub(crate) struct Navigator { root_path: String, sub_path: String, } impl Navigator { pub(crate) fn workspace_page(&self, workspace_id: Uuid) -> Self { Self { sub_path: format!("/w/{0}/", workspace_id.simple()), ..self.clone() } } pub(crate) fn portal_page(&self) -> PortalPageBuilder { PortalPageBuilder { root_path: Some(self.get_root_path()), ..Default::default() } } pub(crate) fn form_page(&self, portal_id: Uuid) -> FormPageBuilder { FormPageBuilder { root_path: Some(self.get_root_path()), portal_id: Some(portal_id), } } /// Returns a [`NavigatorPage`] builder for navigating to a relation's /// "settings" page. pub(crate) fn rel_settings_page(&self) -> RelSettingsPageBuilder { RelSettingsPageBuilder { root_path: Some(self.get_root_path()), ..Default::default() } } pub(crate) fn get_root_path(&self) -> String { self.root_path.to_owned() } pub(crate) fn abs_path(&self) -> String { format!("{0}{1}", self.root_path, self.sub_path) } pub(crate) fn redirect_to(&self) -> Response { Redirect::to(&self.abs_path()).into_response() } } impl FromRequestParts for Navigator { type Rejection = AppError; async fn from_request_parts(_: &mut Parts, state: &App) -> Result { Ok(Navigator { root_path: state.settings.root_path.clone(), sub_path: "/".to_owned(), }) } } #[derive(Builder, Clone, Debug)] pub(crate) struct PortalPage { portal_id: Uuid, rel_oid: Oid, #[builder(setter(custom))] root_path: String, /// Any value provided for `suffix` will be appended (without %-encoding) to /// the final path value. This may be used for sub-paths and/or search /// parameters. #[builder(default, setter(strip_option))] suffix: Option, workspace_id: Uuid, } impl NavigatorPage for PortalPage { fn get_path(&self) -> String { format!( "{root_path}/w/{workspace_id}/r/{rel_oid}/p/{portal_id}/{suffix}", root_path = self.root_path, workspace_id = self.workspace_id.simple(), rel_oid = self.rel_oid.0, portal_id = self.portal_id.simple(), suffix = self.suffix.clone().unwrap_or_default() ) } } #[derive(Builder, Clone, Debug)] pub(crate) struct FormPage { portal_id: Uuid, #[builder(setter(custom))] root_path: String, } impl NavigatorPage for FormPage { fn get_path(&self) -> String { format!( "{root_path}/f/{portal_id}", root_path = self.root_path, portal_id = self.portal_id ) } } #[derive(Builder, Clone, Debug)] pub(crate) struct RelSettingsPage { rel_oid: Oid, #[builder(setter(custom))] root_path: String, /// Any value provided for `suffix` will be appended (without %-encoding) to /// the final path value. This may be used for sub-paths and/or search /// parameters. #[builder(default, setter(strip_option))] suffix: Option, workspace_id: Uuid, } impl NavigatorPage for RelSettingsPage { fn get_path(&self) -> String { format!( "{root_path}/w/{workspace_id}/r/{rel_oid}/settings/{suffix}", root_path = self.root_path, workspace_id = self.workspace_id.simple(), rel_oid = self.rel_oid.0, suffix = self.suffix.clone().unwrap_or_default(), ) } }