use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use axum::routing::{get, post};
use axum::Router;
use crate::core::SqlValue;
use crate::sql::sqlx::PgPool;
use super::errors::AdminError;
use super::views;
pub type AdminActionFuture<'a> =
Pin<Box<dyn Future<Output = Result<(), AdminError>> + Send + 'a>>;
pub type AdminActionFn = Arc<
dyn for<'a> Fn(&'a PgPool, &'a [SqlValue]) -> AdminActionFuture<'a>
+ Send
+ Sync,
>;
pub(crate) type AdminActionRegistry =
HashMap<&'static str, HashMap<&'static str, AdminActionFn>>;
pub fn router(pool: PgPool) -> Router {
Builder::new(pool).build()
}
#[must_use]
pub struct Builder {
pool: PgPool,
config: Config,
}
#[derive(Clone, Default)]
pub(crate) struct Config {
pub(crate) title: Option<String>,
pub(crate) subtitle: Option<String>,
pub(crate) allowed_tables: Option<HashSet<String>>,
pub(crate) read_only_tables: HashSet<String>,
pub(crate) read_only_all: bool,
pub(crate) actions: AdminActionRegistry,
}
impl Builder {
pub fn new(pool: PgPool) -> Self {
Self {
pool,
config: Config::default(),
}
}
pub fn show_only<I, S>(mut self, tables: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.config.allowed_tables = Some(tables.into_iter().map(Into::into).collect());
self
}
pub fn read_only<I, S>(mut self, tables: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.config
.read_only_tables
.extend(tables.into_iter().map(Into::into));
self
}
pub fn read_only_all(mut self) -> Self {
self.config.read_only_all = true;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.config.title = Some(title.into());
self
}
pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
self.config.subtitle = Some(subtitle.into());
self
}
pub fn register_action<F>(
mut self,
model_table: &'static str,
action_name: &'static str,
handler: F,
) -> Self
where
F: for<'a> Fn(&'a PgPool, &'a [SqlValue]) -> AdminActionFuture<'a>
+ Send
+ Sync
+ 'static,
{
self.config
.actions
.entry(model_table)
.or_default()
.insert(action_name, Arc::new(handler));
self
}
pub fn build(self) -> Router {
Router::new()
.route("/", get(views::index))
.route("/__audit", get(super::audit::audit_log_view))
.route("/__audit/cleanup", post(super::audit::audit_cleanup_submit))
.route("/{table}", get(views::table_view).post(views::create_submit))
.route("/{table}/new", get(views::create_form))
.route("/{table}/__action", post(views::action_submit))
.route(
"/{table}/{pk}",
get(views::detail_view).post(views::update_submit),
)
.route("/{table}/{pk}/edit", get(views::edit_form))
.route("/{table}/{pk}/delete", post(views::delete_submit))
.with_state(AppState {
pool: self.pool,
config: Arc::new(self.config),
})
}
}
#[derive(Clone)]
pub(crate) struct AppState {
pub(crate) pool: PgPool,
pub(crate) config: Arc<Config>,
}
impl AppState {
pub(crate) fn is_visible(&self, table: &str) -> bool {
self.config
.allowed_tables
.as_ref()
.is_none_or(|allowed| allowed.contains(table))
}
pub(crate) fn is_read_only(&self, table: &str) -> bool {
self.config.read_only_all || self.config.read_only_tables.contains(table)
}
pub(crate) fn action_handler(
&self,
table: &str,
action: &str,
) -> Option<AdminActionFn> {
self.config
.actions
.get(table)
.and_then(|m| m.get(action))
.cloned()
}
}