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.verbose_name_singular.unwrap_or_else(|| {
293 self.name.replace('_', " ")
294 });
295
296 let table_name = self.table_name.unwrap_or_else(|| {
297 self.name.to_lowercase().replace(' ', "_")
298 });
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(name: impl Into<String>, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
403 Self {
404 name: Some(name.into()),
405 fields: fields.into_iter().map(Into::into).collect(),
406 classes: Vec::new(),
407 description: None,
408 collapsible: false,
409 collapsed: false,
410 }
411 }
412
413 pub fn description(mut self, desc: impl Into<String>) -> Self {
415 self.description = Some(desc.into());
416 self
417 }
418
419 pub fn collapsible(mut self) -> Self {
421 self.collapsible = true;
422 self
423 }
424
425 pub fn collapsed(mut self) -> Self {
427 self.collapsible = true;
428 self.collapsed = true;
429 self
430 }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct AdminAction {
436 pub name: String,
438 pub label: String,
440 pub description: Option<String>,
442 pub icon: Option<String>,
444 pub dangerous: bool,
446 pub requires_selection: bool,
448}
449
450impl AdminAction {
451 pub fn new(name: impl Into<String>, label: impl Into<String>) -> Self {
453 Self {
454 name: name.into(),
455 label: label.into(),
456 description: None,
457 icon: None,
458 dangerous: false,
459 requires_selection: true,
460 }
461 }
462
463 pub fn delete() -> Self {
465 Self {
466 name: "delete".to_string(),
467 label: "Delete selected".to_string(),
468 description: Some("Permanently delete selected items".to_string()),
469 icon: Some("trash".to_string()),
470 dangerous: true,
471 requires_selection: true,
472 }
473 }
474
475 pub fn export() -> Self {
477 Self {
478 name: "export".to_string(),
479 label: "Export".to_string(),
480 description: Some("Export selected items to CSV".to_string()),
481 icon: Some("download".to_string()),
482 dangerous: false,
483 requires_selection: false,
484 }
485 }
486
487 pub fn description(mut self, desc: impl Into<String>) -> Self {
489 self.description = Some(desc.into());
490 self
491 }
492
493 pub fn icon(mut self, icon: impl Into<String>) -> Self {
495 self.icon = Some(icon.into());
496 self
497 }
498
499 pub fn dangerous(mut self) -> Self {
501 self.dangerous = true;
502 self
503 }
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct InlineDefinition {
509 pub model: String,
511 pub fk_field: String,
513 pub fields: Vec<String>,
515 pub extra: usize,
517 pub max_num: Option<usize>,
519 pub min_num: usize,
521 pub can_delete: bool,
523 pub verbose_name: Option<String>,
525}
526
527impl InlineDefinition {
528 pub fn new(model: impl Into<String>, fk_field: impl Into<String>) -> Self {
530 Self {
531 model: model.into(),
532 fk_field: fk_field.into(),
533 fields: Vec::new(),
534 extra: 3,
535 max_num: None,
536 min_num: 0,
537 can_delete: true,
538 verbose_name: None,
539 }
540 }
541
542 pub fn fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
544 self.fields = fields.into_iter().map(Into::into).collect();
545 self
546 }
547
548 pub fn extra(mut self, extra: usize) -> Self {
550 self.extra = extra;
551 self
552 }
553
554 pub fn max_num(mut self, max: usize) -> Self {
556 self.max_num = Some(max);
557 self
558 }
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
566 fn test_model_builder() {
567 let model = ModelDefinition::builder("user")
568 .id_field()
569 .field(FieldDefinition::new("name", FieldType::String).required())
570 .field(FieldDefinition::new("email", FieldType::Email))
571 .timestamps()
572 .list_display(["id", "name", "email"])
573 .search_fields(["name", "email"])
574 .build();
575
576 assert_eq!(model.name, "user");
577 assert_eq!(model.verbose_name, "users");
578 assert_eq!(model.primary_key, "id");
579 assert_eq!(model.fields.len(), 5);
580 }
581
582 #[test]
583 fn test_fieldset() {
584 let fieldset = Fieldset::named("Personal Info", ["name", "email"])
585 .description("User's personal information")
586 .collapsible();
587
588 assert_eq!(fieldset.name, Some("Personal Info".to_string()));
589 assert_eq!(fieldset.fields.len(), 2);
590 assert!(fieldset.collapsible);
591 }
592
593 #[test]
594 fn test_ordering() {
595 let desc = OrderingField::desc("created_at");
596 assert_eq!(desc.as_sql(), "created_at DESC");
597
598 let asc = OrderingField::asc("name");
599 assert_eq!(asc.as_sql(), "name ASC");
600 }
601}
602