use std::sync::Arc;
use axum::Router;
use laterite_core::{Db, Registry, Text};
#[derive(Clone)]
pub struct RouteCtx {
db: Db,
base_path: Arc<str>,
admin_path: Arc<str>,
base_url: Arc<str>,
plugin_defined: Arc<Registry>,
}
impl RouteCtx {
pub(crate) fn new(
db: Db,
base_path: &str,
admin_path: &str,
base_url: &str,
plugin_defined: Arc<Registry>,
) -> Self {
Self {
db,
base_path: Arc::from(base_path),
admin_path: Arc::from(admin_path),
base_url: Arc::from(base_url),
plugin_defined,
}
}
pub fn db(&self) -> &Db {
&self.db
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn contributions<T: Send + Sync + 'static>(&self) -> Vec<&T> {
self.plugin_defined.items::<T>()
}
pub fn base_path(&self) -> &str {
&self.base_path
}
pub fn admin_path(&self) -> &str {
&self.admin_path
}
pub fn url(&self, relative: &str) -> String {
join(&self.base_path, relative)
}
pub fn builder(db: Db) -> RouteCtxBuilder {
RouteCtxBuilder {
db,
base_path: "/".to_string(),
admin_path: "/admin".to_string(),
base_url: "http://localhost".to_string(),
plugin_defined: None,
}
}
}
pub struct RouteCtxBuilder {
db: Db,
base_path: String,
admin_path: String,
base_url: String,
plugin_defined: Option<Arc<Registry>>,
}
impl RouteCtxBuilder {
pub fn base_path(mut self, path: impl Into<String>) -> Self {
self.base_path = path.into();
self
}
pub fn admin_path(mut self, path: impl Into<String>) -> Self {
self.admin_path = path.into();
self
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
pub fn contributions(mut self, registry: Arc<Registry>) -> Self {
self.plugin_defined = Some(registry);
self
}
pub fn build(self) -> RouteCtx {
RouteCtx::new(
self.db,
&self.base_path,
&self.admin_path,
&self.base_url,
self.plugin_defined
.unwrap_or_else(|| Arc::new(Registry::new())),
)
}
}
fn join(base: &str, relative: &str) -> String {
let base = base.trim_end_matches('/');
let relative = relative.trim_start_matches('/');
if relative.is_empty() {
base.to_string()
} else {
format!("{base}/{relative}")
}
}
pub trait Screen: Send + Sync + 'static {
fn mount(&self, ctx: &RouteCtx) -> Router;
}
#[non_exhaustive]
pub struct ScreenReg {
pub base_path: String,
pub nav_label: Option<Text>,
pub permission: String,
pub screen: Arc<dyn Screen>,
}
impl ScreenReg {
pub fn new(
base_path: impl Into<String>,
permission: impl Into<String>,
screen: Arc<dyn Screen>,
) -> Self {
Self {
base_path: base_path.into(),
nav_label: None,
permission: permission.into(),
screen,
}
}
pub fn in_menu(mut self, label: impl Into<Text>) -> Self {
self.nav_label = Some(label.into());
self
}
}
pub trait PublicRoute: Send + Sync + 'static {
fn mount(&self, ctx: &RouteCtx) -> Router;
}
#[non_exhaustive]
pub struct PublicRouteReg {
pub path: String,
pub route: Arc<dyn PublicRoute>,
}
impl PublicRouteReg {
pub fn new(path: impl Into<String>, route: Arc<dyn PublicRoute>) -> Self {
Self {
path: path.into(),
route,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_self_link_builds_from_where_the_screen_actually_mounted() {
let base = "/admin/rainmill/location/import";
assert_eq!(
join(base, "/step2"),
"/admin/rainmill/location/import/step2"
);
assert_eq!(join(base, "step2"), "/admin/rainmill/location/import/step2");
assert_eq!(join(base, ""), "/admin/rainmill/location/import");
}
#[test]
fn a_moved_screen_moves_its_links_with_it() {
assert_eq!(join("/admin/places", "/step2"), "/admin/places/step2");
}
#[test]
fn a_registration_defaults_to_no_menu_entry() {
struct Noop;
impl Screen for Noop {
fn mount(&self, _ctx: &RouteCtx) -> Router {
Router::new()
}
}
let reg = ScreenReg::new("/import", "acme.import", Arc::new(Noop));
assert!(reg.nav_label.is_none());
assert!(reg.in_menu("Import").nav_label.is_some());
}
}