armature_admin/
lib.rs

1//! Admin Dashboard Generator for Armature Framework
2//!
3//! Auto-generates a complete CRUD admin interface from your models,
4//! similar to Django Admin or Rails Admin.
5//!
6//! ## Overview
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────────────┐
10//! │                    Admin Dashboard                               │
11//! │                                                                  │
12//! │  ┌──────────────────────────────────────────────────────────┐  │
13//! │  │  Navigation    │  Content Area                           │  │
14//! │  │  ───────────   │  ─────────────                          │  │
15//! │  │  Dashboard     │  ┌────────────────────────────────────┐ │  │
16//! │  │  Users         │  │  Users List                        │ │  │
17//! │  │  Products      │  │  ─────────────────────────────────  │ │  │
18//! │  │  Orders        │  │  [Search] [Filter] [+Add]          │ │  │
19//! │  │  Settings      │  │  ┌────┬────────┬────────┬───────┐  │ │  │
20//! │  │                │  │  │ ID │ Name   │ Email  │ Actions│  │ │  │
21//! │  │                │  │  ├────┼────────┼────────┼───────┤  │ │  │
22//! │  │                │  │  │ 1  │ Alice  │ a@...  │ ✏️ 🗑️ │  │ │  │
23//! │  │                │  │  │ 2  │ Bob    │ b@...  │ ✏️ 🗑️ │  │ │  │
24//! │  │                │  │  └────┴────────┴────────┴───────┘  │ │  │
25//! │  │                │  │  [◀ Prev] Page 1 of 10 [Next ▶]   │ │  │
26//! │  │                │  └────────────────────────────────────┘ │  │
27//! │  └──────────────────────────────────────────────────────────┘  │
28//! └─────────────────────────────────────────────────────────────────┘
29//! ```
30//!
31//! ## Quick Start
32//!
33//! ```rust,ignore
34//! use armature_admin::{Admin, AdminModel, Field};
35//!
36//! #[derive(AdminModel)]
37//! #[admin(list_display = ["id", "name", "email"])]
38//! #[admin(search_fields = ["name", "email"])]
39//! struct User {
40//!     #[admin(primary_key)]
41//!     id: i64,
42//!     #[admin(required)]
43//!     name: String,
44//!     #[admin(widget = "email")]
45//!     email: String,
46//!     #[admin(readonly)]
47//!     created_at: DateTime<Utc>,
48//! }
49//!
50//! let admin = Admin::new()
51//!     .title("My Admin")
52//!     .register::<User>()
53//!     .build();
54//!
55//! // Mount at /admin
56//! app.mount("/admin", admin.routes());
57//! ```
58
59pub mod config;
60pub mod dashboard;
61pub mod error;
62pub mod field;
63pub mod model;
64pub mod registry;
65pub mod ui;
66pub mod views;
67
68pub use config::*;
69pub use dashboard::*;
70pub use error::*;
71pub use field::*;
72pub use model::*;
73pub use registry::*;
74pub use ui::*;
75pub use views::*;
76
77use serde::{Deserialize, Serialize};
78use std::collections::HashMap;
79use std::sync::Arc;
80
81/// Admin instance builder
82pub struct Admin {
83    /// Admin configuration
84    config: AdminConfig,
85    /// Model registry
86    registry: ModelRegistry,
87}
88
89impl Admin {
90    /// Create a new admin builder
91    pub fn new() -> Self {
92        Self {
93            config: AdminConfig::default(),
94            registry: ModelRegistry::new(),
95        }
96    }
97
98    /// Set the admin title
99    pub fn title(mut self, title: impl Into<String>) -> Self {
100        self.config.title = title.into();
101        self
102    }
103
104    /// Set the base URL path
105    pub fn base_path(mut self, path: impl Into<String>) -> Self {
106        self.config.base_path = path.into();
107        self
108    }
109
110    /// Set the theme
111    pub fn theme(mut self, theme: Theme) -> Self {
112        self.config.theme = theme;
113        self
114    }
115
116    /// Set items per page
117    pub fn items_per_page(mut self, count: usize) -> Self {
118        self.config.items_per_page = count;
119        self
120    }
121
122    /// Enable/disable authentication
123    pub fn require_auth(mut self, required: bool) -> Self {
124        self.config.require_auth = required;
125        self
126    }
127
128    /// Register a model with the admin
129    pub fn register_model(mut self, model: ModelDefinition) -> Self {
130        self.registry.register(model);
131        self
132    }
133
134    /// Build the admin instance
135    pub fn build(self) -> AdminInstance {
136        AdminInstance {
137            config: Arc::new(self.config),
138            registry: Arc::new(self.registry),
139        }
140    }
141}
142
143impl Default for Admin {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149/// Built admin instance
150#[derive(Clone)]
151pub struct AdminInstance {
152    /// Configuration
153    pub config: Arc<AdminConfig>,
154    /// Model registry
155    pub registry: Arc<ModelRegistry>,
156}
157
158impl AdminInstance {
159    /// Get the configuration
160    pub fn config(&self) -> &AdminConfig {
161        &self.config
162    }
163
164    /// Get the model registry
165    pub fn registry(&self) -> &ModelRegistry {
166        &self.registry
167    }
168
169    /// Generate routes for the admin interface
170    pub fn routes(&self) -> AdminRoutes {
171        AdminRoutes::new(self.clone())
172    }
173
174    /// Get a model definition by name
175    pub fn get_model(&self, name: &str) -> Option<&ModelDefinition> {
176        self.registry.get(name)
177    }
178
179    /// List all registered models
180    pub fn models(&self) -> Vec<&ModelDefinition> {
181        self.registry.all()
182    }
183}
184
185/// Admin route handler
186pub struct AdminRoutes {
187    admin: AdminInstance,
188}
189
190impl AdminRoutes {
191    /// Create new admin routes
192    pub fn new(admin: AdminInstance) -> Self {
193        Self { admin }
194    }
195
196    /// Get the base path
197    pub fn base_path(&self) -> &str {
198        &self.admin.config.base_path
199    }
200
201    /// Handle dashboard request
202    pub async fn dashboard(&self) -> DashboardView {
203        DashboardView::new(&self.admin)
204    }
205
206    /// Handle model list request
207    pub async fn list(&self, model_name: &str, params: ListParams) -> Option<ListView> {
208        self.admin
209            .get_model(model_name)
210            .map(|model| ListView::new(model, params))
211    }
212
213    /// Handle model detail/edit request
214    pub async fn detail(&self, model_name: &str, id: &str) -> Option<DetailView> {
215        self.admin
216            .get_model(model_name)
217            .map(|model| DetailView::new(model, id.to_string()))
218    }
219
220    /// Handle create request
221    pub async fn create(&self, model_name: &str) -> Option<CreateView> {
222        self.admin
223            .get_model(model_name)
224            .map(|model| CreateView::new(model))
225    }
226}
227
228/// Parameters for list view
229#[derive(Debug, Clone, Default, Serialize, Deserialize)]
230pub struct ListParams {
231    /// Current page (1-indexed)
232    pub page: Option<usize>,
233    /// Items per page
234    pub per_page: Option<usize>,
235    /// Sort field
236    pub sort: Option<String>,
237    /// Sort direction
238    pub order: Option<SortOrder>,
239    /// Search query
240    pub search: Option<String>,
241    /// Filters
242    pub filters: HashMap<String, String>,
243}
244
245impl ListParams {
246    /// Get effective page number
247    pub fn page(&self) -> usize {
248        self.page.unwrap_or(1).max(1)
249    }
250
251    /// Get effective items per page
252    pub fn per_page(&self, default: usize) -> usize {
253        self.per_page.unwrap_or(default).min(100)
254    }
255
256    /// Get offset for pagination
257    pub fn offset(&self, default_per_page: usize) -> usize {
258        (self.page() - 1) * self.per_page(default_per_page)
259    }
260}
261
262/// Sort order
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
264pub enum SortOrder {
265    #[default]
266    Asc,
267    Desc,
268}
269
270impl SortOrder {
271    /// Get SQL representation
272    pub fn as_sql(&self) -> &'static str {
273        match self {
274            Self::Asc => "ASC",
275            Self::Desc => "DESC",
276        }
277    }
278
279    /// Toggle order
280    pub fn toggle(&self) -> Self {
281        match self {
282            Self::Asc => Self::Desc,
283            Self::Desc => Self::Asc,
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_admin_builder() {
294        let admin = Admin::new()
295            .title("Test Admin")
296            .base_path("/admin")
297            .items_per_page(25)
298            .build();
299
300        assert_eq!(admin.config.title, "Test Admin");
301        assert_eq!(admin.config.base_path, "/admin");
302        assert_eq!(admin.config.items_per_page, 25);
303    }
304
305    #[test]
306    fn test_list_params() {
307        let params = ListParams {
308            page: Some(2),
309            per_page: Some(20),
310            ..Default::default()
311        };
312
313        assert_eq!(params.page(), 2);
314        assert_eq!(params.per_page(10), 20);
315        assert_eq!(params.offset(10), 20);
316    }
317
318    #[test]
319    fn test_sort_order() {
320        assert_eq!(SortOrder::Asc.toggle(), SortOrder::Desc);
321        assert_eq!(SortOrder::Desc.toggle(), SortOrder::Asc);
322    }
323}
324