1use crate::field::{FieldDefinition, FieldType};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ModelDefinition {
9 pub name: String,
11 pub verbose_name: String,
13 pub verbose_name_singular: String,
15 pub table_name: String,
17 pub fields: Vec<FieldDefinition>,
19 pub primary_key: String,
21 pub list_display: Vec<String>,
23 pub search_fields: Vec<String>,
25 pub ordering: Vec<OrderingField>,
27 pub list_filter: Vec<String>,
29 pub readonly_fields: Vec<String>,
31 pub exclude: Vec<String>,
33 pub fieldsets: Vec<Fieldset>,
35 pub actions: Vec<AdminAction>,
37 pub icon: Option<String>,
39 pub list_template: Option<String>,
41 pub detail_template: Option<String>,
43 pub form_template: Option<String>,
45 pub inlines: Vec<InlineDefinition>,
47 pub can_add: bool,
49 pub can_edit: bool,
51 pub can_delete: bool,
53 pub can_export: bool,
55}
56
57impl ModelDefinition {
58 pub fn builder(name: impl Into<String>) -> ModelDefinitionBuilder {
60 ModelDefinitionBuilder::new(name)
61 }
62
63 pub fn get_field(&self, name: &str) -> Option<&FieldDefinition> {
65 self.fields.iter().find(|f| f.name == name)
66 }
67
68 pub fn display_fields(&self) -> Vec<&FieldDefinition> {
70 self.list_display
71 .iter()
72 .filter_map(|name| self.get_field(name))
73 .collect()
74 }
75
76 pub fn searchable_fields(&self) -> Vec<&FieldDefinition> {
78 self.search_fields
79 .iter()
80 .filter_map(|name| self.get_field(name))
81 .collect()
82 }
83
84 pub fn filterable_fields(&self) -> Vec<&FieldDefinition> {
86 self.list_filter
87 .iter()
88 .filter_map(|name| self.get_field(name))
89 .collect()
90 }
91
92 pub fn form_fields(&self) -> Vec<&FieldDefinition> {
94 self.fields
95 .iter()
96 .filter(|f| !f.primary_key && !self.exclude.contains(&f.name))
97 .collect()
98 }
99
100 pub fn pk_field(&self) -> Option<&FieldDefinition> {
102 self.get_field(&self.primary_key)
103 }
104}
105
106pub struct ModelDefinitionBuilder {
108 name: String,
109 verbose_name: Option<String>,
110 verbose_name_singular: Option<String>,
111 table_name: Option<String>,
112 fields: Vec<FieldDefinition>,
113 primary_key: String,
114 list_display: Vec<String>,
115 search_fields: Vec<String>,
116 ordering: Vec<OrderingField>,
117 list_filter: Vec<String>,
118 readonly_fields: Vec<String>,
119 exclude: Vec<String>,
120 fieldsets: Vec<Fieldset>,
121 actions: Vec<AdminAction>,
122 icon: Option<String>,
123 inlines: Vec<InlineDefinition>,
124 can_add: bool,
125 can_edit: bool,
126 can_delete: bool,
127 can_export: bool,
128}
129
130impl ModelDefinitionBuilder {
131 pub fn new(name: impl Into<String>) -> Self {
133 let name = name.into();
134 Self {
135 name: name.clone(),
136 verbose_name: None,
137 verbose_name_singular: None,
138 table_name: None,
139 fields: Vec::new(),
140 primary_key: "id".to_string(),
141 list_display: Vec::new(),
142 search_fields: Vec::new(),
143 ordering: vec![OrderingField::desc("id")],
144 list_filter: Vec::new(),
145 readonly_fields: Vec::new(),
146 exclude: Vec::new(),
147 fieldsets: Vec::new(),
148 actions: Vec::new(),
149 icon: None,
150 inlines: Vec::new(),
151 can_add: true,
152 can_edit: true,
153 can_delete: true,
154 can_export: true,
155 }
156 }
157
158 pub fn verbose_name(mut self, name: impl Into<String>) -> Self {
160 self.verbose_name = Some(name.into());
161 self
162 }
163
164 pub fn table_name(mut self, name: impl Into<String>) -> Self {
166 self.table_name = Some(name.into());
167 self
168 }
169
170 pub fn field(mut self, field: FieldDefinition) -> Self {
172 if field.primary_key {
173 self.primary_key = field.name.clone();
174 }
175 self.fields.push(field);
176 self
177 }
178
179 pub fn id_field(self) -> Self {
181 self.field(
182 FieldDefinition::new("id", FieldType::BigInteger)
183 .primary_key()
184 .label("ID"),
185 )
186 }
187
188 pub fn timestamps(self) -> Self {
190 self.field(
191 FieldDefinition::new("created_at", FieldType::DateTime)
192 .readonly()
193 .label("Created"),
194 )
195 .field(
196 FieldDefinition::new("updated_at", FieldType::DateTime)
197 .readonly()
198 .label("Updated"),
199 )
200 }
201
202 pub fn list_display(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
204 self.list_display = fields.into_iter().map(Into::into).collect();
205 self
206 }
207
208 pub fn search_fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
210 self.search_fields = fields.into_iter().map(Into::into).collect();
211 self
212 }
213
214 pub fn ordering(mut self, fields: impl IntoIterator<Item = OrderingField>) -> Self {
216 self.ordering = fields.into_iter().collect();
217 self
218 }
219
220 pub fn list_filter(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
222 self.list_filter = fields.into_iter().map(Into::into).collect();
223 self
224 }
225
226 pub fn readonly_fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
228 self.readonly_fields = fields.into_iter().map(Into::into).collect();
229 self
230 }
231
232 pub fn exclude(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
234 self.exclude = fields.into_iter().map(Into::into).collect();
235 self
236 }
237
238 pub fn fieldset(mut self, fieldset: Fieldset) -> Self {
240 self.fieldsets.push(fieldset);
241 self
242 }
243
244 pub fn action(mut self, action: AdminAction) -> Self {
246 self.actions.push(action);
247 self
248 }
249
250 pub fn icon(mut self, icon: impl Into<String>) -> Self {
252 self.icon = Some(icon.into());
253 self
254 }
255
256 pub fn inline(mut self, inline: InlineDefinition) -> Self {
258 self.inlines.push(inline);
259 self
260 }
261
262 pub fn no_add(mut self) -> Self {
264 self.can_add = false;
265 self
266 }
267
268 pub fn no_edit(mut self) -> Self {
270 self.can_edit = false;
271 self
272 }
273
274 pub fn no_delete(mut self) -> Self {
276 self.can_delete = false;
277 self
278 }
279
280 pub fn build(self) -> ModelDefinition {
282 let verbose_name = self.verbose_name.unwrap_or_else(|| {
283 let name = self.name.replace('_', " ");
285 if name.ends_with('s') {
286 name
287 } else {
288 format!("{}s", name)
289 }
290 });
291
292 let verbose_name_singular = self
293 .verbose_name_singular
294 .unwrap_or_else(|| self.name.replace('_', " "));
295
296 let table_name = self
297 .table_name
298 .unwrap_or_else(|| self.name.to_lowercase().replace(' ', "_"));
299
300 let list_display = if self.list_display.is_empty() {
302 self.fields.iter().take(5).map(|f| f.name.clone()).collect()
303 } else {
304 self.list_display
305 };
306
307 ModelDefinition {
308 name: self.name,
309 verbose_name,
310 verbose_name_singular,
311 table_name,
312 fields: self.fields,
313 primary_key: self.primary_key,
314 list_display,
315 search_fields: self.search_fields,
316 ordering: self.ordering,
317 list_filter: self.list_filter,
318 readonly_fields: self.readonly_fields,
319 exclude: self.exclude,
320 fieldsets: self.fieldsets,
321 actions: self.actions,
322 icon: self.icon,
323 list_template: None,
324 detail_template: None,
325 form_template: None,
326 inlines: self.inlines,
327 can_add: self.can_add,
328 can_edit: self.can_edit,
329 can_delete: self.can_delete,
330 can_export: self.can_export,
331 }
332 }
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct OrderingField {
338 pub field: String,
340 pub descending: bool,
342}
343
344impl OrderingField {
345 pub fn asc(field: impl Into<String>) -> Self {
347 Self {
348 field: field.into(),
349 descending: false,
350 }
351 }
352
353 pub fn desc(field: impl Into<String>) -> Self {
355 Self {
356 field: field.into(),
357 descending: true,
358 }
359 }
360
361 pub fn as_sql(&self) -> String {
363 format!(
364 "{} {}",
365 self.field,
366 if self.descending { "DESC" } else { "ASC" }
367 )
368 }
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct Fieldset {
374 pub name: Option<String>,
376 pub fields: Vec<String>,
378 pub classes: Vec<String>,
380 pub description: Option<String>,
382 pub collapsible: bool,
384 pub collapsed: bool,
386}
387
388impl Fieldset {
389 pub fn new(fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
391 Self {
392 name: None,
393 fields: fields.into_iter().map(Into::into).collect(),
394 classes: Vec::new(),
395 description: None,
396 collapsible: false,
397 collapsed: false,
398 }
399 }
400
401 pub fn named(
403 name: impl Into<String>,
404 fields: impl IntoIterator<Item = impl Into<String>>,
405 ) -> Self {
406 Self {
407 name: Some(name.into()),
408 fields: fields.into_iter().map(Into::into).collect(),
409 classes: Vec::new(),
410 description: None,
411 collapsible: false,
412 collapsed: false,
413 }
414 }
415
416 pub fn description(mut self, desc: impl Into<String>) -> Self {
418 self.description = Some(desc.into());
419 self
420 }
421
422 pub fn collapsible(mut self) -> Self {
424 self.collapsible = true;
425 self
426 }
427
428 pub fn collapsed(mut self) -> Self {
430 self.collapsible = true;
431 self.collapsed = true;
432 self
433 }
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct AdminAction {
439 pub name: String,
441 pub label: String,
443 pub description: Option<String>,
445 pub icon: Option<String>,
447 pub dangerous: bool,
449 pub requires_selection: bool,
451}
452
453impl AdminAction {
454 pub fn new(name: impl Into<String>, label: impl Into<String>) -> Self {
456 Self {
457 name: name.into(),
458 label: label.into(),
459 description: None,
460 icon: None,
461 dangerous: false,
462 requires_selection: true,
463 }
464 }
465
466 pub fn delete() -> Self {
468 Self {
469 name: "delete".to_string(),
470 label: "Delete selected".to_string(),
471 description: Some("Permanently delete selected items".to_string()),
472 icon: Some("trash".to_string()),
473 dangerous: true,
474 requires_selection: true,
475 }
476 }
477
478 pub fn export() -> Self {
480 Self {
481 name: "export".to_string(),
482 label: "Export".to_string(),
483 description: Some("Export selected items to CSV".to_string()),
484 icon: Some("download".to_string()),
485 dangerous: false,
486 requires_selection: false,
487 }
488 }
489
490 pub fn description(mut self, desc: impl Into<String>) -> Self {
492 self.description = Some(desc.into());
493 self
494 }
495
496 pub fn icon(mut self, icon: impl Into<String>) -> Self {
498 self.icon = Some(icon.into());
499 self
500 }
501
502 pub fn dangerous(mut self) -> Self {
504 self.dangerous = true;
505 self
506 }
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct InlineDefinition {
512 pub model: String,
514 pub fk_field: String,
516 pub fields: Vec<String>,
518 pub extra: usize,
520 pub max_num: Option<usize>,
522 pub min_num: usize,
524 pub can_delete: bool,
526 pub verbose_name: Option<String>,
528}
529
530impl InlineDefinition {
531 pub fn new(model: impl Into<String>, fk_field: impl Into<String>) -> Self {
533 Self {
534 model: model.into(),
535 fk_field: fk_field.into(),
536 fields: Vec::new(),
537 extra: 3,
538 max_num: None,
539 min_num: 0,
540 can_delete: true,
541 verbose_name: None,
542 }
543 }
544
545 pub fn fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
547 self.fields = fields.into_iter().map(Into::into).collect();
548 self
549 }
550
551 pub fn extra(mut self, extra: usize) -> Self {
553 self.extra = extra;
554 self
555 }
556
557 pub fn max_num(mut self, max: usize) -> Self {
559 self.max_num = Some(max);
560 self
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn test_model_builder() {
570 let model = ModelDefinition::builder("user")
571 .id_field()
572 .field(FieldDefinition::new("name", FieldType::String).required())
573 .field(FieldDefinition::new("email", FieldType::Email))
574 .timestamps()
575 .list_display(["id", "name", "email"])
576 .search_fields(["name", "email"])
577 .build();
578
579 assert_eq!(model.name, "user");
580 assert_eq!(model.verbose_name, "users");
581 assert_eq!(model.primary_key, "id");
582 assert_eq!(model.fields.len(), 5);
583 }
584
585 #[test]
586 fn test_fieldset() {
587 let fieldset = Fieldset::named("Personal Info", ["name", "email"])
588 .description("User's personal information")
589 .collapsible();
590
591 assert_eq!(fieldset.name, Some("Personal Info".to_string()));
592 assert_eq!(fieldset.fields.len(), 2);
593 assert!(fieldset.collapsible);
594 }
595
596 #[test]
597 fn test_ordering() {
598 let desc = OrderingField::desc("created_at");
599 assert_eq!(desc.as_sql(), "created_at DESC");
600
601 let asc = OrderingField::asc("name");
602 assert_eq!(asc.as_sql(), "name ASC");
603 }
604}