Skip to main content

reinhardt_forms/
model_form.rs

1//! ModelForm implementation for ORM integration
2//!
3//! ModelForms automatically generate forms from ORM models, handling field
4//! inference, validation, and saving.
5
6use crate::{CharField, EmailField, FloatField, Form, FormError, FormField, IntegerField, Widget};
7use serde_json::Value;
8use std::collections::HashMap;
9use std::marker::PhantomData;
10
11/// Field type metadata for ModelForm field inference
12#[derive(Debug, Clone)]
13pub enum FieldType {
14	Char { max_length: Option<usize> },
15	Text,
16	Integer,
17	Float,
18	Boolean,
19	DateTime,
20	Date,
21	Time,
22	Email,
23	Url,
24	Json,
25}
26
27/// Trait for models that can be used with ModelForm
28///
29/// This trait is specifically for form models. For ORM models, use `reinhardt_db::orm::Model`.
30pub trait FormModel: Send + Sync {
31	/// Get the model's field names
32	fn field_names() -> Vec<String>;
33
34	/// Get field type metadata for form field inference
35	///
36	/// # Examples
37	///
38	/// ```no_run
39	/// # use reinhardt_forms::model_form::FieldType;
40	/// fn field_type(name: &str) -> Option<FieldType> {
41	///     match name {
42	///         "name" => Some(FieldType::Char { max_length: Some(100) }),
43	///         "email" => Some(FieldType::Email),
44	///         "age" => Some(FieldType::Integer),
45	///         _ => None,
46	///     }
47	/// }
48	/// ```
49	fn field_type(_name: &str) -> Option<FieldType> {
50		None
51	}
52
53	/// Get a field value by name
54	fn get_field(&self, name: &str) -> Option<Value>;
55
56	/// Set a field value by name
57	fn set_field(&mut self, name: &str, value: Value) -> Result<(), String>;
58
59	/// Save the model to the database
60	fn save(&mut self) -> Result<(), String>;
61
62	/// Validate the model
63	fn validate(&self) -> Result<(), Vec<String>> {
64		Ok(())
65	}
66
67	/// Convert model instance to a choice label for display in forms
68	///
69	/// Default implementation returns the string representation of the primary key.
70	/// Override this method to provide custom display labels.
71	///
72	/// # Examples
73	///
74	/// ```no_run
75	/// # struct Example { id: i32, name: String }
76	/// # impl Example {
77	/// fn to_choice_label(&self) -> String {
78	///     format!("{} - {}", self.id, self.name)
79	/// }
80	/// # }
81	/// ```
82	fn to_choice_label(&self) -> String {
83		// Default: use the "id" field or empty string
84		self.get_field("id")
85			.and_then(|v| v.as_i64().map(|i| i.to_string()))
86			.or_else(|| {
87				self.get_field("id")
88					.and_then(|v| v.as_str().map(|s| s.to_string()))
89			})
90			.unwrap_or_default()
91	}
92
93	/// Get the primary key value as a string for form field validation
94	///
95	/// Default implementation uses the "id" field.
96	///
97	/// # Examples
98	///
99	/// ```no_run
100	/// # struct Example { id: i32 }
101	/// # impl Example {
102	/// fn to_choice_value(&self) -> String {
103	///     self.id.to_string()
104	/// }
105	/// # }
106	/// ```
107	fn to_choice_value(&self) -> String {
108		self.get_field("id")
109			.and_then(|v| v.as_i64().map(|i| i.to_string()))
110			.or_else(|| {
111				self.get_field("id")
112					.and_then(|v| v.as_str().map(|s| s.to_string()))
113			})
114			.unwrap_or_default()
115	}
116}
117
118/// ModelForm configuration
119#[derive(Debug, Clone, Default)]
120pub struct ModelFormConfig {
121	/// Fields to include in the form (None = all fields)
122	pub fields: Option<Vec<String>>,
123	/// Fields to exclude from the form
124	pub exclude: Vec<String>,
125	/// Custom widgets for specific fields
126	pub widgets: HashMap<String, crate::Widget>,
127	/// Custom labels for specific fields
128	pub labels: HashMap<String, String>,
129	/// Custom help text for specific fields
130	pub help_texts: HashMap<String, String>,
131}
132
133impl ModelFormConfig {
134	pub fn new() -> Self {
135		Self::default()
136	}
137	pub fn fields(mut self, fields: Vec<String>) -> Self {
138		self.fields = Some(fields);
139		self
140	}
141	pub fn exclude(mut self, exclude: Vec<String>) -> Self {
142		self.exclude = exclude;
143		self
144	}
145	pub fn widget(mut self, field: String, widget: crate::Widget) -> Self {
146		self.widgets.insert(field, widget);
147		self
148	}
149	pub fn label(mut self, field: String, label: String) -> Self {
150		self.labels.insert(field, label);
151		self
152	}
153	pub fn help_text(mut self, field: String, text: String) -> Self {
154		self.help_texts.insert(field, text);
155		self
156	}
157}
158
159/// A form that is automatically generated from a Model
160pub struct ModelForm<T: FormModel> {
161	form: Form,
162	instance: Option<T>,
163	#[allow(dead_code)]
164	config: ModelFormConfig,
165	_phantom: PhantomData<T>,
166}
167
168impl<T: FormModel> ModelForm<T> {
169	/// Create a form field from field type metadata
170	fn create_form_field(
171		name: &str,
172		field_type: FieldType,
173		config: &ModelFormConfig,
174	) -> Box<dyn FormField> {
175		let label = config.labels.get(name).cloned();
176		let help_text = config.help_texts.get(name).cloned();
177		let widget = config.widgets.get(name).cloned();
178
179		match field_type {
180			FieldType::Char { max_length } => {
181				let mut field = CharField::new(name.to_string());
182				if let Some(label) = label {
183					field.label = Some(label);
184				}
185				if let Some(help) = help_text {
186					field.help_text = Some(help);
187				}
188				if let Some(w) = widget {
189					field.widget = w;
190				}
191				field.max_length = max_length;
192				Box::new(field)
193			}
194			FieldType::Text => {
195				let mut field = CharField::new(name.to_string());
196				if let Some(label) = label {
197					field.label = Some(label);
198				}
199				if let Some(help) = help_text {
200					field.help_text = Some(help);
201				}
202				if let Some(w) = widget {
203					field.widget = w;
204				} else {
205					field.widget = Widget::TextArea;
206				}
207				Box::new(field)
208			}
209			FieldType::Email => {
210				let mut field = EmailField::new(name.to_string());
211				if let Some(label) = label {
212					field.label = Some(label);
213				}
214				if let Some(help) = help_text {
215					field.help_text = Some(help);
216				}
217				if let Some(w) = widget {
218					field.widget = w;
219				}
220				Box::new(field)
221			}
222			FieldType::Integer => {
223				let mut field = IntegerField::new(name.to_string());
224				if let Some(label) = label {
225					field.label = Some(label);
226				}
227				if let Some(help) = help_text {
228					field.help_text = Some(help);
229				}
230				if let Some(w) = widget {
231					field.widget = w;
232				}
233				Box::new(field)
234			}
235			FieldType::Float => {
236				let mut field = FloatField::new(name.to_string());
237				if let Some(label) = label {
238					field.label = Some(label);
239				}
240				if let Some(help) = help_text {
241					field.help_text = Some(help);
242				}
243				if let Some(w) = widget {
244					field.widget = w;
245				}
246				Box::new(field)
247			}
248			// For unsupported types, default to CharField
249			_ => {
250				let mut field = CharField::new(name.to_string());
251				if let Some(label) = label {
252					field.label = Some(label);
253				}
254				if let Some(help) = help_text {
255					field.help_text = Some(help);
256				}
257				if let Some(w) = widget {
258					field.widget = w;
259				}
260				Box::new(field)
261			}
262		}
263	}
264
265	/// Create a new ModelForm from a model instance
266	///
267	/// # Examples
268	///
269	/// ```ignore
270	/// use reinhardt_forms::{ModelForm, ModelFormConfig};
271	///
272	// Assuming we have a model that implements the Model trait
273	/// let config = ModelFormConfig::new();
274	/// let form = ModelForm::new(Some(instance), config);
275	/// ```
276	pub fn new(instance: Option<T>, config: ModelFormConfig) -> Self {
277		let mut form = Form::new();
278
279		// Get field names from model
280		let all_fields = T::field_names();
281
282		// Filter fields based on config
283		let fields_to_include: Vec<String> = if let Some(ref include) = config.fields {
284			include
285				.iter()
286				.filter(|f| !config.exclude.contains(f))
287				.cloned()
288				.collect()
289		} else {
290			all_fields
291				.iter()
292				.filter(|f| !config.exclude.contains(f))
293				.cloned()
294				.collect()
295		};
296
297		// Infer field types from model metadata and add to form
298		for field_name in &fields_to_include {
299			if let Some(field_type) = T::field_type(field_name) {
300				let form_field = Self::create_form_field(field_name, field_type, &config);
301				form.add_field(form_field);
302			}
303		}
304
305		// If instance exists, populate initial data from the instance
306		if let Some(ref inst) = instance {
307			let mut initial = HashMap::new();
308			for field_name in &fields_to_include {
309				if let Some(value) = inst.get_field(field_name) {
310					initial.insert(field_name.clone(), value);
311				}
312			}
313			form.bind(initial);
314		}
315
316		Self {
317			form,
318			instance,
319			config,
320			_phantom: PhantomData,
321		}
322	}
323	/// Create a new ModelForm without an instance (for creation)
324	///
325	/// # Examples
326	///
327	/// ```ignore
328	/// use reinhardt_forms::{ModelForm, ModelFormConfig};
329	///
330	/// let config = ModelFormConfig::new();
331	/// let form = ModelForm::<MyModel>::empty(config);
332	/// ```
333	pub fn empty(config: ModelFormConfig) -> Self {
334		Self::new(None, config)
335	}
336	/// Bind data to the form
337	///
338	/// # Examples
339	///
340	/// ```ignore
341	/// use reinhardt_forms::{ModelForm, ModelFormConfig};
342	/// use std::collections::HashMap;
343	/// use serde_json::json;
344	///
345	/// let config = ModelFormConfig::new();
346	/// let mut form = ModelForm::<MyModel>::empty(config);
347	/// let mut data = HashMap::new();
348	/// data.insert("field".to_string(), json!("value"));
349	/// form.bind(data);
350	/// ```
351	pub fn bind(&mut self, data: HashMap<String, Value>) -> &mut Self {
352		// Bind data to the underlying form
353		self.form.bind(data);
354		self
355	}
356	/// Check if the form is valid
357	///
358	/// # Examples
359	///
360	/// ```ignore
361	/// use reinhardt_forms::{ModelForm, ModelFormConfig};
362	///
363	/// let config = ModelFormConfig::new();
364	/// let mut form = ModelForm::<MyModel>::empty(config);
365	/// let is_valid = form.is_valid();
366	/// ```
367	pub fn is_valid(&mut self) -> bool {
368		// Validate the model if instance exists
369		if let Some(ref instance) = self.instance
370			&& let Err(_errors) = instance.validate()
371		{
372			return false;
373		}
374
375		true
376	}
377	/// Save the form data to the model instance
378	///
379	/// Returns `FormError::NoInstance` if no model instance is available.
380	///
381	/// # Examples
382	///
383	/// ```ignore
384	/// use reinhardt_forms::{ModelForm, ModelFormConfig};
385	///
386	/// let config = ModelFormConfig::new();
387	/// let mut form = ModelForm::<MyModel>::empty(config);
388	/// // Returns Err(FormError::NoInstance) without an instance
389	/// assert!(form.save().is_err());
390	/// ```
391	pub fn save(&mut self) -> Result<T, FormError> {
392		if !self.is_valid() {
393			return Err(FormError::Validation("Form is not valid".to_string()));
394		}
395
396		// Get existing instance or return error
397		let mut instance = self.instance.take().ok_or(FormError::NoInstance)?;
398
399		// Set field values from form's cleaned_data
400		let cleaned_data = self.form.cleaned_data();
401		for (field_name, value) in cleaned_data.iter() {
402			if let Err(e) = instance.set_field(field_name, value.clone()) {
403				return Err(FormError::Validation(format!(
404					"Failed to set field {}: {}",
405					field_name, e
406				)));
407			}
408		}
409
410		// Save the instance
411		if let Err(e) = instance.save() {
412			return Err(FormError::Validation(format!("Failed to save: {}", e)));
413		}
414
415		Ok(instance)
416	}
417	/// Set a field value directly on the model instance.
418	///
419	/// This is used by `InlineFormSet` to set foreign key values on child
420	/// instances before saving.
421	///
422	/// If no instance exists, this method is a no-op.
423	pub fn set_field_value(&mut self, field_name: &str, value: Value) {
424		if let Some(ref mut instance) = self.instance {
425			// Silently ignore errors from set_field, as the field may not exist
426			// on all model types (defensive approach for inline formsets)
427			let _ = instance.set_field(field_name, value);
428		}
429	}
430
431	pub fn form(&self) -> &Form {
432		&self.form
433	}
434	pub fn form_mut(&mut self) -> &mut Form {
435		&mut self.form
436	}
437	pub fn instance(&self) -> Option<&T> {
438		self.instance.as_ref()
439	}
440}
441
442/// Builder for creating ModelForm instances
443pub struct ModelFormBuilder<T: FormModel> {
444	config: ModelFormConfig,
445	_phantom: PhantomData<T>,
446}
447
448impl<T: FormModel> ModelFormBuilder<T> {
449	pub fn new() -> Self {
450		Self {
451			config: ModelFormConfig::default(),
452			_phantom: PhantomData,
453		}
454	}
455	pub fn fields(mut self, fields: Vec<String>) -> Self {
456		self.config.fields = Some(fields);
457		self
458	}
459	pub fn exclude(mut self, exclude: Vec<String>) -> Self {
460		self.config.exclude = exclude;
461		self
462	}
463	pub fn widget(mut self, field: String, widget: crate::Widget) -> Self {
464		self.config.widgets.insert(field, widget);
465		self
466	}
467	pub fn label(mut self, field: String, label: String) -> Self {
468		self.config.labels.insert(field, label);
469		self
470	}
471	pub fn help_text(mut self, field: String, text: String) -> Self {
472		self.config.help_texts.insert(field, text);
473		self
474	}
475	/// Build the ModelForm with the configured settings
476	///
477	/// # Examples
478	///
479	/// ```ignore
480	/// use reinhardt_forms::{ModelFormBuilder, ModelFormConfig};
481	///
482	/// let config = ModelFormConfig::new();
483	/// let builder = ModelFormBuilder::<MyModel>::new();
484	/// let form = builder.build(None);
485	/// ```
486	pub fn build(self, instance: Option<T>) -> ModelForm<T> {
487		ModelForm::new(instance, self.config)
488	}
489}
490
491impl<T: FormModel> Default for ModelFormBuilder<T> {
492	fn default() -> Self {
493		Self::new()
494	}
495}
496
497#[cfg(test)]
498mod tests {
499	use super::*;
500	use rstest::rstest;
501
502	// Mock model for testing
503	#[derive(Debug)]
504	struct TestModel {
505		id: i32,
506		name: String,
507		email: String,
508	}
509
510	impl FormModel for TestModel {
511		fn field_names() -> Vec<String> {
512			vec!["id".to_string(), "name".to_string(), "email".to_string()]
513		}
514
515		fn field_type(name: &str) -> Option<FieldType> {
516			match name {
517				"id" => Some(FieldType::Integer),
518				"name" => Some(FieldType::Char {
519					max_length: Some(100),
520				}),
521				"email" => Some(FieldType::Email),
522				_ => None,
523			}
524		}
525
526		fn get_field(&self, name: &str) -> Option<Value> {
527			match name {
528				"id" => Some(Value::Number(self.id.into())),
529				"name" => Some(Value::String(self.name.clone())),
530				"email" => Some(Value::String(self.email.clone())),
531				_ => None,
532			}
533		}
534
535		fn set_field(&mut self, name: &str, value: Value) -> Result<(), String> {
536			match name {
537				"id" => {
538					if let Value::Number(n) = value {
539						self.id = n.as_i64().unwrap() as i32;
540						Ok(())
541					} else {
542						Err("Invalid type for id".to_string())
543					}
544				}
545				"name" => {
546					if let Value::String(s) = value {
547						self.name = s;
548						Ok(())
549					} else {
550						Err("Invalid type for name".to_string())
551					}
552				}
553				"email" => {
554					if let Value::String(s) = value {
555						self.email = s;
556						Ok(())
557					} else {
558						Err("Invalid type for email".to_string())
559					}
560				}
561				_ => Err(format!("Unknown field: {}", name)),
562			}
563		}
564
565		fn save(&mut self) -> Result<(), String> {
566			// Mock save
567			Ok(())
568		}
569	}
570
571	#[rstest]
572	fn test_model_form_config() {
573		// Arrange
574		let config = ModelFormConfig::new()
575			.fields(vec!["name".to_string(), "email".to_string()])
576			.exclude(vec!["id".to_string()]);
577
578		// Assert
579		assert_eq!(
580			config.fields,
581			Some(vec!["name".to_string(), "email".to_string()])
582		);
583		assert_eq!(config.exclude, vec!["id".to_string()]);
584	}
585
586	#[rstest]
587	fn test_model_form_builder() {
588		// Arrange
589		let instance = TestModel {
590			id: 1,
591			name: "John".to_string(),
592			email: "john@example.com".to_string(),
593		};
594
595		// Act
596		let form = ModelFormBuilder::<TestModel>::new()
597			.fields(vec!["name".to_string(), "email".to_string()])
598			.build(Some(instance));
599
600		// Assert
601		assert!(form.instance().is_some());
602	}
603
604	#[rstest]
605	fn test_model_field_names() {
606		// Act
607		let fields = TestModel::field_names();
608
609		// Assert
610		assert_eq!(
611			fields,
612			vec!["id".to_string(), "name".to_string(), "email".to_string()]
613		);
614	}
615
616	#[rstest]
617	fn test_save_without_instance_returns_no_instance_error() {
618		// Arrange
619		let config = ModelFormConfig::new();
620		let mut form = ModelForm::<TestModel>::empty(config);
621
622		// Act
623		let result = form.save();
624
625		// Assert
626		assert!(result.is_err());
627		let err = result.unwrap_err();
628		assert!(
629			matches!(err, FormError::NoInstance),
630			"Expected FormError::NoInstance, got: {err}"
631		);
632	}
633}