pub mod config;
pub mod dashboard;
pub mod error;
pub mod field;
pub mod model;
pub mod registry;
pub mod ui;
pub mod views;
pub use config::*;
pub use dashboard::*;
pub use error::*;
pub use field::*;
pub use model::*;
pub use registry::*;
pub use ui::*;
pub use views::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
pub struct Admin {
config: AdminConfig,
registry: ModelRegistry,
}
impl Admin {
pub fn new() -> Self {
Self {
config: AdminConfig::default(),
registry: ModelRegistry::new(),
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.config.title = title.into();
self
}
pub fn base_path(mut self, path: impl Into<String>) -> Self {
self.config.base_path = path.into();
self
}
pub fn theme(mut self, theme: Theme) -> Self {
self.config.theme = theme;
self
}
pub fn items_per_page(mut self, count: usize) -> Self {
self.config.items_per_page = count;
self
}
pub fn require_auth(mut self, required: bool) -> Self {
self.config.require_auth = required;
self
}
pub fn register_model(mut self, model: ModelDefinition) -> Self {
self.registry.register(model);
self
}
pub fn build(self) -> AdminInstance {
AdminInstance {
config: Arc::new(self.config),
registry: Arc::new(self.registry),
}
}
}
impl Default for Admin {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct AdminInstance {
pub config: Arc<AdminConfig>,
pub registry: Arc<ModelRegistry>,
}
impl AdminInstance {
pub fn config(&self) -> &AdminConfig {
&self.config
}
pub fn registry(&self) -> &ModelRegistry {
&self.registry
}
pub fn routes(&self) -> AdminRoutes {
AdminRoutes::new(self.clone())
}
pub fn get_model(&self, name: &str) -> Option<&ModelDefinition> {
self.registry.get(name)
}
pub fn models(&self) -> Vec<&ModelDefinition> {
self.registry.all()
}
}
pub struct AdminRoutes {
admin: AdminInstance,
}
impl AdminRoutes {
pub fn new(admin: AdminInstance) -> Self {
Self { admin }
}
pub fn base_path(&self) -> &str {
&self.admin.config.base_path
}
pub async fn dashboard(&self) -> DashboardView {
DashboardView::new(&self.admin)
}
pub async fn list(&self, model_name: &str, params: ListParams) -> Option<ListView> {
self.admin
.get_model(model_name)
.map(|model| ListView::new(model, params))
}
pub async fn detail(&self, model_name: &str, id: &str) -> Option<DetailView> {
self.admin
.get_model(model_name)
.map(|model| DetailView::new(model, id.to_string()))
}
pub async fn create(&self, model_name: &str) -> Option<CreateView> {
self.admin
.get_model(model_name)
.map(|model| CreateView::new(model))
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListParams {
pub page: Option<usize>,
pub per_page: Option<usize>,
pub sort: Option<String>,
pub order: Option<SortOrder>,
pub search: Option<String>,
pub filters: HashMap<String, String>,
}
impl ListParams {
pub fn page(&self) -> usize {
self.page.unwrap_or(1).max(1)
}
pub fn per_page(&self, default: usize) -> usize {
self.per_page.unwrap_or(default).min(100)
}
pub fn offset(&self, default_per_page: usize) -> usize {
(self.page() - 1) * self.per_page(default_per_page)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SortOrder {
#[default]
Asc,
Desc,
}
impl SortOrder {
pub fn as_sql(&self) -> &'static str {
match self {
Self::Asc => "ASC",
Self::Desc => "DESC",
}
}
pub fn toggle(&self) -> Self {
match self {
Self::Asc => Self::Desc,
Self::Desc => Self::Asc,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_admin_builder() {
let admin = Admin::new()
.title("Test Admin")
.base_path("/admin")
.items_per_page(25)
.build();
assert_eq!(admin.config.title, "Test Admin");
assert_eq!(admin.config.base_path, "/admin");
assert_eq!(admin.config.items_per_page, 25);
}
#[test]
fn test_list_params() {
let params = ListParams {
page: Some(2),
per_page: Some(20),
..Default::default()
};
assert_eq!(params.page(), 2);
assert_eq!(params.per_page(10), 20);
assert_eq!(params.offset(10), 20);
}
#[test]
fn test_sort_order() {
assert_eq!(SortOrder::Asc.toggle(), SortOrder::Desc);
assert_eq!(SortOrder::Desc.toggle(), SortOrder::Asc);
}
}