use std::collections::HashMap;
use std::sync::Arc;
use axum::Router;
use laterite_core::{Db, Events, Registry, Text};
use crate::form::{FormHandle, Forms};
use crate::list::ImportLink;
#[derive(Clone)]
pub struct RouteCtx {
db: Db,
base_path: Arc<str>,
admin_path: Arc<str>,
base_url: Arc<str>,
plugin_defined: Arc<Registry>,
events: Events,
forms: Forms,
}
impl RouteCtx {
pub(crate) fn new(
db: Db,
base_path: &str,
admin_path: &str,
base_url: &str,
plugin_defined: Arc<Registry>,
events: Events,
forms: Forms,
) -> Self {
Self {
db,
base_path: Arc::from(base_path),
admin_path: Arc::from(admin_path),
base_url: Arc::from(base_url),
plugin_defined,
events,
forms,
}
}
pub fn db(&self) -> &Db {
&self.db
}
pub fn form(&self, path: &str) -> Option<FormHandle> {
self.forms
.get(path)
.map(|form| FormHandle::new(self.db.clone(), form.clone()))
}
pub fn events(&self) -> &Events {
&self.events
}
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,
events: None,
}
}
}
pub struct RouteCtxBuilder {
db: Db,
base_path: String,
admin_path: String,
base_url: String,
plugin_defined: Option<Arc<Registry>>,
events: Option<Events>,
}
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 events(mut self, events: Events) -> Self {
self.events = Some(events);
self
}
pub fn build(self) -> RouteCtx {
let events = self.events.unwrap_or_else(|| Events::new(self.db.clone()));
RouteCtx::new(
self.db,
&self.base_path,
&self.admin_path,
&self.base_url,
self.plugin_defined
.unwrap_or_else(|| Arc::new(Registry::new())),
events,
Arc::new(HashMap::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 menu: crate::nav::Placement,
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,
menu: crate::nav::Placement::default(),
permission: permission.into(),
screen,
}
}
pub fn in_menu(mut self, label: impl Into<Text>) -> Self {
self.nav_label = Some(label.into());
self
}
pub fn menu(mut self, placement: crate::nav::Placement) -> Self {
self.menu = placement;
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,
}
}
}
pub trait ResourceFeature: Send + Sync + 'static {
fn mount(&self, ctx: &RouteCtx, resource: &ResourceInfo) -> Router;
}
#[non_exhaustive]
pub struct ResourceFeatureReg {
pub key: String,
pub feature: Arc<dyn ResourceFeature>,
}
impl ResourceFeatureReg {
pub fn new(key: impl Into<String>, feature: Arc<dyn ResourceFeature>) -> Self {
Self {
key: key.into(),
feature,
}
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct ResourceInfo {
pub path: String,
pub base_path: String,
pub label: Text,
pub permission: String,
pub form: FormHandle,
}
impl ResourceInfo {
pub(crate) fn new(
path: String,
base_path: String,
label: Text,
permission: String,
form: FormHandle,
) -> Self {
Self {
path,
base_path,
label,
permission,
form,
}
}
}
pub(crate) fn feature_link(base_path: &str, key: &str, permission: &str) -> ImportLink {
ImportLink {
href: format!("{base_path}/{key}"),
permission: permission.to_string(),
}
}
#[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());
}
}